tstest/integration: add userspace-networking + proxymap WhoIs integration test
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>
This commit is contained in:
committed by
Brad Fitzpatrick
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)
|
||||
}
|
||||
Reference in New Issue
Block a user