Before sending a fix for #18991, this adds an integration test that locks in that the proxymap WhoIs code works with two nodes running as different users, with the second node running a localhost service and able to use its local tailscaled to identify a Tailscale connection from the other tailscaled. Updates #18991 Change-Id: I6fbb0810204d77d2ac558f0cc786b73e3248d031 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
96dde53b43
commit
4c91f90776
@ -0,0 +1,152 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package integration |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"testing" |
||||
"time" |
||||
|
||||
"tailscale.com/tstest" |
||||
) |
||||
|
||||
// TestUserspaceWhoIsProxyMap verifies that WhoIs lookups work via the
|
||||
// proxymap in userspace-networking mode. It sets up two nodes (n1 and
|
||||
// n2), starts a TCP listener on localhost, and has n1 connect to n2's
|
||||
// Tailscale IP on the listener's port via "tailscale nc". Node n2's
|
||||
// netstack forwards the connection to localhost, and the listener
|
||||
// calls WhoIs on n2's LocalAPI to identify the remote peer as n1.
|
||||
func TestUserspaceWhoIsProxyMap(t *testing.T) { |
||||
tstest.Shard(t) |
||||
tstest.Parallel(t) |
||||
env := NewTestEnv(t) |
||||
|
||||
n1 := NewTestNode(t, env) |
||||
d1 := n1.StartDaemon() |
||||
|
||||
n2 := NewTestNode(t, env) |
||||
d2 := n2.StartDaemon() |
||||
|
||||
n1.AwaitListening() |
||||
n2.AwaitListening() |
||||
n1.MustUp() |
||||
n2.MustUp() |
||||
n1.AwaitRunning() |
||||
n2.AwaitRunning() |
||||
|
||||
// Wait for n1 to see n2 as a peer.
|
||||
if err := tstest.WaitFor(10*time.Second, func() error { |
||||
st := n1.MustStatus() |
||||
if len(st.Peer) == 0 { |
||||
return errors.New("no peers") |
||||
} |
||||
return nil |
||||
}); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Verify the two nodes have different users. If they were the
|
||||
// same user, a WhoIs hit could pass trivially.
|
||||
st1 := n1.MustStatus() |
||||
st2 := n2.MustStatus() |
||||
if st1.Self.UserID == st2.Self.UserID { |
||||
t.Fatalf("n1 and n2 have the same UserID %v; want different users", st1.Self.UserID) |
||||
} |
||||
t.Logf("n1: UserID=%v", st1.Self.UserID) |
||||
t.Logf("n2: UserID=%v", st2.Self.UserID) |
||||
|
||||
n2IP := n2.AwaitIP4() |
||||
t.Logf("n2 IP: %v", n2IP) |
||||
|
||||
// Start a TCP listener on localhost:0. When n1 connects to n2's
|
||||
// Tailscale IP on this port, n2's netstack (userspace networking)
|
||||
// will forward the connection to 127.0.0.1:<port>. The listener
|
||||
// uses n2's LocalAPI WhoIs to identify the connecting peer.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer ln.Close() |
||||
port := ln.Addr().(*net.TCPAddr).Port |
||||
t.Logf("listener on port %d", port) |
||||
|
||||
type result struct { |
||||
msg string |
||||
err error |
||||
} |
||||
resultCh := make(chan result, 1) |
||||
|
||||
go func() { |
||||
conn, err := ln.Accept() |
||||
if err != nil { |
||||
resultCh <- result{err: fmt.Errorf("accept: %w", err)} |
||||
return |
||||
} |
||||
defer conn.Close() |
||||
|
||||
// The RemoteAddr is 127.0.0.1:<ephemeral>, the local side of
|
||||
// n2's netstack dial. WhoIs on n2 should resolve this via the
|
||||
// proxymap to n1's Tailscale identity.
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) |
||||
defer cancel() |
||||
who, err := n2.LocalClient().WhoIs(ctx, conn.RemoteAddr().String()) |
||||
if err != nil { |
||||
resultCh <- result{err: fmt.Errorf("WhoIs(%q): %w", conn.RemoteAddr(), err)} |
||||
return |
||||
} |
||||
if who.Node == nil { |
||||
resultCh <- result{err: errors.New("WhoIs returned nil Node")} |
||||
return |
||||
} |
||||
if who.UserProfile == nil { |
||||
resultCh <- result{err: errors.New("WhoIs returned nil UserProfile")} |
||||
return |
||||
} |
||||
|
||||
msg := fmt.Sprintf("Hello, %s (%v %v)!", |
||||
who.UserProfile.LoginName, who.Node.Name, who.Node.ID) |
||||
conn.Write([]byte(msg)) |
||||
resultCh <- result{msg: msg} |
||||
}() |
||||
|
||||
// Use "tailscale nc" on n1 to connect to n2's Tailscale IP on
|
||||
// the listener port. This goes through n1's tailscaled, over
|
||||
// wireguard to n2's netstack, which dials localhost:<port>.
|
||||
//
|
||||
// We need to keep stdin open so nc doesn't exit before reading
|
||||
// the server's response (nc returns on the first goroutine to
|
||||
// complete: stdin→conn or conn→stdout).
|
||||
cmd := n1.TailscaleForOutput("nc", n2IP.String(), fmt.Sprint(port)) |
||||
stdin, err := cmd.StdinPipe() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
out, err := cmd.Output() |
||||
stdin.Close() |
||||
if err != nil { |
||||
t.Fatalf("tailscale nc: %v", err) |
||||
} |
||||
|
||||
// Verify the listener goroutine completed without error.
|
||||
r := <-resultCh |
||||
if r.err != nil { |
||||
t.Fatal(r.err) |
||||
} |
||||
|
||||
got := string(out) |
||||
if got != r.msg { |
||||
t.Fatalf("nc output %q doesn't match server-sent message %q", got, r.msg) |
||||
} |
||||
const wantPrefix = "Hello, user-1@fake-control.example.net (" |
||||
if len(got) < len(wantPrefix) || got[:len(wantPrefix)] != wantPrefix { |
||||
t.Errorf("got %q, want prefix %q", got, wantPrefix) |
||||
} |
||||
t.Logf("response: %s", got) |
||||
|
||||
d1.MustCleanShutdown(t) |
||||
d2.MustCleanShutdown(t) |
||||
} |
||||
Loading…
Reference in new issue