Skip to content

Commit 08eff7f

Browse files
authored
chore: improve tailnet integration test (coder#18124)
Refactors tailnet integration test and adds UDP echo tests with different MTU related to coder#15523 I still haven't gotten to the bottom of what's causing the issue (the added test case I expected to fail actually succeeds), but these integration test improvements are generally useful. also: * consolidates networking setup with easy and hard NAT * consolidates client setup * makes Client2 act like an agent at the tailnet layer, so it will send ReadyForHandshake and speed up the tunnel establishment * adds support for logging tunneled packets * adds support for dumping outer (underlay) IP traffic * adds support for adjusting veth MTU * adds support for IPv6 in the outer (underlay) network topology
1 parent 628b81c commit 08eff7f

File tree

4 files changed

+464
-196
lines changed

4 files changed

+464
-196
lines changed

tailnet/test/integration/integration.go

Lines changed: 154 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import (
2828
"golang.org/x/xerrors"
2929
"tailscale.com/derp"
3030
"tailscale.com/derp/derphttp"
31+
"tailscale.com/net/packet"
3132
"tailscale.com/tailcfg"
3233
"tailscale.com/types/key"
34+
"tailscale.com/wgengine/capture"
3335

3436
"cdr.dev/slog"
3537
"github.com/coder/coder/v2/coderd/httpapi"
@@ -54,35 +56,36 @@ type Client struct {
5456
ID uuid.UUID
5557
ListenPort uint16
5658
ShouldRunTests bool
59+
TunnelSrc bool
5760
}
5861

5962
var Client1 = Client{
6063
Number: ClientNumber1,
6164
ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
6265
ListenPort: client1Port,
6366
ShouldRunTests: true,
67+
TunnelSrc: true,
6468
}
6569

6670
var Client2 = Client{
6771
Number: ClientNumber2,
6872
ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"),
6973
ListenPort: client2Port,
7074
ShouldRunTests: false,
75+
TunnelSrc: false,
7176
}
7277

7378
type TestTopology struct {
7479
Name string
75-
// SetupNetworking creates interfaces and network namespaces for the test.
76-
// The most simple implementation is NetworkSetupDefault, which only creates
77-
// a network namespace shared for all tests.
78-
SetupNetworking func(t *testing.T, logger slog.Logger) TestNetworking
80+
81+
NetworkingProvider NetworkingProvider
7982

8083
// Server is the server starter for the test. It is executed in the server
8184
// subprocess.
8285
Server ServerStarter
83-
// StartClient gets called in each client subprocess. It's expected to
86+
// ClientStarter.StartClient gets called in each client subprocess. It's expected to
8487
// create the tailnet.Conn and ensure connectivity to it's peer.
85-
StartClient func(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me Client, peer Client) *tailnet.Conn
88+
ClientStarter ClientStarter
8689

8790
// RunTests is the main test function. It's called in each of the client
8891
// subprocesses. If tests can only run once, they should check the client ID
@@ -97,6 +100,17 @@ type ServerStarter interface {
97100
StartServer(t *testing.T, logger slog.Logger, listenAddr string)
98101
}
99102

103+
type NetworkingProvider interface {
104+
// SetupNetworking creates interfaces and network namespaces for the test.
105+
// The most simple implementation is NetworkSetupDefault, which only creates
106+
// a network namespace shared for all tests.
107+
SetupNetworking(t *testing.T, logger slog.Logger) TestNetworking
108+
}
109+
110+
type ClientStarter interface {
111+
StartClient(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me Client, peer Client) *tailnet.Conn
112+
}
113+
100114
type SimpleServerOptions struct {
101115
// FailUpgradeDERP will make the DERP server fail to handle the initial DERP
102116
// upgrade in a way that causes the client to fallback to
@@ -369,77 +383,107 @@ http {
369383
_, _ = ExecBackground(t, "server.nginx", nil, "nginx", []string{"-c", cfgPath})
370384
}
371385

372-
// StartClientDERP creates a client connection to the server for coordination
373-
// and creates a tailnet.Conn which will only use DERP to connect to the peer.
374-
func StartClientDERP(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn {
375-
return startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{
376-
Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)},
377-
DERPMap: derpMap,
378-
BlockEndpoints: true,
379-
Logger: logger,
380-
DERPForceWebSockets: false,
381-
ListenPort: me.ListenPort,
382-
// These tests don't have internet connection, so we need to force
383-
// magicsock to do anything.
384-
ForceNetworkUp: true,
385-
})
386+
type BasicClientStarter struct {
387+
BlockEndpoints bool
388+
DERPForceWebsockets bool
389+
// WaitForConnection means wait for (any) peer connection before returning from StartClient
390+
WaitForConnection bool
391+
// WaitForConnection means wait for a direct peer connection before returning from StartClient
392+
WaitForDirect bool
393+
// Service is a network service (e.g. an echo server) to start on the client. If Wait* is set, the service is
394+
// started prior to waiting.
395+
Service NetworkService
396+
LogPackets bool
386397
}
387398

388-
// StartClientDERPWebSockets does the same thing as StartClientDERP but will
389-
// only use DERP WebSocket fallback.
390-
func StartClientDERPWebSockets(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn {
391-
return startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{
392-
Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)},
393-
DERPMap: derpMap,
394-
BlockEndpoints: true,
395-
Logger: logger,
396-
DERPForceWebSockets: true,
397-
ListenPort: me.ListenPort,
398-
// These tests don't have internet connection, so we need to force
399-
// magicsock to do anything.
400-
ForceNetworkUp: true,
401-
})
399+
type NetworkService interface {
400+
StartService(t *testing.T, logger slog.Logger, conn *tailnet.Conn)
402401
}
403402

404-
// StartClientDirect does the same thing as StartClientDERP but disables
405-
// BlockEndpoints (which enables Direct connections), and waits for a direct
406-
// connection to be established between the two peers.
407-
func StartClientDirect(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn {
403+
func (b BasicClientStarter) StartClient(t *testing.T, logger slog.Logger, serverURL *url.URL, derpMap *tailcfg.DERPMap, me, peer Client) *tailnet.Conn {
404+
var hook capture.Callback
405+
if b.LogPackets {
406+
pktLogger := packetLogger{logger}
407+
hook = pktLogger.LogPacket
408+
}
408409
conn := startClientOptions(t, logger, serverURL, me, peer, &tailnet.Options{
409410
Addresses: []netip.Prefix{tailnet.TailscaleServicePrefix.PrefixFromUUID(me.ID)},
410411
DERPMap: derpMap,
411-
BlockEndpoints: false,
412+
BlockEndpoints: b.BlockEndpoints,
412413
Logger: logger,
413-
DERPForceWebSockets: true,
414+
DERPForceWebSockets: b.DERPForceWebsockets,
414415
ListenPort: me.ListenPort,
415416
// These tests don't have internet connection, so we need to force
416417
// magicsock to do anything.
417418
ForceNetworkUp: true,
419+
CaptureHook: hook,
418420
})
419421

420-
// Wait for direct connection to be established.
421-
peerIP := tailnet.TailscaleServicePrefix.AddrFromUUID(peer.ID)
422-
require.Eventually(t, func() bool {
423-
t.Log("attempting ping to peer to judge direct connection")
424-
ctx := testutil.Context(t, testutil.WaitShort)
425-
_, p2p, pong, err := conn.Ping(ctx, peerIP)
426-
if err != nil {
427-
t.Logf("ping failed: %v", err)
428-
return false
429-
}
430-
if !p2p {
431-
t.Log("ping succeeded, but not direct yet")
432-
return false
433-
}
434-
t.Logf("ping succeeded, direct connection established via %s", pong.Endpoint)
435-
return true
436-
}, testutil.WaitLong, testutil.IntervalMedium)
422+
if b.Service != nil {
423+
b.Service.StartService(t, logger, conn)
424+
}
425+
426+
if b.WaitForConnection || b.WaitForDirect {
427+
// Wait for connection to be established.
428+
peerIP := tailnet.TailscaleServicePrefix.AddrFromUUID(peer.ID)
429+
require.Eventually(t, func() bool {
430+
t.Log("attempting ping to peer to judge direct connection")
431+
ctx := testutil.Context(t, testutil.WaitShort)
432+
_, p2p, pong, err := conn.Ping(ctx, peerIP)
433+
if err != nil {
434+
t.Logf("ping failed: %v", err)
435+
return false
436+
}
437+
if !p2p && b.WaitForDirect {
438+
t.Log("ping succeeded, but not direct yet")
439+
return false
440+
}
441+
t.Logf("ping succeeded, p2p=%t, endpoint=%s", p2p, pong.Endpoint)
442+
return true
443+
}, testutil.WaitLong, testutil.IntervalMedium)
444+
}
437445

438446
return conn
439447
}
440448

441-
type ClientStarter struct {
442-
Options *tailnet.Options
449+
const EchoPort = 2381
450+
451+
type UDPEchoService struct{}
452+
453+
func (UDPEchoService) StartService(t *testing.T, logger slog.Logger, _ *tailnet.Conn) {
454+
// tailnet doesn't handle UDP connections "in-process" the way we do for TCP, so we need to listen in the OS,
455+
// and tailnet will forward packets.
456+
l, err := net.ListenUDP("udp", &net.UDPAddr{
457+
IP: net.IPv6zero, // all interfaces
458+
Port: EchoPort,
459+
})
460+
require.NoError(t, err)
461+
logger.Info(context.Background(), "started UDPEcho server")
462+
t.Cleanup(func() {
463+
lCloseErr := l.Close()
464+
if lCloseErr != nil {
465+
t.Logf("error closing UDPEcho listener: %v", lCloseErr)
466+
}
467+
})
468+
go func() {
469+
buf := make([]byte, 1500)
470+
for {
471+
n, remote, readErr := l.ReadFromUDP(buf)
472+
if readErr != nil {
473+
logger.Info(context.Background(), "error reading UDPEcho listener", slog.Error(readErr))
474+
return
475+
}
476+
logger.Info(context.Background(), "received UDPEcho packet",
477+
slog.F("len", n), slog.F("remote", remote))
478+
n, writeErr := l.WriteToUDP(buf[:n], remote)
479+
if writeErr != nil {
480+
logger.Info(context.Background(), "error writing UDPEcho listener", slog.Error(writeErr))
481+
return
482+
}
483+
logger.Info(context.Background(), "wrote UDPEcho packet",
484+
slog.F("len", n), slog.F("remote", remote))
485+
}
486+
}()
443487
}
444488

445489
func startClientOptions(t *testing.T, logger slog.Logger, serverURL *url.URL, me, peer Client, options *tailnet.Options) *tailnet.Conn {
@@ -467,9 +511,16 @@ func startClientOptions(t *testing.T, logger slog.Logger, serverURL *url.URL, me
467511
_ = conn.Close()
468512
})
469513

470-
ctrl := tailnet.NewTunnelSrcCoordController(logger, conn)
471-
ctrl.AddDestination(peer.ID)
472-
coordination := ctrl.New(coord)
514+
var coordination tailnet.CloserWaiter
515+
if me.TunnelSrc {
516+
ctrl := tailnet.NewTunnelSrcCoordController(logger, conn)
517+
ctrl.AddDestination(peer.ID)
518+
coordination = ctrl.New(coord)
519+
} else {
520+
// use the "Agent" controller so that we act as a tunnel destination and send "ReadyForHandshake" acks.
521+
ctrl := tailnet.NewAgentCoordinationController(logger, conn)
522+
coordination = ctrl.New(coord)
523+
}
473524
t.Cleanup(func() {
474525
cctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
475526
defer cancel()
@@ -492,11 +543,17 @@ func basicDERPMap(serverURLStr string) (*tailcfg.DERPMap, error) {
492543
}
493544

494545
hostname := serverURL.Hostname()
495-
ipv4 := ""
546+
ipv4 := "none"
547+
ipv6 := "none"
496548
ip, err := netip.ParseAddr(hostname)
497549
if err == nil {
498550
hostname = ""
499-
ipv4 = ip.String()
551+
if ip.Is4() {
552+
ipv4 = ip.String()
553+
}
554+
if ip.Is6() {
555+
ipv6 = ip.String()
556+
}
500557
}
501558

502559
return &tailcfg.DERPMap{
@@ -511,7 +568,7 @@ func basicDERPMap(serverURLStr string) (*tailcfg.DERPMap, error) {
511568
RegionID: 1,
512569
HostName: hostname,
513570
IPv4: ipv4,
514-
IPv6: "none",
571+
IPv6: ipv6,
515572
DERPPort: port,
516573
STUNPort: -1,
517574
ForceHTTP: true,
@@ -648,3 +705,35 @@ func (w *testWriter) Flush() {
648705
}
649706
w.capturedLines = nil
650707
}
708+
709+
type packetLogger struct {
710+
l slog.Logger
711+
}
712+
713+
func (p packetLogger) LogPacket(path capture.Path, when time.Time, pkt []byte, _ packet.CaptureMeta) {
714+
q := new(packet.Parsed)
715+
q.Decode(pkt)
716+
p.l.Info(context.Background(), "Packet",
717+
slog.F("path", pathString(path)),
718+
slog.F("when", when),
719+
slog.F("decode", q.String()),
720+
slog.F("len", len(pkt)),
721+
)
722+
}
723+
724+
func pathString(path capture.Path) string {
725+
switch path {
726+
case capture.FromLocal:
727+
return "Local"
728+
case capture.FromPeer:
729+
return "Peer"
730+
case capture.SynthesizedToLocal:
731+
return "SynthesizedToLocal"
732+
case capture.SynthesizedToPeer:
733+
return "SynthesizedToPeer"
734+
case capture.PathDisco:
735+
return "Disco"
736+
default:
737+
return "<<UNKNOWN>>"
738+
}
739+
}

0 commit comments

Comments
 (0)