|
|
@@ -39,6 +39,7 @@ import (
|
|
|
"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"
|
|
|
@@ -48,11 +49,13 @@ import (
|
|
|
"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/views"
|
|
|
@@ -1860,6 +1863,676 @@ func mustDirect(t *testing.T, logf logger.Logf, lc1, lc2 *local.Client) {
|
|
|
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, 10),
|
|
|
+ Outbound: make(chan []byte, 10),
|
|
|
+ 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):
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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 {
|
|
|
+ s1, s2 *Server
|
|
|
+ s1ip4, s1ip6 netip.Addr
|
|
|
+ s2ip4, s2ip6 netip.Addr
|
|
|
+ tun *chanTUN // nil for netstack mode
|
|
|
+}
|
|
|
+
|
|
|
+// setupListenTest creates two tsnet servers for testing.
|
|
|
+// If useTUN is true, s2 uses a chanTUN; otherwise it uses netstack only.
|
|
|
+func setupListenTest(t *testing.T, useTUN bool) *listenTest {
|
|
|
+ t.Helper()
|
|
|
+ tstest.Shard(t)
|
|
|
+ tstest.ResourceCheck(t)
|
|
|
+ ctx := t.Context()
|
|
|
+ controlURL, _ := 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ s1ip4, s1ip6 := s1.TailscaleIPs()
|
|
|
+ s2ip4 := s2status.TailscaleIPs[0]
|
|
|
+ var s2ip6 netip.Addr
|
|
|
+ if len(s2status.TailscaleIPs) > 1 {
|
|
|
+ s2ip6 = s2status.TailscaleIPs[1]
|
|
|
+ }
|
|
|
+
|
|
|
+ lc1 := must.Get(s1.LocalClient())
|
|
|
+ must.Get(lc1.Ping(ctx, s2ip4, tailcfg.PingTSMP))
|
|
|
+
|
|
|
+ return &listenTest{
|
|
|
+ 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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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 := setupListenTest(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)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 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.Split(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) {
|
|
|
tstest.Shard(t)
|
|
|
deptest.DepChecker{
|