Files
tailscale/tsnet/tsnet_test.go
Alex Chan 9169b206be Revert "control/controlclient: continue map poll during key expiry to receive extensions" (#20257)
* Revert "control/controlclient: continue map poll during key expiry to receive extensions"

This reverts commit 6a822dcc36. This commit
has caused test failures in the corp repo by unexpected changing the login
behaviour when nodes have a valid node key.

Updates tailscale/corp#43705
Updates #19326

Signed-off-by: Alex Chan <alexc@tailscale.com>

* Revert "tsnet: test key extension after server restart"

This reverts commit 317201375f. This test
relies on changes in 317201375f, which is
also being reverted because it causes test failures in corp.

Updates tailscale/corp#43705
Updates #19326

Signed-off-by: Alex Chan <alexc@tailscale.com>

---------

Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-06-25 15:24:12 -07:00

3887 lines
111 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsnet
import (
"bufio"
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/net/proxy"
"tailscale.com/client/local"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/netns"
"tailscale.com/net/packet"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/views"
"tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/wgengine/filter"
)
// pingTimeout returns a per-ping budget for use within the larger test ctx:
// enough headroom for wireguard-go's RekeyTimeout (5s) plus the actual handshake on slow CI
// (notably GOARCH=386 emulation where Curve25519/ChaCha20 lack the amd64 assembly fast paths),
// but tight enough that a hung Ping points the finger at its callsite
// rather than swallowing the whole test deadline.
func pingTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, 10*time.Second)
}
// TestListener_Server ensures that the listener type always keeps the Server
// method, which is used by some external applications to identify a tsnet.Listener
// from other net.Listeners, as well as access the underlying Server.
func TestListener_Server(t *testing.T) {
s := &Server{}
ln := listener{s: s}
if ln.Server() != s {
t.Errorf("listener.Server() returned %v, want %v", ln.Server(), s)
}
}
func TestListenerPort(t *testing.T) {
errNone := errors.New("sentinel start error")
tests := []struct {
network string
addr string
wantErr bool
}{
{"tcp", ":80", false},
{"foo", ":80", true},
{"tcp", ":http", false}, // built-in name to Go; doesn't require cgo, /etc/services
{"tcp", ":https", false}, // built-in name to Go; doesn't require cgo, /etc/services
{"tcp", ":gibberishsdlkfj", true},
{"tcp", ":%!d(string=80)", true}, // issue 6201
{"udp", ":80", false},
{"udp", "100.102.104.108:80", false},
{"udp", "not-an-ip:80", true},
{"udp4", ":80", false},
{"udp4", "100.102.104.108:80", false},
{"udp4", "not-an-ip:80", true},
// Verify network type matches IP
{"tcp4", "1.2.3.4:80", false},
{"tcp6", "1.2.3.4:80", true},
{"tcp4", "[12::34]:80", true},
{"tcp6", "[12::34]:80", false},
}
for _, tt := range tests {
s := &Server{}
s.initOnce.Do(func() { s.initErr = errNone })
_, err := s.Listen(tt.network, tt.addr)
gotErr := err != nil && err != errNone
if gotErr != tt.wantErr {
t.Errorf("Listen(%q, %q) error = %v, want %v", tt.network, tt.addr, gotErr, tt.wantErr)
}
}
}
func TestResolveListenAddrUnspecified(t *testing.T) {
tests := []struct {
name string
network string
addr string
wantIP netip.Addr
}{
{"empty_host", "tcp", ":80", netip.Addr{}},
{"ipv4_unspecified", "tcp", "0.0.0.0:80", netip.Addr{}},
{"ipv6_unspecified", "tcp", "[::]:80", netip.Addr{}},
{"specific_ipv4", "tcp", "100.64.0.1:80", netip.MustParseAddr("100.64.0.1")},
{"specific_ipv6", "tcp6", "[fd7a:115c:a1e0::1]:80", netip.MustParseAddr("fd7a:115c:a1e0::1")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveListenAddr(tt.network, tt.addr)
if err != nil {
t.Fatal(err)
}
if got.Addr() != tt.wantIP {
t.Errorf("Addr() = %v, want %v", got.Addr(), tt.wantIP)
}
})
}
}
func TestAllocEphemeral(t *testing.T) {
s := &Server{listeners: make(map[listenKey]*listener)}
// Sequential allocations should return unique ports in range.
var ports []uint16
for range 5 {
s.mu.Lock()
p, ok := s.allocEphemeralLocked("tcp", netip.Addr{}, listenOnTailnet)
s.mu.Unlock()
if !ok {
t.Fatal("allocEphemeralLocked failed unexpectedly")
}
if p < ephemeralPortFirst || p > ephemeralPortLast {
t.Errorf("port %d outside [%d, %d]", p, ephemeralPortFirst, ephemeralPortLast)
}
for _, prev := range ports {
if p == prev {
t.Errorf("duplicate port %d", p)
}
}
ports = append(ports, p)
// Occupy the port so the next call skips it.
s.listeners[listenKey{"tcp", netip.Addr{}, p, false}] = &listener{}
}
// Verify skip over occupied port.
s.mu.Lock()
next := s.nextEphemeralPort
if next < ephemeralPortFirst || next > ephemeralPortLast {
next = ephemeralPortFirst
}
s.listeners[listenKey{"tcp", netip.Addr{}, next, false}] = &listener{}
p, ok := s.allocEphemeralLocked("tcp", netip.Addr{}, listenOnTailnet)
s.mu.Unlock()
if !ok {
t.Fatal("allocEphemeralLocked failed after skip")
}
if p == next {
t.Errorf("should have skipped occupied port %d", next)
}
// Wrap-around.
s.mu.Lock()
s.nextEphemeralPort = ephemeralPortLast
p, ok = s.allocEphemeralLocked("tcp", netip.Addr{}, listenOnTailnet)
s.mu.Unlock()
if !ok {
t.Fatal("allocEphemeralLocked failed at wrap")
}
if p < ephemeralPortFirst || p > ephemeralPortLast {
t.Errorf("port %d outside range after wrap", p)
}
}
var verboseDERP = flag.Bool("verbose-derp", false, "if set, print DERP and STUN logs")
var verboseNodes = flag.Bool("verbose-nodes", false, "if set, print tsnet.Server logs")
func startControl(t *testing.T) (controlURL string, control *testcontrol.Server) {
// Corp#4520: don't use netns for tests.
netns.SetEnabled(false)
t.Cleanup(func() {
netns.SetEnabled(true)
})
derpLogf := logger.Discard
if *verboseDERP {
derpLogf = t.Logf
}
derpMap := integration.RunDERPAndSTUN(t, derpLogf, "127.0.0.1")
control = &testcontrol.Server{
DERPMap: derpMap,
DNSConfig: &tailcfg.DNSConfig{
Proxied: true,
},
MagicDNSDomain: "tail-scale.ts.net",
Logf: t.Logf,
}
control.HTTPTestServer = httptest.NewUnstartedServer(control)
control.HTTPTestServer.Start()
t.Cleanup(control.HTTPTestServer.Close)
controlURL = control.HTTPTestServer.URL
t.Logf("testcontrol listening on %s", controlURL)
return controlURL, control
}
type testCertIssuer struct {
mu sync.Mutex
certs map[string]ipnlocal.TLSCertKeyPair // keyed by hostname
root *x509.Certificate
rootKey *ecdsa.PrivateKey
}
func newCertIssuer() *testCertIssuer {
rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
t := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "root",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
rootDER, err := x509.CreateCertificate(rand.Reader, t, t, &rootKey.PublicKey, rootKey)
if err != nil {
panic(err)
}
rootCA, err := x509.ParseCertificate(rootDER)
if err != nil {
panic(err)
}
return &testCertIssuer{
root: rootCA,
rootKey: rootKey,
certs: map[string]ipnlocal.TLSCertKeyPair{},
}
}
func (tci *testCertIssuer) getCert(hostname string) (*ipnlocal.TLSCertKeyPair, error) {
tci.mu.Lock()
defer tci.mu.Unlock()
cert, ok := tci.certs[hostname]
if ok {
return &cert, nil
}
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
certTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: []string{hostname},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
}
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, tci.root, &certPrivKey.PublicKey, tci.rootKey)
if err != nil {
return nil, err
}
keyDER, err := x509.MarshalPKCS8PrivateKey(certPrivKey)
if err != nil {
return nil, err
}
cert = ipnlocal.TLSCertKeyPair{
CertPEM: pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
}),
KeyPEM: pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: keyDER,
}),
}
tci.certs[hostname] = cert
return &cert, nil
}
func (tci *testCertIssuer) Pool() *x509.CertPool {
p := x509.NewCertPool()
p.AddCert(tci.root)
return p
}
var testCertRoot = newCertIssuer()
func startServer(t *testing.T, ctx context.Context, controlURL, hostname string) (*Server, netip.Addr, key.NodePublic) {
t.Helper()
tmp := filepath.Join(t.TempDir(), hostname)
os.MkdirAll(tmp, 0755)
s := &Server{
Dir: tmp,
ControlURL: controlURL,
Hostname: hostname,
Store: new(mem.Store),
Ephemeral: true,
}
if *verboseNodes {
s.Logf = t.Logf
}
t.Cleanup(func() { s.Close() })
status, err := s.Up(ctx)
if err != nil {
t.Fatal(err)
}
s.lb.ConfigureCertsForTest(testCertRoot.getCert)
// Wait for the server to finish connecting to its home DERP server,
// to prevent fast tests from racing the DERP handshake resulting
// in a dropped request with PeerGoneNotHere.
waitForHomeDERPConnected(t, ctx, s)
return s, status.TailscaleIPs[0], status.Self.PublicKey
}
// waitForHomeDERPConnected blocks until s has selected a home DERP region
// and received its first frame from that region.
// Until s establishes a complete connection to its home DERP server,
// the DERP server will drop any incoming peer DISCO frames looking for s
// with PeerGoneNotHere.
func waitForHomeDERPConnected(t testing.TB, ctx context.Context, s *Server) {
t.Helper()
h := s.Sys().HealthTracker.Get()
ms := s.Sys().MagicSock.Get()
for {
if r := ms.GetLastNetcheckReport(ctx); r != nil &&
r.PreferredDERP != 0 &&
!h.GetDERPRegionReceivedTime(r.PreferredDERP).IsZero() {
return
}
select {
case <-ctx.Done():
t.Fatalf("waitForHomeDERPConnected(%s): %v", s.hostname, ctx.Err())
case <-time.After(20 * time.Millisecond):
}
}
}
func TestDialBlocks(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
// Make one tsnet that blocks until it's up.
s1, _, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8080")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
// Then make another tsnet node that will only be woken up
// upon the first dial.
tmp := filepath.Join(t.TempDir(), "s2")
os.MkdirAll(tmp, 0755)
s2 := &Server{
Dir: tmp,
ControlURL: controlURL,
Hostname: "s2",
Store: new(mem.Store),
Ephemeral: true,
}
if *verboseNodes {
s2.Logf = log.Printf
}
t.Cleanup(func() { s2.Close() })
c, err := s2.Dial(ctx, "tcp", "s1:8080")
if err != nil {
t.Fatal(err)
}
defer c.Close()
}
// TestConn tests basic TCP connections between two tsnet Servers, s1 and s2:
//
// - s1, a subnet router, first listens on its TCP :8081.
// - s2 can connect to s1:8081
// - s2 cannot connect to s1:8082 (no listener)
// - s2 can dial through the subnet router functionality (getting a synthetic RST
// that we verify we generated & saw)
func TestConn(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, c := startControl(t)
s1, s1ip, s1PubKey := startServer(t, ctx, controlURL, "s1")
// Track whether we saw an attempted dial to 192.0.2.1:8081.
var saw192DocNetDial atomic.Bool
s1.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
t.Logf("s1: fallback TCP handler called for %v -> %v", src, dst)
if dst.String() == "192.0.2.1:8081" {
saw192DocNetDial.Store(true)
}
return nil, true // nil handler but intercept=true means to send RST
})
lc1 := must.Get(s1.LocalClient())
must.Get(lc1.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
AdvertiseRoutes: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")},
},
AdvertiseRoutesSet: true,
}))
c.SetSubnetRoutes(s1PubKey, []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")})
// Start s2 after s1 is fully set up, including advertising its routes,
// otherwise the test is flaky if the test starts dialing through s2 before
// our test control server has told s2 about s1's routes.
s2, _, _ := startServer(t, ctx, controlURL, "s2")
lc2 := must.Get(s2.LocalClient())
must.Get(lc2.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
RouteAll: true,
},
RouteAllSet: true,
}))
// ping to make sure the connection is up.
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
res, err := lc2.Ping(pingCtx, s1ip, tailcfg.PingTSMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
// pass some data through TCP.
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
s1Conns := make(chan net.Conn)
go func() {
for {
c, err := ln.Accept()
if err != nil {
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return
}
t.Errorf("s1.Accept: %v", err)
return
}
select {
case s1Conns <- c:
case <-ctx.Done():
c.Close()
}
}
}()
w, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatal(err)
}
want := "hello"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
select {
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for connection")
case r := <-s1Conns:
got := make([]byte, len(want))
_, err := io.ReadAtLeast(r, got, len(got))
r.Close()
if err != nil {
t.Fatal(err)
}
t.Logf("got: %q", got)
if string(got) != want {
t.Errorf("got %q, want %q", got, want)
}
}
// Dial a non-existent port on s1 and expect it to fail.
_, err = s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8082", s1ip)) // some random port
if err == nil {
t.Fatalf("unexpected success; should have seen a connection refused error")
}
t.Logf("got expected failure: %v", err)
// s1 is a subnet router for TEST-NET-1 (192.0.2.0/24). Let's dial to that
// subnet from s2 to ensure a listener without an IP address (i.e. our
// ":8081" listen above) only matches destination IPs corresponding to the
// s1 node's IP addresses, and not to any random IP of a subnet it's routing.
//
// The RegisterFallbackTCPHandler on s1 above handles sending a RST when the
// TCP SYN arrives from s2. But we bound it to 5 seconds lest a regression
// like tailscale/tailscale#17805 recur.
s2dialer := s2.Sys().Dialer.Get()
s2dialer.SetSystemDialerForTest(func(ctx context.Context, netw, addr string) (net.Conn, error) {
t.Logf("s2: unexpected system dial called for %s %s", netw, addr)
return nil, fmt.Errorf("system dialer called unexpectedly for %s %s", netw, addr)
})
docCtx, docCancel := context.WithTimeout(ctx, 5*time.Second)
defer docCancel()
_, err = s2.Dial(docCtx, "tcp", "192.0.2.1:8081")
if err == nil {
t.Fatalf("unexpected success; should have seen a connection refused error")
}
if !saw192DocNetDial.Load() {
t.Errorf("expected s1's fallback TCP handler to have been called for 192.0.2.1:8081")
}
}
func TestLoopbackLocalAPI(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557")
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
addr, proxyCred, localAPICred, err := s1.Loopback()
if err != nil {
t.Fatal(err)
}
if proxyCred == localAPICred {
t.Fatal("proxy password matches local API password, they should be different")
}
url := "http://" + addr + "/localapi/v0/status"
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
t.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != 403 {
t.Errorf("GET %s returned %d, want 403 without Sec- header", url, res.StatusCode)
}
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Sec-Tailscale", "localapi")
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != 401 {
t.Errorf("GET %s returned %d, want 401 without basic auth", url, res.StatusCode)
}
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
t.Fatal(err)
}
req.SetBasicAuth("", localAPICred)
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != 403 {
t.Errorf("GET %s returned %d, want 403 without Sec- header", url, res.StatusCode)
}
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Sec-Tailscale", "localapi")
req.SetBasicAuth("", localAPICred)
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
if res.StatusCode != 200 {
t.Errorf("GET /status returned %d, want 200", res.StatusCode)
}
}
func TestLoopbackSOCKS5(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8198")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
s2, _, _ := startServer(t, ctx, controlURL, "s2")
addr, proxyCred, _, err := s2.Loopback()
if err != nil {
t.Fatal(err)
}
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
auth := &proxy.Auth{User: "tsnet", Password: proxyCred}
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
if err != nil {
t.Fatal(err)
}
w, err := socksDialer.Dial("tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatal(err)
}
r, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
want := "hello"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
if _, err := io.ReadAtLeast(r, got, len(got)); err != nil {
t.Fatal(err)
}
t.Logf("got: %q", got)
if string(got) != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestTailscaleIPs(t *testing.T) {
controlURL, _ := startControl(t)
tmp := t.TempDir()
tmps1 := filepath.Join(tmp, "s1")
os.MkdirAll(tmps1, 0755)
s1 := &Server{
Dir: tmps1,
ControlURL: controlURL,
Hostname: "s1",
Store: new(mem.Store),
Ephemeral: true,
}
defer s1.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s1status, err := s1.Up(ctx)
if err != nil {
t.Fatal(err)
}
var upIp4, upIp6 netip.Addr
for _, ip := range s1status.TailscaleIPs {
if ip.Is6() {
upIp6 = ip
}
if ip.Is4() {
upIp4 = ip
}
}
sIp4, sIp6 := s1.TailscaleIPs()
if !(upIp4 == sIp4 && upIp6 == sIp6) {
t.Errorf("s1.TailscaleIPs returned a different result than S1.Up, (%s, %s) != (%s, %s)",
sIp4, upIp4, sIp6, upIp6)
}
}
// TestListenerCleanup is a regression test to verify that s.Close doesn't
// deadlock if a listener is still open.
func TestListenerCleanup(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
if err := s1.Close(); err != nil {
t.Fatal(err)
}
if err := ln.Close(); !errors.Is(err, net.ErrClosed) {
t.Fatalf("second ln.Close error: %v, want net.ErrClosed", err)
}
// Verify that handling a connection from gVisor (from a packet arriving)
// after a listener closed doesn't panic (previously: sending on a closed
// channel) or hang.
c := &closeTrackConn{}
ln.(*listener).handle(c)
if !c.closed {
t.Errorf("c.closed = false, want true")
}
}
type closeTrackConn struct {
net.Conn
closed bool
}
func (wc *closeTrackConn) Close() error {
wc.closed = true
return nil
}
// tests https://github.com/tailscale/tailscale/issues/6973 -- that we can start a tsnet server,
// stop it, and restart it, even on Windows.
func TestStartStopStartGetsSameIP(t *testing.T) {
controlURL, _ := startControl(t)
tmp := t.TempDir()
tmps1 := filepath.Join(tmp, "s1")
os.MkdirAll(tmps1, 0755)
newServer := func() *Server {
return &Server{
Dir: tmps1,
ControlURL: controlURL,
Hostname: "s1",
Logf: tstest.WhileTestRunningLogger(t),
}
}
s1 := newServer()
defer s1.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
s1status, err := s1.Up(ctx)
if err != nil {
t.Fatal(err)
}
firstIPs := s1status.TailscaleIPs
t.Logf("IPs: %v", firstIPs)
if err := s1.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
s2 := newServer()
defer s2.Close()
s2status, err := s2.Up(ctx)
if err != nil {
t.Fatalf("second Up: %v", err)
}
secondIPs := s2status.TailscaleIPs
t.Logf("IPs: %v", secondIPs)
if !reflect.DeepEqual(firstIPs, secondIPs) {
t.Fatalf("got %v but later %v", firstIPs, secondIPs)
}
}
func TestFunnel(t *testing.T) {
ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dialCancel()
controlURL, _ := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
s2, _, _ := startServer(t, ctx, controlURL, "s2")
ln := must.Get(s1.ListenFunnel("tcp", ":443"))
defer ln.Close()
wantSrcAddrPort := netip.MustParseAddrPort("127.0.0.1:1234")
wantTarget := ipn.HostPort("s1.tail-scale.ts.net:443")
srv := &http.Server{
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
tc, ok := c.(*tls.Conn)
if !ok {
t.Errorf("ConnContext called with non-TLS conn: %T", c)
}
if fc, ok := tc.NetConn().(*ipn.FunnelConn); !ok {
t.Errorf("ConnContext called with non-FunnelConn: %T", c)
} else if fc.Src != wantSrcAddrPort {
t.Errorf("ConnContext called with wrong SrcAddrPort; got %v, want %v", fc.Src, wantSrcAddrPort)
} else if fc.Target != wantTarget {
t.Errorf("ConnContext called with wrong Target; got %q, want %q", fc.Target, wantTarget)
}
return ctx
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}),
}
go srv.Serve(ln)
c := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialIngressConn(s2, s1, addr)
},
TLSClientConfig: &tls.Config{
RootCAs: testCertRoot.Pool(),
},
},
}
resp, err := c.Get("https://s1.tail-scale.ts.net:443")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("unexpected status code: %v", resp.StatusCode)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != "hello" {
t.Errorf("unexpected body: %q", body)
}
}
// TestFunnelClose ensures that the listener returned by ListenFunnel cleans up
// after itself when closed. Specifically, changes made to the serve config
// should be cleared.
func TestFunnelClose(t *testing.T) {
marshalServeConfig := func(t *testing.T, sc ipn.ServeConfigView) string {
t.Helper()
return string(must.Get(json.MarshalIndent(sc, "", "\t")))
}
t.Run("simple", func(t *testing.T) {
controlURL, _ := startControl(t)
s, _, _ := startServer(t, t.Context(), controlURL, "s")
before := s.lb.ServeConfig()
ln := must.Get(s.ListenFunnel("tcp", ":443"))
ln.Close()
after := s.lb.ServeConfig()
if diff := cmp.Diff(marshalServeConfig(t, after), marshalServeConfig(t, before)); diff != "" {
t.Fatalf("expected serve config to be unchanged after close (-got, +want):\n%s", diff)
}
})
// Closing the listener shouldn't clear out config that predates it.
t.Run("no_clobbering", func(t *testing.T) {
controlURL, _ := startControl(t)
s, _, _ := startServer(t, t.Context(), controlURL, "s")
// To obtain config the listener might want to clobber, we:
// - run a listener
// - grab the config
// - close the listener (so we can run another on the same port)
ln := must.Get(s.ListenFunnel("tcp", ":443"))
before := s.lb.ServeConfig()
ln.Close()
// Now we manually write the config to the local backend (it should have
// been cleared), run the listener again, and close it again.
must.Do(s.lb.SetServeConfig(before.AsStruct(), ""))
ln = must.Get(s.ListenFunnel("tcp", ":443"))
ln.Close()
// The config should not have been cleared this time since it predated
// the most recent run.
after := s.lb.ServeConfig()
if diff := cmp.Diff(marshalServeConfig(t, after), marshalServeConfig(t, before)); diff != "" {
t.Fatalf("expected existing config to remain intact (-got, +want):\n%s", diff)
}
})
// Closing one listener shouldn't affect config for another listener.
t.Run("two_listeners", func(t *testing.T) {
controlURL, _ := startControl(t)
s, _, _ := startServer(t, t.Context(), controlURL, "s1")
// Start a listener on 443.
ln1 := must.Get(s.ListenFunnel("tcp", ":443"))
defer ln1.Close()
// Save the serve config for this original listener.
before := s.lb.ServeConfig()
// Now start and close a new listener on a different port.
ln2 := must.Get(s.ListenFunnel("tcp", ":8080"))
ln2.Close()
// The serve config for the original listener should be intact.
after := s.lb.ServeConfig()
if diff := cmp.Diff(marshalServeConfig(t, after), marshalServeConfig(t, before)); diff != "" {
t.Fatalf("expected existing config to remain intact (-got, +want):\n%s", diff)
}
})
// It should be possible to close a listener and free system resources even
// when the Server has been closed (or the listener should be automatically
// closed).
t.Run("after_server_close", func(t *testing.T) {
controlURL, _ := startControl(t)
s, _, _ := startServer(t, t.Context(), controlURL, "s")
ln := must.Get(s.ListenFunnel("tcp", ":443"))
// Close the server, then close the listener.
must.Do(s.Close())
// We don't care whether we get an error from the listener closing.
ln.Close()
// The listener should immediately return an error indicating closure.
_, err := ln.Accept()
if !errors.Is(err, net.ErrClosed) {
t.Fatal("expected listener to be closed, got:", err)
}
})
}
// setUpServiceState performs all necessary state setup for testing with a
// Tailscale Service. When this function returns, the host will be able to
// advertise a Service (via [Server.ListenService]) and the client will be able
// to dial the Service via the Service name.
//
// extraSetup, when non-nil, can be used to perform additional state setup and
// this state will be observable by client and host when this function returns.
func setUpServiceState(t *testing.T, name, ip string, host, client *Server,
control *testcontrol.Server, extraSetup func(*testing.T, *testcontrol.Server)) {
t.Helper()
serviceName := tailcfg.ServiceName(name)
must.Do(serviceName.Validate())
// The Service host must have the 'service-host' capability, which
// is a mapping from the Service name to the Service VIP.
cm := host.lb.NetMap().SelfNode.CapMap()
svcIPMap := make(tailcfg.ServiceIPMappings)
if cm.Contains(tailcfg.NodeAttrServiceHost) {
parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, tailcfg.NodeAttrServiceHost))
if len(parsed) != 1 {
t.Fatalf("expected only one capability for %v, got %d", tailcfg.NodeAttrServiceHost, len(parsed))
}
svcIPMap = parsed[0]
}
svcIPMap[serviceName] = []netip.Addr{netip.MustParseAddr(ip)}
svcIPMapJSON := must.Get(json.Marshal(svcIPMap))
newCM := cm.AsMap()
mak.Set(&newCM, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)})
control.SetNodeCapMap(host.lb.NodeKey(), newCM)
// The Service host must be allowed to advertise the Service VIP.
subnetRoutes := []netip.Prefix{netip.MustParsePrefix(ip + `/32`)}
selfAddresses := host.lb.NetMap().SelfNode.Addresses()
for _, existingRoute := range host.lb.NetMap().SelfNode.AllowedIPs().All() {
if views.SliceContains(selfAddresses, existingRoute) {
continue
}
subnetRoutes = append(subnetRoutes, existingRoute)
}
control.SetSubnetRoutes(host.lb.NodeKey(), subnetRoutes)
// The Service host must be a tagged node (any tag will do).
serviceHostNode := control.Node(host.lb.NodeKey())
serviceHostNode.Tags = append(serviceHostNode.Tags, "some-tag")
control.UpdateNode(serviceHostNode)
// The service client must accept routes advertised by other nodes
// (RouteAll is equivalent to --accept-routes).
must.Get(client.localClient.EditPrefs(t.Context(), &ipn.MaskedPrefs{
RouteAllSet: true,
Prefs: ipn.Prefs{
RouteAll: true,
},
}))
// Do the test's extra setup before configuring DNS. This allows
// us to use the configured DNS records as sentinel values when
// waiting for all of this setup to be visible to test nodes.
if extraSetup != nil {
extraSetup(t, control)
}
// Set up DNS for our Service.
control.AddDNSRecords(tailcfg.DNSRecord{
Name: serviceName.WithoutPrefix() + "." + control.MagicDNSDomain,
Value: ip,
})
// Wait until both nodes have up-to-date netmaps before
// proceeding with the test.
netmapUpToDate := func(nm *netmap.NetworkMap) bool {
return nm != nil && slices.ContainsFunc(nm.DNS.ExtraRecords, func(r tailcfg.DNSRecord) bool {
return r.Value == ip
})
}
waitForLatestNetmap := func(t *testing.T, s *Server) {
t.Helper()
w := must.Get(s.localClient.WatchIPNBus(t.Context(), ipn.NotifyInitialNetMap))
defer w.Close()
for {
must.Get(w.Next())
if nm := s.lb.NetMapWithPeers(); nm != nil && netmapUpToDate(nm) {
return
}
}
}
waitForLatestNetmap(t, client)
waitForLatestNetmap(t, host)
}
func TestListenService(t *testing.T) {
type dialFn func(context.Context, string, string) (net.Conn, error)
// TCP helpers
acceptAndEcho := func(t *testing.T, ln net.Listener) {
t.Helper()
conn, err := ln.Accept()
if err != nil {
t.Error("accept error:", err)
return
}
defer conn.Close()
if _, err := io.Copy(conn, conn); err != nil {
t.Error("copy error:", err)
}
}
assertEcho := func(t *testing.T, conn net.Conn) {
t.Helper()
msg := "echo"
buf := make([]byte, 1024)
if _, err := conn.Write([]byte(msg)); err != nil {
t.Fatal("write failed:", err)
}
n, err := conn.Read(buf)
if err != nil {
t.Fatal("read failed:", err)
}
got := string(buf[:n])
if got != msg {
t.Fatalf("unexpected response:\n\twant: %s\n\tgot: %s", msg, got)
}
}
// HTTP helpers
checkAndEcho := func(t *testing.T, ln net.Listener, check func(r *http.Request)) {
t.Helper()
if check == nil {
check = func(*http.Request) {}
}
http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
check(r)
if _, err := io.Copy(w, r.Body); err != nil {
t.Error("copy error:", err)
w.WriteHeader(http.StatusInternalServerError)
}
}))
}
assertEchoHTTP := func(t *testing.T, hostname, path string, dial dialFn) {
t.Helper()
c := http.Client{
Transport: &http.Transport{
DialContext: dial,
},
}
msg := "echo"
resp, err := c.Post("http://"+hostname+path, "text/plain", strings.NewReader(msg))
if err != nil {
t.Fatal("posting request:", err)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal("reading body:", err)
}
got := string(b)
if got != msg {
t.Fatalf("unexpected response:\n\twant: %s\n\tgot: %s", msg, got)
}
}
tests := []struct {
name string
// modes is used as input to [Server.ListenService].
//
// If this slice has multiple modes, then ListenService will be invoked
// multiple times. The number of listeners provided to the run function
// (below) will always match the number of elements in this slice.
modes []ServiceMode
extraSetup func(t *testing.T, control *testcontrol.Server)
// run executes the test. This function does not need to close any of
// the input resources, but it should close any new resources it opens.
// listeners[i] corresponds to inputs[i].
run func(t *testing.T, listeners []*ServiceListener, peer *Server)
}{
{
name: "basic_TCP",
modes: []ServiceMode{
ServiceModeTCP{Port: 99},
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
go acceptAndEcho(t, listeners[0])
target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 99)
conn := must.Get(peer.Dial(t.Context(), "tcp", target))
defer conn.Close()
assertEcho(t, conn)
},
},
{
name: "TLS_terminated_TCP",
modes: []ServiceMode{
ServiceModeTCP{
TerminateTLS: true,
Port: 443,
},
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
go acceptAndEcho(t, listeners[0])
target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 443)
conn := must.Get(peer.Dial(t.Context(), "tcp", target))
defer conn.Close()
assertEcho(t, tls.Client(conn, &tls.Config{
ServerName: listeners[0].FQDN,
RootCAs: testCertRoot.Pool(),
}))
},
},
{
name: "identity_headers",
modes: []ServiceMode{
ServiceModeHTTP{
Port: 80,
},
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
expectHeader := "Tailscale-User-Name"
go checkAndEcho(t, listeners[0], func(r *http.Request) {
if _, ok := r.Header[expectHeader]; !ok {
t.Error("did not see expected header:", expectHeader)
}
})
assertEchoHTTP(t, listeners[0].FQDN, "", peer.Dial)
},
},
{
name: "identity_headers_TLS",
modes: []ServiceMode{
ServiceModeHTTP{
HTTPS: true,
Port: 80,
},
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
expectHeader := "Tailscale-User-Name"
go checkAndEcho(t, listeners[0], func(r *http.Request) {
if _, ok := r.Header[expectHeader]; !ok {
t.Error("did not see expected header:", expectHeader)
}
})
dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
tcpConn, err := peer.Dial(ctx, network, addr)
if err != nil {
return nil, err
}
return tls.Client(tcpConn, &tls.Config{
ServerName: listeners[0].FQDN,
RootCAs: testCertRoot.Pool(),
}), nil
}
assertEchoHTTP(t, listeners[0].FQDN, "", dial)
},
},
{
name: "app_capabilities",
modes: []ServiceMode{
ServiceModeHTTP{
Port: 80,
AcceptAppCaps: map[string][]string{
"/": {"example.com/cap/all-paths"},
"/foo": {"example.com/cap/all-paths", "example.com/cap/foo"},
},
},
},
extraSetup: func(t *testing.T, control *testcontrol.Server) {
control.SetGlobalAppCaps(tailcfg.PeerCapMap{
"example.com/cap/all-paths": []tailcfg.RawMessage{`true`},
"example.com/cap/foo": []tailcfg.RawMessage{`true`},
})
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
allPathsCap := "example.com/cap/all-paths"
fooCap := "example.com/cap/foo"
checkCaps := func(r *http.Request) {
rawCaps, ok := r.Header["Tailscale-App-Capabilities"]
if !ok {
t.Error("no app capabilities header")
return
}
if len(rawCaps) != 1 {
t.Error("expected one app capabilities header value, got", len(rawCaps))
return
}
var caps map[string][]any
if err := json.Unmarshal([]byte(rawCaps[0]), &caps); err != nil {
t.Error("error unmarshaling app caps:", err)
return
}
if _, ok := caps[allPathsCap]; !ok {
t.Errorf("got app caps, but %v is not present; saw:\n%v", allPathsCap, caps)
}
if strings.HasPrefix(r.URL.Path, "/foo") {
if _, ok := caps[fooCap]; !ok {
t.Errorf("%v should be present for /foo request; saw:\n%v", fooCap, caps)
}
} else {
if _, ok := caps[fooCap]; ok {
t.Errorf("%v should not be present for non-/foo request; saw:\n%v", fooCap, caps)
}
}
}
go checkAndEcho(t, listeners[0], checkCaps)
assertEchoHTTP(t, listeners[0].FQDN, "", peer.Dial)
assertEchoHTTP(t, listeners[0].FQDN, "/foo", peer.Dial)
assertEchoHTTP(t, listeners[0].FQDN, "/foo/bar", peer.Dial)
},
},
{
name: "multiple_ports",
modes: []ServiceMode{
ServiceModeTCP{
Port: 99,
},
ServiceModeHTTP{
Port: 80,
},
},
run: func(t *testing.T, listeners []*ServiceListener, peer *Server) {
go acceptAndEcho(t, listeners[0])
target := fmt.Sprintf("%s:%d", listeners[0].FQDN, 99)
conn := must.Get(peer.Dial(t.Context(), "tcp", target))
defer conn.Close()
assertEcho(t, conn)
go checkAndEcho(t, listeners[1], nil)
assertEchoHTTP(t, listeners[1].FQDN, "", peer.Dial)
},
},
}
for _, tt := range tests {
// Overview:
// - start test control
// - start 2 tsnet nodes:
// one to act as Service host and a second to act as a peer client
// - configure necessary state on control mock
// - start a Service listener from the host
// - call tt.run with our test bed
//
// This ends up also testing the Service forwarding logic in
// LocalBackend, but that's useful too.
t.Run(tt.name, func(t *testing.T) {
// We run each test with and without a TUN device ([Server.Tun]).
// Note that this TUN device is distinct from TUN mode for Services.
doTest := func(t *testing.T, withTUNDevice bool) {
lt := setupTwoClientTest(t, withTUNDevice)
serviceHost := lt.s2
serviceClient := lt.s1
const serviceName = "svc:foo"
const serviceVIP = "100.11.22.33"
setUpServiceState(t, serviceName, serviceVIP,
serviceHost, serviceClient, lt.control, tt.extraSetup)
listeners := make([]*ServiceListener, 0, len(tt.modes))
for _, input := range tt.modes {
ln := must.Get(serviceHost.ListenService(serviceName, input))
defer ln.Close()
listeners = append(listeners, ln)
}
tt.run(t, listeners, serviceClient)
}
t.Run("TUN", func(t *testing.T) { doTest(t, true) })
t.Run("netstack", func(t *testing.T) { doTest(t, false) })
})
}
// Error cases.
t.Run("untagged_node_error", func(t *testing.T) {
ctx := t.Context()
controlURL, _ := startControl(t)
serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
ln, err := serviceHost.ListenService("svc:foo", ServiceModeTCP{Port: 8080})
if ln != nil {
ln.Close()
}
if !errors.Is(err, ErrUntaggedServiceHost) {
t.Fatalf("expected %v, got %v", ErrUntaggedServiceHost, err)
}
})
t.Run("duplicate_listeners", func(t *testing.T) {
ctx := t.Context()
const serviceName = "svc:foo"
controlURL, control := startControl(t)
serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client")
setUpServiceState(t, serviceName, "1.2.3.4", serviceHost, serviceClient, control, nil)
ln := must.Get(serviceHost.ListenService(serviceName, ServiceModeTCP{Port: 8080}))
defer ln.Close()
ln, err := serviceHost.ListenService(serviceName, ServiceModeTCP{Port: 8080})
if ln != nil {
ln.Close()
}
if err == nil {
t.Fatal("expected error for redundant listener")
}
// An HTTP listener on the same port should also collide
ln, err = serviceHost.ListenService(serviceName, ServiceModeHTTP{Port: 8080})
if ln != nil {
ln.Close()
}
if err == nil {
t.Fatal("expected error for redundant listener")
}
})
t.Run("multiple_services", func(t *testing.T) {
const numberServices = 10
const port = 80
lt := setupTwoClientTest(t, false)
serviceHost := lt.s2
serviceClient := lt.s1
names := make([]string, numberServices)
fqdns := make([]string, numberServices)
for i := range numberServices {
serviceName := "svc:foo" + strconv.Itoa(i+1)
serviceIP := `11.22.33.` + strconv.Itoa(i+1)
setUpServiceState(t, serviceName, serviceIP, serviceHost, serviceClient, lt.control, nil)
ln := must.Get(serviceHost.ListenService(serviceName, ServiceModeTCP{Port: port}))
defer ln.Close()
names[i] = serviceName
fqdns[i] = ln.FQDN
go func() {
// Accept a single connection, echo, then return.
conn, err := ln.Accept()
if err != nil {
t.Errorf("accept error from %v: %v", serviceName, err)
return
}
defer conn.Close()
if _, err := io.Copy(conn, conn); err != nil {
t.Errorf("copy error from %v: %v", serviceName, err)
}
}()
}
for i := range numberServices {
msg := []byte("hello, " + fqdns[i])
conn := must.Get(serviceClient.Dial(t.Context(), "tcp", fqdns[i]+":"+strconv.Itoa(port)))
defer conn.Close()
must.Get(conn.Write(msg))
buf := make([]byte, len(msg))
n := must.Get(conn.Read(buf))
if !bytes.Equal(buf[:n], msg) {
t.Fatalf("did not receive expected message:\n\tgot: %s\n\twant: %s\n", buf[:n], msg)
}
}
// Each of the Services should be advertised by our Service host.
advertised := serviceHost.lb.Prefs().AdvertiseServices()
for _, name := range names {
if !views.SliceContains(advertised, name) {
t.Log("advertised Services:", advertised)
t.Fatalf("did not find %q in advertised Services", name)
}
}
})
}
func TestListenServiceClose(t *testing.T) {
const serviceName = "svc:foo"
diffServeConfig := func(a, b ipn.ServeConfigView) string {
// We treat a mapping from svc:foo to nil or the zero value as if it
// didn't exist at all. This is consistent with how the local backend
// treats service configs when nil or zero.
tr := cmp.Transformer("DeleteEmptyServices", func(m map[tailcfg.ServiceName]*ipn.ServiceConfig) map[tailcfg.ServiceName]*ipn.ServiceConfig {
mCopy := map[tailcfg.ServiceName]*ipn.ServiceConfig{}
for k, v := range m {
if v == nil {
continue
}
if rv := reflect.ValueOf(*v); rv.IsValid() && rv.IsZero() {
continue
}
mCopy[k] = v
}
return mCopy
})
return cmp.Diff(a.AsStruct(), b.AsStruct(), tr)
}
tests := []struct {
name string
run func(t *testing.T, serviceHost *Server)
}{
{
name: "TCP",
run: func(t *testing.T, s *Server) {
before := s.lb.ServeConfig()
ln := must.Get(s.ListenService(serviceName, ServiceModeTCP{Port: 8080}))
ln.Close()
after := s.lb.ServeConfig()
if diff := diffServeConfig(after, before); diff != "" {
t.Fatalf("expected serve config to be unchanged after close (-got, +want):\n%s", diff)
}
},
},
{
name: "HTTP",
run: func(t *testing.T, s *Server) {
before := s.lb.ServeConfig()
ln := must.Get(s.ListenService(serviceName, ServiceModeHTTP{Port: 8080}))
ln.Close()
after := s.lb.ServeConfig()
if diff := diffServeConfig(after, before); diff != "" {
t.Fatalf("expected serve config to be unchanged after close (-got, +want):\n%s", diff)
}
},
},
{
// Closing one listener should not affect config for another listener.
name: "two_listeners",
run: func(t *testing.T, s *Server) {
// Start a listener on 443.
ln1 := must.Get(s.ListenService(serviceName, ServiceModeTCP{Port: 443}))
defer ln1.Close()
// Save the serve config for this original listener.
before := s.lb.ServeConfig()
// Now start and close a new listener on a different port.
ln2 := must.Get(s.ListenService(serviceName, ServiceModeTCP{Port: 8080}))
ln2.Close()
// The serve config for the original listener should be intact.
after := s.lb.ServeConfig()
if diff := diffServeConfig(after, before); diff != "" {
t.Fatalf("expected existing config to remain intact (-got, +want):\n%s", diff)
}
},
},
{
// It should be possible to close a listener and free system
// resources even when the Server has been closed (or the listener
// should be automatically closed).
name: "after_server_close",
run: func(t *testing.T, s *Server) {
ln := must.Get(s.ListenService(serviceName, ServiceModeTCP{Port: 8080}))
// Close the server, then close the listener.
must.Do(s.Close())
// We don't care whether we get an error from the listener closing.
t.Log("close error:", ln.Close())
// The listener should immediately return an error indicating closure.
_, err := ln.Accept()
if !errors.Is(err, net.ErrClosed) {
t.Fatal("expected listener to be closed, got:", err)
}
},
},
{
// Regression test for https://github.com/tailscale/tailscale/issues/19169,
// in which concurrent ServiceListener.Close calls (by different
// listeners) would fail.
name: "concurrent_close",
run: func(t *testing.T, s *Server) {
const concurrentCloseCalls = 100
readyGroup := new(sync.WaitGroup)
closedGroup := new(sync.WaitGroup)
closeThemAll := make(chan (struct{}))
errC := make(chan error, concurrentCloseCalls)
for i := range concurrentCloseCalls {
readyGroup.Add(1)
closedGroup.Add(1)
ln := must.Get(s.ListenService(serviceName, ServiceModeTCP{
Port: uint16(i + 1),
}))
go func() {
readyGroup.Done()
<-closeThemAll
errC <- ln.Close()
closedGroup.Done()
}()
}
readyGroup.Wait()
close(closeThemAll)
closedGroup.Wait()
close(errC)
var errs []error
for err := range errC {
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
t.Fatalf("%d close errors; sample: %v", len(errs), errs[0])
}
if diff := diffServeConfig(s.lb.ServeConfig(), (&ipn.ServeConfig{}).View()); diff != "" {
t.Fatalf("expected empty config (-got, +want):\n%s", diff)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := t.Context()
controlURL, control := startControl(t)
serviceHost, _, _ := startServer(t, ctx, controlURL, "service-host")
serviceClient, _, _ := startServer(t, ctx, controlURL, "service-client")
setUpServiceState(t, serviceName, "1.2.3.4", serviceHost, serviceClient, control, nil)
tt.run(t, serviceHost)
})
}
}
func TestListenerClose(t *testing.T) {
ctx := context.Background()
controlURL, _ := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8080")
if err != nil {
t.Fatal(err)
}
errc := make(chan error, 1)
go func() {
c, err := ln.Accept()
if c != nil {
c.Close()
}
errc <- err
}()
ln.Close()
select {
case err := <-errc:
if !errors.Is(err, net.ErrClosed) {
t.Errorf("unexpected error: %v", err)
}
case <-time.After(10 * time.Second):
t.Fatal("timeout waiting for Accept to return")
}
}
func dialIngressConn(from, to *Server, target string) (net.Conn, error) {
toLC := must.Get(to.LocalClient())
toStatus := must.Get(toLC.StatusWithoutPeers(context.Background()))
peer6 := toStatus.Self.PeerAPIURL[1] // IPv6
toPeerAPI, ok := strings.CutPrefix(peer6, "http://")
if !ok {
return nil, fmt.Errorf("unexpected PeerAPIURL %q", peer6)
}
dialCtx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second)
outConn, err := from.Dial(dialCtx, "tcp", toPeerAPI)
dialCancel()
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", "/v0/ingress", nil)
if err != nil {
return nil, err
}
req.Host = toPeerAPI
req.Header.Set("Tailscale-Ingress-Src", "127.0.0.1:1234")
req.Header.Set("Tailscale-Ingress-Target", target)
if err := req.Write(outConn); err != nil {
return nil, err
}
br := bufio.NewReader(outConn)
res, err := http.ReadResponse(br, req)
if err != nil {
return nil, err
}
defer res.Body.Close() // just to appease vet
if res.StatusCode != 101 {
return nil, fmt.Errorf("unexpected status code: %v", res.StatusCode)
}
return &bufferedConn{outConn, br}, nil
}
type bufferedConn struct {
net.Conn
reader *bufio.Reader
}
func (c *bufferedConn) Read(b []byte) (int, error) {
return c.reader.Read(b)
}
func TestFallbackTCPHandler(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
s2, _, _ := startServer(t, ctx, controlURL, "s2")
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// ping to make sure the connection is up.
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
res, err := lc2.Ping(pingCtx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
var s1TcpConnCount atomic.Int32
deregister := s1.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
s1TcpConnCount.Add(1)
return nil, false
})
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
t.Fatal("Expected dial error because fallback handler did not intercept")
}
if got := s1TcpConnCount.Load(); got != 1 {
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
}
deregister()
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
t.Fatal("Expected dial error because nothing would intercept")
}
if got := s1TcpConnCount.Load(); got != 1 {
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
}
}
// TestPingPeerLearnedViaDelta verifies that `tailscale ping` works
// for a peer that the local node learned about only via a
// [tailcfg.MapResponse.PeersChanged] delta, never via a full
// [tailcfg.MapResponse.Peers] list.
//
// Before aa5da2e5f22a, every peer change rebuilt a full netmap on
// the engine, so [wgengine.Engine.SetNetworkMap] kept the engine's
// cached netmap fresh and [wgengine.Engine.Reconfig] kept wgdev and
// e.lastCfgFull fresh. After that refactor, peer adds and removes
// ride the delta path and only mutate [nodeBackend]; the engine's
// cached netmap and wireguard config stayed stale, and
// [wgengine.Engine.PeerForIP] / [wgengine.Engine.Ping] / wgdev's
// outbound encryption all missed the new peer.
//
// Two subtests exercise different layers:
//
// - "disco" uses [tailcfg.PingDisco], which goes straight to
// magicsock (which has the peer via UpsertPeer) and bypasses
// wireguard-go entirely. It targets the cold-path PeerForIP
// lookup -- before the fix this missed the new peer with
// "no matching peer".
//
// - "tsmp" uses [tailcfg.PingTSMP], which builds a real
// IP-proto-99 packet and pushes it through the full data path:
// PeerForIP -> lookupPeerByIP -> wgdev encryption ->
// magicsock transport. The receiving side's [tstun.Wrapper]
// intercepts TSMP and replies with a pong. Catches the
// wireguard-go side too: wgdev's PeerLookupFunc closure
// (captured at the last ReconfigDevice) didn't have the new
// peer's noise key, so even after lookupPeerByIP returned the
// right NodePublic wgdev couldn't lazily create the peer for
// outbound encryption.
//
// See tailscale/corp#43394.
func TestPingPeerLearnedViaDelta(t *testing.T) {
for _, pt := range []tailcfg.PingType{tailcfg.PingDisco, tailcfg.PingTSMP} {
t.Run(string(pt), func(t *testing.T) {
testPingPeerLearnedViaDelta(t, pt)
})
}
}
func testPingPeerLearnedViaDelta(t *testing.T, pt tailcfg.PingType) {
if runtime.GOARCH == "386" {
t.Skip("skipping on 386: see https://github.com/tailscale/tailscale/issues/20146")
}
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, control := startControl(t)
// Bring up s1 alone so its initial (auto-generated) MapResponse
// has no peers; testcontrol only has s1 registered at this point.
s1, _, s1Key := startServer(t, ctx, controlURL, "s1")
// Switch s1 into manual MapResponse mode. The empty response is a
// no-op heartbeat; the side effect is that suppressAutoMapResponses
// is set for s1, so the s2 join below cannot reach s1 as a full
// auto-generated netmap.
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{}) {
t.Fatal("AddRawMapResponse(s1, empty): node not connected")
}
// Bring up s2. testcontrol would normally push a peer-changed
// update to s1's long-poll, but autos for s1 are suppressed.
// s2 itself is not suppressed, so it gets a full netmap that
// includes s1, which is necessary for disco to complete in both
// directions (and for the TSMP pong to make it back).
_, s2ip, s2Key := startServer(t, ctx, controlURL, "s2")
// Snapshot s2's node-as-seen-by-control and inject it into s1's
// stream as a PeersChanged delta.
s2Node := control.Node(s2Key)
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{s2Node},
}) {
t.Fatal("AddRawMapResponse(s1, PeersChanged): node not connected")
}
// Wait for the delta to land in s1's nodeBackend.
if err := waitFor(t, ctx, s1, func(nm *netmap.NetworkMap) bool {
return slices.ContainsFunc(nm.Peers, func(p tailcfg.NodeView) bool {
return p.Key() == s2Key
})
}); err != nil {
t.Fatalf("waitFor s2 in s1 netmap: %v", err)
}
lc1, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
pr, err := lc1.Ping(pingCtx, s2ip, pt)
if err != nil {
t.Fatalf("Ping(%s): %v", pt, err)
}
if pr.Err != "" {
t.Fatalf("Ping(%s) s1->s2 failed: %s (want success)", pt, pr.Err)
}
}
// TestPingSubnetRouteOfDeltaPeer verifies that when a peer arrives
// purely via a [tailcfg.MapResponse.PeersChanged] delta and that peer
// advertises a subnet route, the local node can resolve an IP within
// that subnet to the new peer and exchange traffic with it.
//
// Before the fix, [netmapDeltaNeedsAuthReconfig] returned false for a
// brand-new peer Upsert (the NodeID wasn't already known), so
// [LocalBackend.authReconfigLocked] -- the only path that pushes a
// fresh wireguard config into the engine -- never ran. The engine's
// wireguard-filtered peer list and BART stayed stale, so
// [LocalBackend.lookupPeerByIP] and [LocalBackend.peerForIP] both
// missed any IP inside the advertised CIDR, and wgdev's
// PeerLookupFunc closure didn't have the new peer's noise key for
// outbound encryption.
//
// See tailscale/corp#43394.
func TestPingSubnetRouteOfDeltaPeer(t *testing.T) {
if runtime.GOARCH == "386" {
t.Skip("skipping on 386: see https://github.com/tailscale/tailscale/issues/20146")
}
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, control := startControl(t)
// Bring up s1 alone so its initial netmap has no peers.
s1, _, s1Key := startServer(t, ctx, controlURL, "s1")
// Accept subnet routes on s1; otherwise nmcfg.WGCfg strips a peer's
// non-self AllowedIPs out of the wireguard config (and thus out of
// the engine BART), and a subnet-router delta wouldn't actually
// install the route locally even with the fix.
lc1 := must.Get(s1.LocalClient())
must.Get(lc1.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{RouteAll: true},
RouteAllSet: true,
}))
// Switch s1 into manual MapResponse mode so the s2 join cannot
// arrive as a full auto-generated netmap.
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{}) {
t.Fatal("AddRawMapResponse(s1, empty): node not connected")
}
// Bring up s2. We'll treat it as a subnet router for 10.0.0.0/24
// by adding that prefix to its AllowedIPs in the delta we inject
// below. (s2 isn't a real subnet router -- we don't try to forward
// traffic through it. The receiving side's tstun.Wrapper handles
// TSMP regardless of dst IP, so the pong comes back as long as
// the encrypt/transport chain works.)
_, _, s2Key := startServer(t, ctx, controlURL, "s2")
const subnet = "10.0.0.0/24"
subnetPrefix := netip.MustParsePrefix(subnet)
probeIP := netip.MustParseAddr("10.0.0.5")
// Inject s2 into s1 as a PeersChanged delta with the subnet route
// in AllowedIPs.
s2Node := control.Node(s2Key)
s2Node.PrimaryRoutes = []netip.Prefix{subnetPrefix}
s2Node.AllowedIPs = append(s2Node.AllowedIPs, subnetPrefix)
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{s2Node},
}) {
t.Fatal("AddRawMapResponse(s1, PeersChanged): node not connected")
}
// Wait for the delta to land in s1's nodeBackend.
if err := waitFor(t, ctx, s1, func(nm *netmap.NetworkMap) bool {
return slices.ContainsFunc(nm.Peers, func(p tailcfg.NodeView) bool {
return p.Key() == s2Key
})
}); err != nil {
t.Fatalf("waitFor s2 in s1 netmap: %v", err)
}
// PingTSMP sends a real IP packet (IP proto 99) over wireguard
// to probeIP, exercising the full data path -- PeerForIP lookup,
// outbound wgdev encryption, magicsock transport. The receiving
// side (s2's tstun.Wrapper) intercepts TSMP regardless of dst
// IP and replies with a pong. So this catches both halves of
// the bug: a stale BART / lastCfgFull (PeerForIP / lookupPeerByIP
// miss) AND a stale wgdev PeerLookupFunc closure (peer's noise
// key not yet registered for outbound encryption).
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
pr, err := lc1.Ping(pingCtx, probeIP, tailcfg.PingTSMP)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if pr.Err != "" {
t.Fatalf("ping s1->%v (subnet route via s2) failed: %s (want success)", probeIP, pr.Err)
}
}
// TestPingSelfReturnsIsLocalIP verifies that pinging one's own
// Tailscale IP takes the IsSelf early-out in [wgengine.Engine.Ping]
// instead of trying to ping self via magicsock. Lives here as a
// regression guard against future refactors of the PeerForIP self
// path; the original userspaceEngine.PeerForIP handles self via a
// dedicated nm.GetAddresses() scan, but anything that re-routes
// PeerForIP through a more general index needs to keep
// [wgengine.PeerForIP.IsSelf] set for self addresses.
func TestPingSelfReturnsIsLocalIP(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
lc, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
pr, err := lc.Ping(pingCtx, s1ip, tailcfg.PingDisco)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if !pr.IsLocalIP {
t.Errorf("IsLocalIP = false, want true (pr=%+v)", pr)
}
if pr.Err == "" {
t.Errorf("Err = %q, want a 'local Tailscale IP' message", pr.Err)
}
}
// TestStatusReportsPeerInEngine verifies that a peer with an active
// wireguard session is reported as InEngine=true in the local
// [ipnstate.Status]. This exercises [wgengine.Engine.UpdateStatus] ->
// userspaceEngine.getStatus -> the active-wgdev-peer iteration ->
// [ipnstate.StatusBuilder.AddPeer] with InEngine=true. It's the only
// signal in the tree that exercises getStatus's peer-list path; the
// wgengine and ipnlocal unit tests don't assert on it.
func TestStatusReportsPeerInEngine(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
_, s2ip, s2Key := startServer(t, ctx, controlURL, "s2")
lc1, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
if err := waitFor(t, ctx, s1, func(nm *netmap.NetworkMap) bool {
return slices.ContainsFunc(nm.Peers, func(p tailcfg.NodeView) bool {
return p.Key() == s2Key
})
}); err != nil {
t.Fatalf("waitFor s2 in s1 netmap: %v", err)
}
// Ping via ICMP so a real packet flows through wireguard-go and
// instantiates s2 in s1's wgdev peer map. PingDisco wouldn't
// suffice; it goes directly to magicsock and bypasses wgdev.
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
pr, err := lc1.Ping(pingCtx, s2ip, tailcfg.PingICMP)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if pr.Err != "" {
t.Fatalf("Ping s1->s2 failed: %s", pr.Err)
}
status, err := lc1.Status(ctx)
if err != nil {
t.Fatal(err)
}
peer, ok := status.Peer[s2Key]
if !ok {
t.Fatalf("status.Peer missing s2 (%v); peers=%v", s2Key, status.Peers())
}
if !peer.InEngine {
t.Errorf("peer.InEngine = false, want true (peer=%+v)", peer)
}
}
func TestCapturePcap(t *testing.T) {
const timeLimit = 120
ctx, cancel := context.WithTimeout(context.Background(), timeLimit*time.Second)
defer cancel()
dir := t.TempDir()
s1Pcap := filepath.Join(dir, "s1.pcap")
s2Pcap := filepath.Join(dir, "s2.pcap")
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
s2, _, _ := startServer(t, ctx, controlURL, "s2")
s1.CapturePcap(ctx, s1Pcap)
s2.CapturePcap(ctx, s2Pcap)
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// send a packet which both nodes will capture
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
res, err := lc2.Ping(pingCtx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
fileSize := func(name string) int64 {
fi, err := os.Stat(name)
if err != nil {
return 0
}
return fi.Size()
}
const pcapHeaderSize = 24
// there is a lag before the io.Copy writes a packet to the pcap files
for range timeLimit * 10 {
time.Sleep(100 * time.Millisecond)
if (fileSize(s1Pcap) > pcapHeaderSize) && (fileSize(s2Pcap) > pcapHeaderSize) {
break
}
}
if got := fileSize(s1Pcap); got <= pcapHeaderSize {
t.Errorf("s1 pcap file size = %d, want > pcapHeaderSize(%d)", got, pcapHeaderSize)
}
if got := fileSize(s2Pcap); got <= pcapHeaderSize {
t.Errorf("s2 pcap file size = %d, want > pcapHeaderSize(%d)", got, pcapHeaderSize)
}
}
func TestUDPConn(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
s2, s2ip, _ := startServer(t, ctx, controlURL, "s2")
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// ping to make sure the connection is up.
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
res, err := lc2.Ping(pingCtx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
pc := must.Get(s1.ListenPacket("udp", fmt.Sprintf("%s:8081", s1ip)))
defer pc.Close()
// Dial to s1 from s2
w, err := s2.Dial(ctx, "udp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatal(err)
}
defer w.Close()
// Send a packet from s2 to s1
want := "hello"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
// Receive the packet on s1
got := make([]byte, 1024)
n, from, err := pc.ReadFrom(got)
if err != nil {
t.Fatal(err)
}
got = got[:n]
t.Logf("got: %q", got)
if string(got) != want {
t.Errorf("got %q, want %q", got, want)
}
if from.(*net.UDPAddr).AddrPort().Addr() != s2ip {
t.Errorf("got from %v, want %v", from, s2ip)
}
// Write a response back to s2
if _, err := pc.WriteTo([]byte("world"), from); err != nil {
t.Fatal(err)
}
// Receive the response on s2
got = make([]byte, 1024)
n, err = w.Read(got)
if err != nil {
t.Fatal(err)
}
got = got[:n]
t.Logf("got: %q", got)
if string(got) != "world" {
t.Errorf("got %q, want world", got)
}
}
func parseMetrics(m []byte) (map[string]float64, error) {
metrics := make(map[string]float64)
var parser expfmt.TextParser
mf, err := parser.TextToMetricFamilies(bytes.NewReader(m))
if err != nil {
return nil, err
}
for _, f := range mf {
for _, ff := range f.Metric {
val := float64(0)
switch f.GetType() {
case dto.MetricType_COUNTER:
val = ff.GetCounter().GetValue()
case dto.MetricType_GAUGE:
val = ff.GetGauge().GetValue()
}
metrics[f.GetName()+promMetricLabelsStr(ff.GetLabel())] = val
}
}
return metrics, nil
}
func promMetricLabelsStr(labels []*dto.LabelPair) string {
if len(labels) == 0 {
return ""
}
var b strings.Builder
b.WriteString("{")
for i, lb := range labels {
if i > 0 {
b.WriteString(",")
}
b.WriteString(fmt.Sprintf("%s=%q", lb.GetName(), lb.GetValue()))
}
b.WriteString("}")
return b.String()
}
// sendData sends a given amount of bytes from s1 to s2.
func sendData(logf func(format string, args ...any), ctx context.Context, bytesCount int, s1, s2 *Server, s1ip, s2ip netip.Addr) error {
lb := must.Get(s1.Listen("tcp", fmt.Sprintf("%s:8081", s1ip)))
defer lb.Close()
// Dial to s1 from s2
w, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
return err
}
defer w.Close()
stopReceive := make(chan struct{})
defer close(stopReceive)
allReceived := make(chan error)
defer close(allReceived)
go func() {
conn, err := lb.Accept()
if err != nil {
allReceived <- err
return
}
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
total := 0
recvStart := time.Now()
for {
got := make([]byte, bytesCount)
n, err := conn.Read(got)
if err != nil {
allReceived <- fmt.Errorf("failed reading packet, %s", err)
return
}
got = got[:n]
select {
case <-stopReceive:
return
default:
}
total += n
logf("received %d/%d bytes, %.2f %%", total, bytesCount, (float64(total) / (float64(bytesCount)) * 100))
// Validate the received bytes to be the same as the sent bytes.
for _, b := range string(got) {
if b != 'A' {
allReceived <- fmt.Errorf("received unexpected byte: %c", b)
return
}
}
if total == bytesCount {
break
}
}
logf("all received, took: %s", time.Since(recvStart).String())
allReceived <- nil
}()
sendStart := time.Now()
w.SetWriteDeadline(time.Now().Add(30 * time.Second))
if _, err := w.Write(bytes.Repeat([]byte("A"), bytesCount)); err != nil {
stopReceive <- struct{}{}
return err
}
logf("all sent (%s), waiting for all packets (%d) to be received", time.Since(sendStart).String(), bytesCount)
err, _ = <-allReceived
if err != nil {
return err
}
return nil
}
func TestUserMetricsByteCounters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
defer s1.Close()
s2, s2ip, _ := startServer(t, ctx, controlURL, "s2")
defer s2.Close()
lc1, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// Force an update to the netmap to ensure that the metrics are up-to-date.
s1.lb.DebugForceNetmapUpdate()
s2.lb.DebugForceNetmapUpdate()
// Wait for both nodes to have a peer in their netmap.
waitForCondition(t, "waiting for netmaps to contain peer", 90*time.Second, func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
status1, err := lc1.Status(ctx)
if err != nil {
t.Logf("getting status: %s", err)
return false
}
status2, err := lc2.Status(ctx)
if err != nil {
t.Logf("getting status: %s", err)
return false
}
return len(status1.Peers()) > 0 && len(status2.Peers()) > 0
})
// ping to make sure the connection is up.
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
res, err := lc2.Ping(pingCtx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatalf("pinging: %s", err)
}
t.Logf("ping success: %#+v", res)
mustDirect(t, t.Logf, lc1, lc2)
// 1 megabytes
bytesToSend := 1 * 1024 * 1024
// This asserts generates some traffic, it is factored out
// of TestUDPConn.
start := time.Now()
err = sendData(t.Logf, ctx, bytesToSend, s1, s2, s1ip, s2ip)
if err != nil {
t.Fatalf("Failed to send packets: %v", err)
}
t.Logf("Sent %d bytes from s1 to s2 in %s", bytesToSend, time.Since(start).String())
ctxLc, cancelLc := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelLc()
metrics1, err := lc1.UserMetrics(ctxLc)
if err != nil {
t.Fatal(err)
}
parsedMetrics1, err := parseMetrics(metrics1)
if err != nil {
t.Fatal(err)
}
// Allow the metrics for the bytes sent to be off by 15%.
bytesSentTolerance := 1.15
t.Logf("Metrics1:\n%s\n", metrics1)
// Verify that the amount of data recorded in bytes is higher or equal to the data sent
inboundBytes1 := parsedMetrics1[`tailscaled_inbound_bytes_total{path="direct_ipv4"}`]
if inboundBytes1 < float64(bytesToSend) {
t.Errorf(`metrics1, tailscaled_inbound_bytes_total{path="direct_ipv4"}: expected higher (or equal) than %d, got: %f`, bytesToSend, inboundBytes1)
}
// But ensure that it is not too much higher than the data sent.
if inboundBytes1 > float64(bytesToSend)*bytesSentTolerance {
t.Errorf(`metrics1, tailscaled_inbound_bytes_total{path="direct_ipv4"}: expected lower than %f, got: %f`, float64(bytesToSend)*bytesSentTolerance, inboundBytes1)
}
metrics2, err := lc2.UserMetrics(ctx)
if err != nil {
t.Fatal(err)
}
parsedMetrics2, err := parseMetrics(metrics2)
if err != nil {
t.Fatal(err)
}
t.Logf("Metrics2:\n%s\n", metrics2)
// Verify that the amount of data recorded in bytes is higher or equal than the data sent.
outboundBytes2 := parsedMetrics2[`tailscaled_outbound_bytes_total{path="direct_ipv4"}`]
if outboundBytes2 < float64(bytesToSend) {
t.Errorf(`metrics2, tailscaled_outbound_bytes_total{path="direct_ipv4"}: expected higher (or equal) than %d, got: %f`, bytesToSend, outboundBytes2)
}
// But ensure that it is not too much higher than the data sent.
if outboundBytes2 > float64(bytesToSend)*bytesSentTolerance {
t.Errorf(`metrics2, tailscaled_outbound_bytes_total{path="direct_ipv4"}: expected lower than %f, got: %f`, float64(bytesToSend)*bytesSentTolerance, outboundBytes2)
}
}
func TestUserMetricsRouteGauges(t *testing.T) {
// Windows does not seem to support or report back routes when running in
// userspace via tsnet. So, we skip this check on Windows.
// TODO(kradalby): Figure out if this is correct.
if runtime.GOOS == "windows" {
t.Skipf("skipping on windows")
}
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
controlURL, c := startControl(t)
s1, _, s1PubKey := startServer(t, ctx, controlURL, "s1")
defer s1.Close()
s2, _, _ := startServer(t, ctx, controlURL, "s2")
defer s2.Close()
s1.lb.EditPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.0.3.0/24"),
netip.MustParsePrefix("192.0.5.1/32"),
netip.MustParsePrefix("0.0.0.0/0"),
},
},
AdvertiseRoutesSet: true,
})
c.SetSubnetRoutes(s1PubKey, []netip.Prefix{
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.0.5.1/32"),
netip.MustParsePrefix("0.0.0.0/0"),
})
lc1, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// Force an update to the netmap to ensure that the metrics are up-to-date.
s1.lb.DebugForceNetmapUpdate()
s2.lb.DebugForceNetmapUpdate()
wantRoutes := float64(2)
// Wait for the routes to be propagated to node 1 to ensure
// that the metrics are up-to-date.
waitForCondition(t, "primary routes available for node1", 90*time.Second, func() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
status1, err := lc1.Status(ctx)
if err != nil {
t.Logf("getting status: %s", err)
return false
}
// Wait for the primary routes to reach our desired routes, which is wantRoutes + 1, because
// the PrimaryRoutes list will contain a exit node route, which the metric does not count.
return status1.Self.PrimaryRoutes != nil && status1.Self.PrimaryRoutes.Len() == int(wantRoutes)+1
})
ctxLc, cancelLc := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelLc()
metrics1, err := lc1.UserMetrics(ctxLc)
if err != nil {
t.Fatal(err)
}
parsedMetrics1, err := parseMetrics(metrics1)
if err != nil {
t.Fatal(err)
}
t.Logf("Metrics1:\n%s\n", metrics1)
// The node is advertising 4 routes:
// - 192.0.2.0/24
// - 192.0.3.0/24
// - 192.0.5.1/32
if got, want := parsedMetrics1["tailscaled_advertised_routes"], 3.0; got != want {
t.Errorf("metrics1, tailscaled_advertised_routes: got %v, want %v", got, want)
}
// The control has approved 2 routes:
// - 192.0.2.0/24
// - 192.0.5.1/32
if got, want := parsedMetrics1["tailscaled_approved_routes"], wantRoutes; got != want {
t.Errorf("metrics1, tailscaled_approved_routes: got %v, want %v", got, want)
}
metrics2, err := lc2.UserMetrics(ctx)
if err != nil {
t.Fatal(err)
}
parsedMetrics2, err := parseMetrics(metrics2)
if err != nil {
t.Fatal(err)
}
t.Logf("Metrics2:\n%s\n", metrics2)
// The node is advertising 0 routes
if got, want := parsedMetrics2["tailscaled_advertised_routes"], 0.0; got != want {
t.Errorf("metrics2, tailscaled_advertised_routes: got %v, want %v", got, want)
}
// The control has approved 0 routes
if got, want := parsedMetrics2["tailscaled_approved_routes"], 0.0; got != want {
t.Errorf("metrics2, tailscaled_approved_routes: got %v, want %v", got, want)
}
}
func waitForCondition(t *testing.T, msg string, waitTime time.Duration, f func() bool) {
t.Helper()
for deadline := time.Now().Add(waitTime); time.Now().Before(deadline); time.Sleep(1 * time.Second) {
if f() {
return
}
}
t.Fatalf("waiting for condition: %s", msg)
}
// mustDirect ensures there is a direct connection between LocalClient 1 and 2
func mustDirect(t *testing.T, logf logger.Logf, lc1, lc2 *local.Client) {
t.Helper()
lastLog := time.Now().Add(-time.Minute)
// See https://github.com/tailscale/tailscale/issues/654
// and https://github.com/tailscale/tailscale/issues/3247 for discussions of this deadline.
for deadline := time.Now().Add(30 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
status1, err := lc1.Status(ctx)
if err != nil {
continue
}
status2, err := lc2.Status(ctx)
if err != nil {
continue
}
pst := status1.Peer[status2.Self.PublicKey]
if pst.CurAddr != "" {
logf("direct link %s->%s found with addr %s", status1.Self.HostName, status2.Self.HostName, pst.CurAddr)
return
}
if now := time.Now(); now.Sub(lastLog) > time.Second {
logf("no direct path %s->%s yet, addrs %v", status1.Self.HostName, status2.Self.HostName, pst.Addrs)
lastLog = now
}
}
t.Error("magicsock did not find a direct path from lc1 to lc2")
}
// chanTUN is a tun.Device for testing that uses channels for packet I/O.
// Inbound receives packets written to the TUN (from the perspective of the network stack).
// Outbound is for injecting packets to be read from the TUN.
type chanTUN struct {
Inbound chan []byte // packets written to TUN
Outbound chan []byte // packets to read from TUN
closed chan struct{}
events chan tun.Event
}
func newChanTUN() *chanTUN {
t := &chanTUN{
Inbound: make(chan []byte, 1024),
Outbound: make(chan []byte, 1024),
closed: make(chan struct{}),
events: make(chan tun.Event, 1),
}
t.events <- tun.EventUp
return t
}
func (t *chanTUN) File() *os.File { panic("not implemented") }
func (t *chanTUN) Close() error {
select {
case <-t.closed:
default:
close(t.closed)
close(t.Inbound)
}
return nil
}
func (t *chanTUN) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
select {
case <-t.closed:
return 0, io.EOF
case pkt := <-t.Outbound:
sizes[0] = copy(bufs[0][offset:], pkt)
return 1, nil
}
}
func (t *chanTUN) Write(bufs [][]byte, offset int) (int, error) {
for _, buf := range bufs {
pkt := buf[offset:]
if len(pkt) == 0 {
continue
}
select {
case <-t.closed:
return 0, errors.New("closed")
case t.Inbound <- slices.Clone(pkt):
default:
// Drop the packet if the channel is full, like a real
// TUN under congestion. This avoids blocking the
// WireGuard send path when no goroutine is draining.
}
}
return len(bufs), nil
}
func (t *chanTUN) MTU() (int, error) { return 1280, nil }
func (t *chanTUN) Name() (string, error) { return "chantun", nil }
func (t *chanTUN) Events() <-chan tun.Event { return t.events }
func (t *chanTUN) BatchSize() int { return 1 }
// listenTest provides common setup for listener and TUN tests.
type listenTest struct {
control *testcontrol.Server
s1, s2 *Server
s1ip4, s1ip6 netip.Addr
s2ip4, s2ip6 netip.Addr
tun *chanTUN // nil for netstack mode
}
// waitForPeerReachable blocks until s's current netmap contains the given peer
// with a non-zero HomeDERP and endpoints.
//
// This is polled via the LocalBackend's netmap rather than via the IPN bus
// because the bus does not carry HomeDERP or Endpoint deltas; the netmap
// itself is the source of truth for those fields.
func waitForPeerReachable(t *testing.T, s *Server, peer key.NodePublic) {
t.Helper()
if err := tstest.WaitFor(30*time.Second, func() error {
nm := s.lb.NetMapWithPeers()
if nm == nil {
return errors.New("no netmap yet")
}
for _, p := range nm.Peers {
if p.Key() != peer {
continue
}
if p.HomeDERP() == 0 {
return fmt.Errorf("peer %v: no HomeDERP", peer.ShortString())
}
if p.Endpoints().Len() == 0 {
return fmt.Errorf("peer %v: no endpoints", peer.ShortString())
}
return nil
}
return fmt.Errorf("peer %v not in netmap", peer.ShortString())
}); err != nil {
t.Fatalf("waitForPeerReachable(%v): %v", peer.ShortString(), err)
}
}
// setupTwoClientTest creates two tsnet servers for testing.
// If useTUN is true, s2 uses a chanTUN; otherwise it uses netstack only.
func setupTwoClientTest(t *testing.T, useTUN bool) *listenTest {
t.Helper()
tstest.ResourceCheck(t)
ctx := t.Context()
controlURL, control := startControl(t)
s1, _, _ := startServer(t, ctx, controlURL, "s1")
tmp := filepath.Join(t.TempDir(), "s2")
must.Do(os.MkdirAll(tmp, 0755))
s2 := &Server{
Dir: tmp,
ControlURL: controlURL,
Hostname: "s2",
Store: new(mem.Store),
Ephemeral: true,
}
var tun *chanTUN
if useTUN {
tun = newChanTUN()
s2.Tun = tun
}
if *verboseNodes {
s2.Logf = t.Logf
}
t.Cleanup(func() { s2.Close() })
s2status, err := s2.Up(ctx)
if err != nil {
t.Fatal(err)
}
s2.lb.ConfigureCertsForTest(testCertRoot.getCert)
s1ip4, s1ip6 := s1.TailscaleIPs()
s2ip4 := s2status.TailscaleIPs[0]
var s2ip6 netip.Addr
if len(s2status.TailscaleIPs) > 1 {
s2ip6 = s2status.TailscaleIPs[1]
}
waitForPeerReachable(t, s1, s2.lb.NodeKey())
waitForPeerReachable(t, s2, s1.lb.NodeKey())
lc1 := must.Get(s1.LocalClient())
pingCtx, cancelPing := pingTimeout(ctx)
defer cancelPing()
must.Get(lc1.Ping(pingCtx, s2ip4, tailcfg.PingTSMP))
return &listenTest{
control: control,
s1: s1,
s2: s2,
s1ip4: s1ip4,
s1ip6: s1ip6,
s2ip4: s2ip4,
s2ip6: s2ip6,
tun: tun,
}
}
// echoUDP returns an IP packet with src/dst and ports swapped, with checksums recomputed.
func echoUDP(pkt []byte) []byte {
var p packet.Parsed
p.Decode(pkt)
if p.IPProto != ipproto.UDP {
return nil
}
switch p.IPVersion {
case 4:
h := p.UDP4Header()
h.ToResponse()
return packet.Generate(h, p.Payload())
case 6:
h := packet.UDP6Header{
IP6Header: p.IP6Header(),
SrcPort: p.Src.Port(),
DstPort: p.Dst.Port(),
}
h.ToResponse()
return packet.Generate(h, p.Payload())
}
return nil
}
func TestTUN(t *testing.T) {
tt := setupTwoClientTest(t, true)
go func() {
for pkt := range tt.tun.Inbound {
var p packet.Parsed
p.Decode(pkt)
if p.Dst.Port() == 9999 {
tt.tun.Outbound <- echoUDP(pkt)
}
}
}()
test := func(t *testing.T, s2ip netip.Addr) {
conn, err := tt.s1.Dial(t.Context(), "udp", netip.AddrPortFrom(s2ip, 9999).String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
want := "hello from s1"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatal(err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
t.Fatalf("reading echo response: %v", err)
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("IPv4", func(t *testing.T) { test(t, tt.s2ip4) })
t.Run("IPv6", func(t *testing.T) { test(t, tt.s2ip6) })
}
// TestTUNDNS tests that a TUN can send DNS queries to quad-100 and receive
// responses. This verifies that handleLocalPackets intercepts outbound traffic
// to the service IP.
func TestTUNDNS(t *testing.T) {
tt := setupTwoClientTest(t, true)
test := func(t *testing.T, srcIP netip.Addr, serviceIP netip.Addr) {
tt.tun.Outbound <- buildDNSQuery("s2", srcIP)
ipVersion := uint8(4)
if srcIP.Is6() {
ipVersion = 6
}
for {
select {
case pkt := <-tt.tun.Inbound:
var p packet.Parsed
p.Decode(pkt)
if p.IPVersion != ipVersion || p.IPProto != ipproto.UDP {
continue
}
if p.Src.Addr() == serviceIP && p.Src.Port() == 53 {
if len(p.Payload()) < 12 {
t.Fatalf("DNS response too short: %d bytes", len(p.Payload()))
}
return // success
}
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for DNS response")
}
}
}
t.Run("IPv4", func(t *testing.T) {
test(t, tt.s2ip4, netip.MustParseAddr("100.100.100.100"))
})
t.Run("IPv6", func(t *testing.T) {
test(t, tt.s2ip6, netip.MustParseAddr("fd7a:115c:a1e0::53"))
})
}
// TestListenPacket tests UDP listeners (ListenPacket) in both netstack and TUN modes.
func TestListenPacket(t *testing.T) {
testListenPacket := func(t *testing.T, lt *listenTest, listenIP netip.Addr) {
pc, err := lt.s2.ListenPacket("udp", netip.AddrPortFrom(listenIP, 0).String())
if err != nil {
t.Fatal(err)
}
defer pc.Close()
echoErr := make(chan error, 1)
go func() {
buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
echoErr <- err
return
}
_, err = pc.WriteTo(buf[:n], addr)
if err != nil {
echoErr <- err
return
}
}()
conn, err := lt.s1.Dial(t.Context(), "udp", pc.LocalAddr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
want := "hello udp"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatal(err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("Read failed: %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("IPv4", func(t *testing.T) { testListenPacket(t, lt, lt.s2ip4) })
t.Run("IPv6", func(t *testing.T) { testListenPacket(t, lt, lt.s2ip6) })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
t.Run("IPv4", func(t *testing.T) { testListenPacket(t, lt, lt.s2ip4) })
t.Run("IPv6", func(t *testing.T) { testListenPacket(t, lt, lt.s2ip6) })
})
}
// TestListenTCP tests TCP listeners with concrete addresses in both netstack
// and TUN modes.
func TestListenTCP(t *testing.T) {
testListenTCP := func(t *testing.T, lt *listenTest, listenIP netip.Addr) {
ln, err := lt.s2.Listen("tcp", netip.AddrPortFrom(listenIP, 0).String())
if err != nil {
t.Fatal(err)
}
defer ln.Close()
echoErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
echoErr <- err
return
}
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
echoErr <- err
return
}
_, err = conn.Write(buf[:n])
if err != nil {
echoErr <- err
return
}
}()
conn, err := lt.s1.Dial(t.Context(), "tcp", ln.Addr().String())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
want := "hello tcp"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write failed: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("Read failed: %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("IPv4", func(t *testing.T) { testListenTCP(t, lt, lt.s2ip4) })
t.Run("IPv6", func(t *testing.T) { testListenTCP(t, lt, lt.s2ip6) })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
t.Run("IPv4", func(t *testing.T) { testListenTCP(t, lt, lt.s2ip4) })
t.Run("IPv6", func(t *testing.T) { testListenTCP(t, lt, lt.s2ip6) })
})
}
// TestListenTCPDualStack tests TCP listeners with wildcard addresses (dual-stack)
// in both netstack and TUN modes.
func TestListenTCPDualStack(t *testing.T) {
testListenTCPDualStack := func(t *testing.T, lt *listenTest, dialIP netip.Addr) {
ln, err := lt.s2.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
_, portStr, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatalf("parsing listener address %q: %v", ln.Addr().String(), err)
}
echoErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
echoErr <- err
return
}
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
echoErr <- err
return
}
_, err = conn.Write(buf[:n])
if err != nil {
echoErr <- err
return
}
}()
dialAddr := net.JoinHostPort(dialIP.String(), portStr)
conn, err := lt.s1.Dial(t.Context(), "tcp", dialAddr)
if err != nil {
t.Fatalf("Dial(%q) failed: %v", dialAddr, err)
}
defer conn.Close()
want := "hello tcp dualstack"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write failed: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("Read failed: %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("DialIPv4", func(t *testing.T) { testListenTCPDualStack(t, lt, lt.s2ip4) })
t.Run("DialIPv6", func(t *testing.T) { testListenTCPDualStack(t, lt, lt.s2ip6) })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
t.Run("DialIPv4", func(t *testing.T) { testListenTCPDualStack(t, lt, lt.s2ip4) })
t.Run("DialIPv6", func(t *testing.T) { testListenTCPDualStack(t, lt, lt.s2ip6) })
})
}
// TestDialTCP tests TCP dialing from s2 to s1 in both netstack and TUN modes.
// In TUN mode, this verifies that outbound TCP connections and their replies
// are handled by netstack without packets escaping to the TUN.
func TestDialTCP(t *testing.T) {
testDialTCP := func(t *testing.T, lt *listenTest, listenIP netip.Addr) {
ln, err := lt.s1.Listen("tcp", netip.AddrPortFrom(listenIP, 0).String())
if err != nil {
t.Fatal(err)
}
defer ln.Close()
echoErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
echoErr <- err
return
}
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
echoErr <- err
return
}
_, err = conn.Write(buf[:n])
if err != nil {
echoErr <- err
return
}
}()
conn, err := lt.s2.Dial(t.Context(), "tcp", ln.Addr().String())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
want := "hello tcp dial"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write failed: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("Read failed: %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("IPv4", func(t *testing.T) { testDialTCP(t, lt, lt.s1ip4) })
t.Run("IPv6", func(t *testing.T) { testDialTCP(t, lt, lt.s1ip6) })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
var escapedTCPPackets atomic.Int32
var wg sync.WaitGroup
wg.Go(func() {
for pkt := range lt.tun.Inbound {
var p packet.Parsed
p.Decode(pkt)
if p.IPProto == ipproto.TCP {
escapedTCPPackets.Add(1)
t.Logf("TCP packet escaped to TUN: %v -> %v", p.Src, p.Dst)
}
}
})
t.Run("IPv4", func(t *testing.T) { testDialTCP(t, lt, lt.s1ip4) })
t.Run("IPv6", func(t *testing.T) { testDialTCP(t, lt, lt.s1ip6) })
lt.tun.Close()
wg.Wait()
if escaped := escapedTCPPackets.Load(); escaped > 0 {
t.Errorf("%d TCP packets escaped to TUN", escaped)
}
})
}
// TestDialUDP tests UDP dialing from s2 to s1 in both netstack and TUN modes.
// In TUN mode, this verifies that outbound UDP connections register endpoints
// with gVisor, allowing reply packets to be routed through netstack instead of
// escaping to the TUN.
func TestDialUDP(t *testing.T) {
testDialUDP := func(t *testing.T, lt *listenTest, listenIP netip.Addr) {
pc, err := lt.s1.ListenPacket("udp", netip.AddrPortFrom(listenIP, 0).String())
if err != nil {
t.Fatal(err)
}
defer pc.Close()
echoErr := make(chan error, 1)
go func() {
buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
echoErr <- err
return
}
_, err = pc.WriteTo(buf[:n], addr)
if err != nil {
echoErr <- err
return
}
}()
conn, err := lt.s2.Dial(t.Context(), "udp", pc.LocalAddr().String())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
want := "hello udp dial"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write failed: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("Read failed: %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("IPv4", func(t *testing.T) { testDialUDP(t, lt, lt.s1ip4) })
t.Run("IPv6", func(t *testing.T) { testDialUDP(t, lt, lt.s1ip6) })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
var escapedUDPPackets atomic.Int32
var wg sync.WaitGroup
wg.Go(func() {
for pkt := range lt.tun.Inbound {
var p packet.Parsed
p.Decode(pkt)
if p.IPProto == ipproto.UDP {
escapedUDPPackets.Add(1)
t.Logf("UDP packet escaped to TUN: %v -> %v", p.Src, p.Dst)
}
}
})
t.Run("IPv4", func(t *testing.T) { testDialUDP(t, lt, lt.s1ip4) })
t.Run("IPv6", func(t *testing.T) { testDialUDP(t, lt, lt.s1ip6) })
lt.tun.Close()
wg.Wait()
if escaped := escapedUDPPackets.Load(); escaped > 0 {
t.Errorf("%d UDP packets escaped to TUN", escaped)
}
})
}
// TestDialUDPInjectedReadRecordsFlowState reproduces tailscale/tailscale#14229
// and #20064: a tsnet/netstack client dialing UDP must record reverse-flow
// state in its inbound filter for the outbound packet it injects via
// [netstack.Impl] → [tstun.Wrapper.InjectOutboundPacketBuffer]. If it doesn't,
// the inbound reply is silently dropped by the inbound packet filter when no
// ACL rule explicitly admits it.
//
// [TestDialUDP] doesn't catch this because [testcontrol.Server] serves
// [tailcfg.FilterAllowAll] by default, so the reply is always admitted by
// rule and the flow-state path is never exercised. Each subtest below sets
// up s1 and s2 so that the inbound reply is admissible only via the
// reverse-flow state recorded when s2 dialed.
func TestDialUDPInjectedReadRecordsFlowState(t *testing.T) {
// RestrictedACL replaces the default allow-all PacketFilter with a
// one-way rule that permits s2 → s1 only. The reply path s1 → s2 matches
// no rule, so it can only be admitted by reverse-flow state on s2's
// main filter.
t.Run("RestrictedACL", func(t *testing.T) {
lt := setupTwoClientTest(t, false) // netstack on both sides.
rule := []tailcfg.FilterRule{{
SrcIPs: []string{lt.s2ip4.String(), lt.s2ip6.String()},
DstPorts: []tailcfg.NetPortRange{
{IP: lt.s1ip4.String(), Ports: tailcfg.PortRange{First: 0, Last: 65535}},
{IP: lt.s1ip6.String(), Ports: tailcfg.PortRange{First: 0, Last: 65535}},
},
IPProto: []int{int(ipproto.TCP), int(ipproto.UDP)},
}}
for _, s := range []*Server{lt.s1, lt.s2} {
if !lt.control.AddRawMapResponse(s.lb.NodeKey(), &tailcfg.MapResponse{
PacketFilter: rule,
}) {
t.Fatalf("AddRawMapResponse(%s) failed", s.Hostname)
}
}
// PacketFilter-only changes don't necessarily fire peer/netmap
// notifications, so poll the wgengine filter directly.
if err := tstest.WaitFor(30*time.Second, func() error {
f := lt.s2.lb.GetFilterForTest()
if f == nil {
return errors.New("no filter yet")
}
if got := f.Check(lt.s1ip4, lt.s2ip4, 1234, ipproto.UDP); got != filter.Drop {
return fmt.Errorf("inbound s1 → s2:1234 UDP: got %v, want Drop", got)
}
return nil
}); err != nil {
t.Fatalf("waiting for restrictive filter on s2: %v", err)
}
runDialUDPEcho(t, lt)
})
// JailedPeer marks s1 as jailed from s2's perspective. tstun.Wrapper
// then routes s2's outbound to s1 and inbound from s1 through a
// separate "jailed" filter (a shields-up filter with no rules; also
// used for Mullvad exit nodes). The reply path can only be admitted
// by reverse-flow state on the *jailed* filter, so injectedRead must
// select the right filter for the outbound packet.
t.Run("JailedPeer", func(t *testing.T) {
lt := setupTwoClientTest(t, false) // netstack on both sides.
s1Key := lt.s1.lb.NodeKey()
lt.control.SetJailed(lt.s2.lb.NodeKey(), s1Key, true)
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
if err := waitFor(t, ctx, lt.s2, func(nm *netmap.NetworkMap) bool {
return slices.ContainsFunc(nm.Peers, func(p tailcfg.NodeView) bool {
return p.Key() == s1Key && p.IsJailed()
})
}); err != nil {
t.Fatalf("waiting for s1 to appear jailed in s2's netmap: %v", err)
}
runDialUDPEcho(t, lt)
})
}
// runDialUDPEcho runs an s2.Dial("udp", s1-listener)/Write/Read round trip
// against listeners on lt.s1's IPv4 and IPv6 addresses as t.Run subtests,
// asserting that the echoed reply makes it back to s2. The caller is
// responsible for configuring lt so that the inbound reply on s2 is only
// admissible via reverse-flow state recorded by the outbound dial.
func runDialUDPEcho(t *testing.T, lt *listenTest) {
t.Helper()
test := func(t *testing.T, listenIP netip.Addr) {
pc, err := lt.s1.ListenPacket("udp", netip.AddrPortFrom(listenIP, 0).String())
if err != nil {
t.Fatalf("ListenPacket: %v", err)
}
defer pc.Close()
echoErr := make(chan error, 1)
go func() {
buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
echoErr <- err
return
}
if _, err := pc.WriteTo(buf[:n], addr); err != nil {
echoErr <- err
}
}()
conn, err := lt.s2.Dial(t.Context(), "udp", pc.LocalAddr().String())
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer conn.Close()
want := "hello udp"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("echo error: %v; read error: %v", e, err)
default:
t.Fatalf("UDP reply dropped — injectedRead didn't record flow state on the filter that runs on the reply (#14229, #20064): %v", err)
}
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
}
t.Run("IPv4", func(t *testing.T) { test(t, lt.s1ip4) })
t.Run("IPv6", func(t *testing.T) { test(t, lt.s1ip6) })
}
// buildDNSQuery builds a UDP/IP packet containing a DNS query for name to the
// Tailscale service IP (100.100.100.100 for IPv4, fd7a:115c:a1e0::53 for IPv6).
func buildDNSQuery(name string, srcIP netip.Addr) []byte {
qtype := byte(0x01) // Type A for IPv4
if srcIP.Is6() {
qtype = 0x1c // Type AAAA for IPv6
}
dns := []byte{
0x12, 0x34, // ID
0x01, 0x00, // Flags: standard query, recursion desired
0x00, 0x01, // QDCOUNT: 1
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ANCOUNT, NSCOUNT, ARCOUNT
}
for label := range strings.SplitSeq(name, ".") {
dns = append(dns, byte(len(label)))
dns = append(dns, label...)
}
dns = append(dns, 0x00, 0x00, qtype, 0x00, 0x01) // null, Type A/AAAA, Class IN
if srcIP.Is4() {
h := packet.UDP4Header{
IP4Header: packet.IP4Header{
Src: srcIP,
Dst: netip.MustParseAddr("100.100.100.100"),
},
SrcPort: 12345,
DstPort: 53,
}
return packet.Generate(h, dns)
}
h := packet.UDP6Header{
IP6Header: packet.IP6Header{
Src: srcIP,
Dst: netip.MustParseAddr("fd7a:115c:a1e0::53"),
},
SrcPort: 12345,
DstPort: 53,
}
return packet.Generate(h, dns)
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
BadDeps: map[string]string{
"golang.org/x/crypto/ssh": "tsnet should not depend on SSH",
"golang.org/x/crypto/ssh/internal/bcrypt_pbkdf": "tsnet should not depend on SSH",
"tailscale.com/feature/syspolicy": "tsnet should not depend on syspolicy",
"tailscale.com/ipn/store/awsstore": "tsnet callers wanting AWS state storage should import awsstore themselves",
"tailscale.com/ipn/store/kubestore": "tsnet callers wanting Kubernetes state storage should import kubestore themselves",
"tailscale.com/wif": "tsnet callers wanting workload identity federation should import tailscale.com/feature/identityfederation themselves",
},
OnDep: func(dep string) {
if strings.Contains(dep, "portlist") ||
strings.Contains(dep, "github.com/aws/") ||
strings.Contains(dep, "k8s.io/") {
t.Errorf("unexpected dep: %q", dep)
}
},
}.Check(t)
}
func TestResolveAuthKey(t *testing.T) {
tests := []struct {
name string
authKey string
clientSecret string
clientID string
idToken string
audience string
oauthAvailable bool
wifAvailable bool
resolveViaOAuth func(ctx context.Context, clientSecret string, tags []string) (string, error)
resolveViaWIF func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error)
wantAuthKey string
wantErr bool
wantErrContains string
}{
{
name: "success-oauth-client-secret",
clientSecret: "tskey-client-secret-123",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
if clientSecret != "tskey-client-secret-123" {
return "", fmt.Errorf("unexpected client secret: %s", clientSecret)
}
return "tskey-auth-via-oauth", nil
},
wantAuthKey: "tskey-auth-via-oauth",
wantErrContains: "",
},
{
name: "fail-oauth-client-secret",
clientSecret: "tskey-client-secret-123",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
return "", fmt.Errorf("resolution failed")
},
wantErrContains: "resolution failed",
},
{
name: "success-federated-id-token",
clientID: "client-id-123",
idToken: "id-token-456",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
if clientID != "client-id-123" {
return "", fmt.Errorf("unexpected client ID: %s", clientID)
}
if idToken != "id-token-456" {
return "", fmt.Errorf("unexpected ID token: %s", idToken)
}
return "tskey-auth-via-wif", nil
},
wantAuthKey: "tskey-auth-via-wif",
wantErrContains: "",
},
{
name: "success-federated-audience",
clientID: "client-id-123",
audience: "api.tailscale.com",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
if clientID != "client-id-123" {
return "", fmt.Errorf("unexpected client ID: %s", clientID)
}
if audience != "api.tailscale.com" {
return "", fmt.Errorf("unexpected ID token: %s", idToken)
}
return "tskey-auth-via-wif", nil
},
wantAuthKey: "tskey-auth-via-wif",
wantErrContains: "",
},
{
name: "fail-federated-id-token",
clientID: "client-id-123",
idToken: "id-token-456",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("resolution failed")
},
wantErrContains: "resolution failed",
},
{
name: "empty-client-id-with-token",
clientID: "",
idToken: "id-token-456",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantErrContains: "empty",
},
{
name: "empty-client-id-with-audience",
clientID: "",
audience: "api.tailscale.com",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantErrContains: "empty",
},
{
name: "empty-id-token",
clientID: "client-id-123",
idToken: "",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantErrContains: "empty",
},
{
name: "audience-with-id-token",
clientID: "client-id-123",
idToken: "id-token-456",
audience: "api.tailscale.com",
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantErrContains: "only one of ID token and audience",
},
{
name: "wif-skipped-oauth-succeeds",
clientSecret: "tskey-client-secret-123",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
if clientSecret != "tskey-client-secret-123" {
return "", fmt.Errorf("unexpected client secret: %s", clientSecret)
}
return "tskey-auth-via-oauth", nil
},
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantAuthKey: "tskey-auth-via-oauth",
wantErrContains: "",
},
{
name: "wif-skipped-oauth-fails",
clientID: "tskey-client-id-123",
idToken: "",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
return "", fmt.Errorf("resolution failed")
},
wifAvailable: true,
resolveViaWIF: func(ctx context.Context, baseURL, clientID, idToken, audience string, tags []string) (string, error) {
return "", fmt.Errorf("should not be called")
},
wantErrContains: "failed",
},
{
name: "authkey-set-no-resolution",
authKey: "tskey-auth-123",
oauthAvailable: false,
wifAvailable: false,
wantAuthKey: "tskey-auth-123",
wantErrContains: "",
},
{
name: "no-authkey-no-resolution",
oauthAvailable: false,
wifAvailable: false,
wantAuthKey: "",
wantErrContains: "",
},
{
name: "authkey-client-secret-oauth-succeeds",
authKey: "tskey-client-secret-123",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
if clientSecret != "tskey-client-secret-123" {
return "", fmt.Errorf("unexpected client secret: %s", clientSecret)
}
return "tskey-auth-via-oauth", nil
},
wantAuthKey: "tskey-auth-via-oauth",
wantErrContains: "",
},
{
name: "authkey-client-secret-oauth-fails",
authKey: "tskey-client-secret-123",
oauthAvailable: true,
resolveViaOAuth: func(ctx context.Context, clientSecret string, tags []string) (string, error) {
return "", fmt.Errorf("resolution failed")
},
wantErrContains: "resolution failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.oauthAvailable {
t.Cleanup(tailscale.HookResolveAuthKey.SetForTest(tt.resolveViaOAuth))
}
if tt.wifAvailable {
t.Cleanup(tailscale.HookResolveAuthKeyViaWIF.SetForTest(tt.resolveViaWIF))
}
s := &Server{
AuthKey: tt.authKey,
ClientSecret: tt.clientSecret,
ClientID: tt.clientID,
IDToken: tt.idToken,
Audience: tt.audience,
ControlURL: "https://control.example.com",
}
s.shutdownCtx = context.Background()
gotAuthKey, err := s.resolveAuthKey()
if tt.wantErrContains != "" {
if err == nil {
t.Errorf("expected error but got none")
return
}
if !strings.Contains(err.Error(), tt.wantErrContains) {
t.Errorf("expected error containing %q but got error: %v", tt.wantErrContains, err)
}
return
}
if err != nil {
t.Errorf("resolveAuthKey expected no error but got error: %v", err)
return
}
if gotAuthKey != tt.wantAuthKey {
t.Errorf("resolveAuthKey() = %q, want %q", gotAuthKey, tt.wantAuthKey)
}
})
}
}
// TestSelfDial verifies that a single tsnet.Server can Dial its own Listen
// address. This is a regression test for a bug where self-addressed TCP SYN
// packets were sent to WireGuard (which has no peer for the node's own IP)
// and silently dropped, causing Dial to hang indefinitely.
func TestSelfDial(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
errc := make(chan error, 1)
connc := make(chan net.Conn, 1)
go func() {
c, err := ln.Accept()
if err != nil {
errc <- err
return
}
connc <- c
}()
// Self-dial: the same server dials its own Tailscale IP.
w, err := s1.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatalf("self-dial failed: %v", err)
}
defer w.Close()
var accepted net.Conn
select {
case accepted = <-connc:
case err := <-errc:
t.Fatalf("accept failed: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("timeout waiting for accept")
}
defer accepted.Close()
// Verify bidirectional data exchange.
want := "hello self"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
if _, err := io.ReadFull(accepted, got); err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Errorf("client->server: got %q, want %q", got, want)
}
reply := "hello back"
if _, err := io.WriteString(accepted, reply); err != nil {
t.Fatal(err)
}
gotReply := make([]byte, len(reply))
if _, err := io.ReadFull(w, gotReply); err != nil {
t.Fatal(err)
}
if string(gotReply) != reply {
t.Errorf("server->client: got %q, want %q", gotReply, reply)
}
}
// TestListenUnspecifiedAddr verifies that listening on 0.0.0.0 or [::] works
// the same as listening on an empty host (":port"), accepting connections
// destined to the node's Tailscale IPs.
func TestListenUnspecifiedAddr(t *testing.T) {
testUnspec := func(t *testing.T, lt *listenTest, addr, dialPort string) {
ln, err := lt.s2.Listen("tcp", addr)
if err != nil {
t.Fatal(err)
}
defer ln.Close()
echoErr := make(chan error, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
echoErr <- err
return
}
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
echoErr <- err
return
}
_, err = conn.Write(buf[:n])
echoErr <- err
}()
dialAddr := net.JoinHostPort(lt.s2ip4.String(), dialPort)
conn, err := lt.s1.Dial(t.Context(), "tcp", dialAddr)
if err != nil {
t.Fatalf("Dial(%q) failed: %v", dialAddr, err)
}
defer conn.Close()
want := "hello unspec"
if _, err := conn.Write([]byte(want)); err != nil {
t.Fatalf("Write failed: %v", err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
n, err := conn.Read(got)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
if string(got[:n]) != want {
t.Errorf("got %q, want %q", got[:n], want)
}
if err := <-echoErr; err != nil {
t.Fatalf("echo error: %v", err)
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
t.Run("v4-unspec", func(t *testing.T) { testUnspec(t, lt, "0.0.0.0:8080", "8080") })
t.Run("::", func(t *testing.T) { testUnspec(t, lt, "[::]:8081", "8081") })
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
t.Run("v4-unspec", func(t *testing.T) { testUnspec(t, lt, "0.0.0.0:8080", "8080") })
t.Run("::", func(t *testing.T) { testUnspec(t, lt, "[::]:8081", "8081") })
})
}
// TestListenMultipleEphemeralPorts verifies that calling Listen with port 0
// multiple times allocates distinct ports, each of which can receive
// connections independently.
func TestListenMultipleEphemeralPorts(t *testing.T) {
testMultipleEphemeral := func(t *testing.T, lt *listenTest) {
const n = 3
listeners := make([]net.Listener, n)
ports := make([]string, n)
for i := range n {
ln, err := lt.s2.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
_, portStr, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatalf("parsing Addr %q: %v", ln.Addr(), err)
}
if portStr == "0" {
t.Fatal("Addr() returned port 0; expected allocated port")
}
for j := range i {
if ports[j] == portStr {
t.Fatalf("listeners %d and %d both got port %s", j, i, portStr)
}
}
listeners[i] = ln
ports[i] = portStr
}
// Verify each listener independently accepts connections.
for i := range n {
echoErr := make(chan error, 1)
go func() {
conn, err := listeners[i].Accept()
if err != nil {
echoErr <- err
return
}
defer conn.Close()
buf := make([]byte, 1024)
rn, err := conn.Read(buf)
if err != nil {
echoErr <- err
return
}
_, err = conn.Write(buf[:rn])
echoErr <- err
}()
dialAddr := net.JoinHostPort(lt.s2ip4.String(), ports[i])
conn, err := lt.s1.Dial(t.Context(), "tcp", dialAddr)
if err != nil {
t.Fatalf("listener %d: Dial(%q) failed: %v", i, dialAddr, err)
}
want := fmt.Sprintf("hello port %d", i)
if _, err := conn.Write([]byte(want)); err != nil {
conn.Close()
t.Fatalf("listener %d: Write failed: %v", i, err)
}
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
got := make([]byte, 1024)
rn, err := conn.Read(got)
conn.Close()
if err != nil {
select {
case e := <-echoErr:
t.Fatalf("listener %d: echo error: %v; read error: %v", i, e, err)
default:
t.Fatalf("listener %d: Read failed: %v", i, err)
}
}
if string(got[:rn]) != want {
t.Errorf("listener %d: got %q, want %q", i, got[:rn], want)
}
if err := <-echoErr; err != nil {
t.Fatalf("listener %d: echo error: %v", i, err)
}
}
}
t.Run("Netstack", func(t *testing.T) {
lt := setupTwoClientTest(t, false)
testMultipleEphemeral(t, lt)
})
t.Run("TUN", func(t *testing.T) {
lt := setupTwoClientTest(t, true)
testMultipleEphemeral(t, lt)
})
}