Expand Up @@ -28,8 +28,10 @@ import ( "golang.org/x/xerrors" "tailscale.com/derp" "tailscale.com/derp/derphttp" "tailscale.com/net/packet" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/wgengine/capture" "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" Expand All @@ -54,35 +56,36 @@ type Client struct { ID uuid.UUID ListenPort uint16 ShouldRunTests bool TunnelSrc bool } var Client1 = Client{ Number: ClientNumber1, ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), ListenPort: client1Port, ShouldRunTests: true, TunnelSrc: true, } var Client2 = Client{ Number: ClientNumber2, ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), ListenPort: client2Port, ShouldRunTests: false, TunnelSrc: false, } type TestTopology struct { Name string // SetupNetworking creates interfaces and network namespaces for the test. // The most simple implementation is NetworkSetupDefault, which only creates // a network namespace shared for all tests. SetupNetworking func(t *testing.T, logger slog.Logger) TestNetworking NetworkingProvider NetworkingProvider // Server is the server starter for the test. It is executed in the server // subprocess. Server ServerStarter // StartClient gets called in each client subprocess. It's expected to //ClientStarter. StartClient gets called in each client subprocess. It's expected to // create the tailnet.Conn and ensure connectivity to it's peer. StartClient func(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me Client, peer Client) *tailnet.Conn ClientStarter ClientStarter // RunTests is the main test function. It's called in each of the client // subprocesses. If tests can only run once, they should check the client ID Expand All @@ -97,6 +100,17 @@ type ServerStarter interface { StartServer(t *testing.T, logger slog.Logger, listenAddr string) } type NetworkingProvider interface { // SetupNetworking creates interfaces and network namespaces for the test. // The most simple implementation is NetworkSetupDefault, which only creates // a network namespace shared for all tests. SetupNetworking(t *testing.T, logger slog.Logger) TestNetworking } type ClientStarter interface { StartClient(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me Client, peer Client) *tailnet.Conn } type SimpleServerOptions struct { // FailUpgradeDERP will make the DERP server fail to handle the initial DERP // upgrade in a way that causes the client to fallback to Expand Down Expand Up @@ -369,77 +383,107 @@ http { _, _ = ExecBackground(t, "server.nginx", nil, "nginx", []string{"-c", cfgPath}) } // StartClientDERP creates a client connection to the server for coordination // and creates a tailnet.Conn which will only use DERP to connect to the peer. func StartClientDERP(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn { return startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{ Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)}, DERPMap: derpMap, BlockEndpoints: true, Logger: logger, DERPForceWebSockets: false, ListenPort: me.ListenPort, // These tests don't have internet connection, so we need to force // magicsock to do anything. ForceNetworkUp: true, }) type BasicClientStarter struct { BlockEndpoints bool DERPForceWebsockets bool // WaitForConnection means wait for (any) peer connection before returning from StartClient WaitForConnection bool // WaitForConnection means wait for a direct peer connection before returning from StartClient WaitForDirect bool // Service is a network service (e.g. an echo server) to start on the client. If Wait* is set, the service is // started prior to waiting. Service NetworkService LogPackets bool } // StartClientDERPWebSockets does the same thing as StartClientDERP but will // only use DERP WebSocket fallback. func StartClientDERPWebSockets(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn { return startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{ Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)}, DERPMap: derpMap, BlockEndpoints: true, Logger: logger, DERPForceWebSockets: true, ListenPort: me.ListenPort, // These tests don't have internet connection, so we need to force // magicsock to do anything. ForceNetworkUp: true, }) type NetworkService interface { StartService(t *testing.T, logger slog.Logger, conn *tailnet.Conn) } // StartClientDirect does the same thing as StartClientDERP but disables // BlockEndpoints (which enables Direct connections), and waits for a direct // connection to be established between the two peers. func StartClientDirect(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn { func (b BasicClientStarter) StartClient(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn { var hook capture.Callback if b.LogPackets { pktLogger := packetLogger{logger} hook = pktLogger.LogPacket } conn := startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{ Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)}, DERPMap: derpMap, BlockEndpoints:false , BlockEndpoints:b.BlockEndpoints , Logger: logger, DERPForceWebSockets:true , DERPForceWebSockets:b.DERPForceWebsockets , ListenPort: me.ListenPort, // These tests don't have internet connection, so we need to force // magicsock to do anything. ForceNetworkUp: true, CaptureHook: hook, }) // Wait for direct connection to be established. peerIP := tailnet.TailscaleServicePrefix.AddrFromUUID(peer.ID) require.Eventually(t, func() bool { t.Log("attempting ping to peer to judge direct connection") ctx := testutil.Context(t, testutil.WaitShort) _, p2p, pong, err := conn.Ping(ctx, peerIP) if err != nil { t.Logf("ping failed: %v", err) return false } if !p2p { t.Log("ping succeeded, but not direct yet") return false } t.Logf("ping succeeded, direct connection established via %s", pong.Endpoint) return true }, testutil.WaitLong, testutil.IntervalMedium) if b.Service != nil { b.Service.StartService(t, logger, conn) } if b.WaitForConnection || b.WaitForDirect { // Wait for connection to be established. peerIP := tailnet.TailscaleServicePrefix.AddrFromUUID(peer.ID) require.Eventually(t, func() bool { t.Log("attempting ping to peer to judge direct connection") ctx := testutil.Context(t, testutil.WaitShort) _, p2p, pong, err := conn.Ping(ctx, peerIP) if err != nil { t.Logf("ping failed: %v", err) return false } if !p2p && b.WaitForDirect { t.Log("ping succeeded, but not direct yet") return false } t.Logf("ping succeeded, p2p=%t, endpoint=%s", p2p, pong.Endpoint) return true }, testutil.WaitLong, testutil.IntervalMedium) } return conn } type ClientStarter struct { Options *tailnet.Options const EchoPort = 2381 type UDPEchoService struct{} func (UDPEchoService) StartService(t *testing.T, logger slog.Logger, _ *tailnet.Conn) { // tailnet doesn't handle UDP connections "in-process" the way we do for TCP, so we need to listen in the OS, // and tailnet will forward packets. l, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv6zero, // all interfaces Port: EchoPort, }) require.NoError(t, err) logger.Info(context.Background(), "started UDPEcho server") t.Cleanup(func() { lCloseErr := l.Close() if lCloseErr != nil { t.Logf("error closing UDPEcho listener: %v", lCloseErr) } }) go func() { buf := make([]byte, 1500) for { n, remote, readErr := l.ReadFromUDP(buf) if readErr != nil { logger.Info(context.Background(), "error reading UDPEcho listener", slog.Error(readErr)) return } logger.Info(context.Background(), "received UDPEcho packet", slog.F("len", n), slog.F("remote", remote)) n, writeErr := l.WriteToUDP(buf[:n], remote) if writeErr != nil { logger.Info(context.Background(), "error writing UDPEcho listener", slog.Error(writeErr)) return } logger.Info(context.Background(), "wrote UDPEcho packet", slog.F("len", n), slog.F("remote", remote)) } }() } func startClientOptions(t *testing.T, logger slog.Logger, serverURL *url.URL, me, peer Client, options *tailnet.Options) *tailnet.Conn { Expand Down Expand Up @@ -467,9 +511,16 @@ func startClientOptions(t *testing.T, logger slog.Logger, serverURL *url.URL, me _ = conn.Close() }) ctrl := tailnet.NewTunnelSrcCoordController(logger, conn) ctrl.AddDestination(peer.ID) coordination := ctrl.New(coord) var coordination tailnet.CloserWaiter if me.TunnelSrc { ctrl := tailnet.NewTunnelSrcCoordController(logger, conn) ctrl.AddDestination(peer.ID) coordination = ctrl.New(coord) } else { // use the "Agent" controller so that we act as a tunnel destination and send "ReadyForHandshake" acks. ctrl := tailnet.NewAgentCoordinationController(logger, conn) coordination = ctrl.New(coord) } t.Cleanup(func() { cctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) defer cancel() Expand All @@ -492,11 +543,17 @@ func basicDERPMap(serverURLStr string) (*tailcfg.DERPMap, error) { } hostname := serverURL.Hostname() ipv4 := "" ipv4 := "none" ipv6 := "none" ip, err := netip.ParseAddr(hostname) if err == nil { hostname = "" ipv4 = ip.String() if ip.Is4() { ipv4 = ip.String() } if ip.Is6() { ipv6 = ip.String() } } return &tailcfg.DERPMap{ Expand All @@ -511,7 +568,7 @@ func basicDERPMap(serverURLStr string) (*tailcfg.DERPMap, error) { RegionID: 1, HostName: hostname, IPv4: ipv4, IPv6:"none" , IPv6:ipv6 , DERPPort: port, STUNPort: -1, ForceHTTP: true, Expand Down Expand Up @@ -648,3 +705,35 @@ func (w *testWriter) Flush() { } w.capturedLines = nil } type packetLogger struct { l slog.Logger } func (p packetLogger) LogPacket(path capture.Path, when time.Time, pkt []byte, _ packet.CaptureMeta) { q := new(packet.Parsed) q.Decode(pkt) p.l.Info(context.Background(), "Packet", slog.F("path", pathString(path)), slog.F("when", when), slog.F("decode", q.String()), slog.F("len", len(pkt)), ) } func pathString(path capture.Path) string { switch path { case capture.FromLocal: return "Local" case capture.FromPeer: return "Peer" case capture.SynthesizedToLocal: return "SynthesizedToLocal" case capture.SynthesizedToPeer: return "SynthesizedToPeer" case capture.PathDisco: return "Disco" default: return "<<UNKNOWN>>" } }