Skip to content

Commit 08eff7f

Browse files
authored
chore: improve tailnet integration test (#18124)
Refactors tailnet integration test and adds UDP echo tests with different MTU related to #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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy