// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package netstack

import (
	"context"
	"fmt"
	"maps"
	"net"
	"net/netip"
	"runtime"
	"testing"
	"time"

	"gvisor.dev/gvisor/pkg/buffer"
	"gvisor.dev/gvisor/pkg/tcpip"
	"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
	"gvisor.dev/gvisor/pkg/tcpip/header"
	"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
	"gvisor.dev/gvisor/pkg/tcpip/stack"
	"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
	"tailscale.com/envknob"
	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnlocal"
	"tailscale.com/ipn/store/mem"
	"tailscale.com/metrics"
	"tailscale.com/net/netx"
	"tailscale.com/net/packet"
	"tailscale.com/net/tsaddr"
	"tailscale.com/net/tsdial"
	"tailscale.com/net/tstun"
	"tailscale.com/tsd"
	"tailscale.com/tstest"
	"tailscale.com/types/ipproto"
	"tailscale.com/types/logid"
	"tailscale.com/types/netmap"
	"tailscale.com/util/clientmetric"
	"tailscale.com/wgengine"
	"tailscale.com/wgengine/filter"
)

// TestInjectInboundLeak tests that injectInbound doesn't leak memory.
// See https://github.com/tailscale/tailscale/issues/3762
func TestInjectInboundLeak(t *testing.T) {
	tunDev := tstun.NewFake()
	dialer := new(tsdial.Dialer)
	logf := func(format string, args ...any) {
		if !t.Failed() {
			t.Logf(format, args...)
		}
	}
	sys := tsd.NewSystem()
	eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
		Tun:           tunDev,
		Dialer:        dialer,
		SetSubsystem:  sys.Set,
		HealthTracker: sys.HealthTracker.Get(),
		Metrics:       sys.UserMetricsRegistry(),
		EventBus:      sys.Bus.Get(),
	})
	if err != nil {
		t.Fatal(err)
	}
	defer eng.Close()
	sys.Set(eng)
	sys.Set(new(mem.Store))

	tunWrap := sys.Tun.Get()
	lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(lb.Shutdown)

	ns, err := Create(logf, tunWrap, eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
	if err != nil {
		t.Fatal(err)
	}
	defer ns.Close()
	ns.ProcessLocalIPs = true
	if err := ns.Start(lb); err != nil {
		t.Fatalf("Start: %v", err)
	}
	ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })

	pkt := &packet.Parsed{}
	const N = 10_000
	ms0 := getMemStats()
	for range N {
		outcome, _ := ns.injectInbound(pkt, tunWrap, nil)
		if outcome != filter.DropSilently {
			t.Fatalf("got outcome %v; want DropSilently", outcome)
		}
	}
	ms1 := getMemStats()
	if grew := int64(ms1.HeapObjects) - int64(ms0.HeapObjects); grew >= N {
		t.Fatalf("grew by %v (which is too much and >= the %v packets we sent)", grew, N)
	}
}

func getMemStats() (ms runtime.MemStats) {
	runtime.GC()
	runtime.ReadMemStats(&ms)
	return
}

func makeNetstack(tb testing.TB, config func(*Impl)) *Impl {
	tunDev := tstun.NewFake()
	sys := tsd.NewSystem()
	sys.Set(new(mem.Store))
	dialer := new(tsdial.Dialer)
	logf := tstest.WhileTestRunningLogger(tb)
	eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
		Tun:           tunDev,
		Dialer:        dialer,
		SetSubsystem:  sys.Set,
		HealthTracker: sys.HealthTracker.Get(),
		Metrics:       sys.UserMetricsRegistry(),
		EventBus:      sys.Bus.Get(),
	})
	if err != nil {
		tb.Fatal(err)
	}
	tb.Cleanup(func() { eng.Close() })
	sys.Set(eng)

	ns, err := Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
	if err != nil {
		tb.Fatal(err)
	}
	tb.Cleanup(func() { ns.Close() })
	sys.Set(ns)

	lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
	if err != nil {
		tb.Fatalf("NewLocalBackend: %v", err)
	}
	tb.Cleanup(lb.Shutdown)

	ns.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return true })
	if config != nil {
		config(ns)
	}
	if err := ns.Start(lb); err != nil {
		tb.Fatalf("Start: %v", err)
	}
	return ns
}

func TestShouldHandlePing(t *testing.T) {
	srcIP := netip.AddrFrom4([4]byte{1, 2, 3, 4})

	t.Run("ICMP4", func(t *testing.T) {
		dst := netip.MustParseAddr("5.6.7.8")
		icmph := packet.ICMP4Header{
			IP4Header: packet.IP4Header{
				IPProto: ipproto.ICMPv4,
				Src:     srcIP,
				Dst:     dst,
			},
			Type: packet.ICMP4EchoRequest,
			Code: packet.ICMP4NoCode,
		}
		_, payload := packet.ICMPEchoPayload(nil)
		icmpPing := packet.Generate(icmph, payload)
		pkt := &packet.Parsed{}
		pkt.Decode(icmpPing)

		impl := makeNetstack(t, func(impl *Impl) {
			impl.ProcessSubnets = true
		})
		pingDst, ok := impl.shouldHandlePing(pkt)
		if !ok {
			t.Errorf("expected shouldHandlePing==true")
		}
		if pingDst != dst {
			t.Errorf("got dst %s; want %s", pingDst, dst)
		}
	})

	t.Run("ICMP6-no-via", func(t *testing.T) {
		dst := netip.MustParseAddr("2a09:8280:1::4169")
		icmph := packet.ICMP6Header{
			IP6Header: packet.IP6Header{
				IPProto: ipproto.ICMPv6,
				Src:     srcIP,
				Dst:     dst,
			},
			Type: packet.ICMP6EchoRequest,
			Code: packet.ICMP6NoCode,
		}
		_, payload := packet.ICMPEchoPayload(nil)
		icmpPing := packet.Generate(icmph, payload)
		pkt := &packet.Parsed{}
		pkt.Decode(icmpPing)

		impl := makeNetstack(t, func(impl *Impl) {
			impl.ProcessSubnets = true
		})
		pingDst, ok := impl.shouldHandlePing(pkt)

		// Expect that we handle this since it's going out onto the
		// network.
		if !ok {
			t.Errorf("expected shouldHandlePing==true")
		}
		if pingDst != dst {
			t.Errorf("got dst %s; want %s", pingDst, dst)
		}
	})

	t.Run("ICMP6-tailscale-addr", func(t *testing.T) {
		dst := netip.MustParseAddr("fd7a:115c:a1e0:ab12::1")
		icmph := packet.ICMP6Header{
			IP6Header: packet.IP6Header{
				IPProto: ipproto.ICMPv6,
				Src:     srcIP,
				Dst:     dst,
			},
			Type: packet.ICMP6EchoRequest,
			Code: packet.ICMP6NoCode,
		}
		_, payload := packet.ICMPEchoPayload(nil)
		icmpPing := packet.Generate(icmph, payload)
		pkt := &packet.Parsed{}
		pkt.Decode(icmpPing)

		impl := makeNetstack(t, func(impl *Impl) {
			impl.ProcessSubnets = true
		})
		_, ok := impl.shouldHandlePing(pkt)

		// We don't handle this because it's a Tailscale IP and not 4via6
		if ok {
			t.Errorf("expected shouldHandlePing==false")
		}
	})

	// Handle pings for 4via6 addresses regardless of ProcessSubnets
	for _, subnets := range []bool{true, false} {
		t.Run("ICMP6-4via6-ProcessSubnets-"+fmt.Sprint(subnets), func(t *testing.T) {
			// The 4via6 route 10.1.1.0/24 siteid 7, and then the IP
			// 10.1.1.9 within that route.
			dst := netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:7:a01:109")
			expectedPingDst := netip.MustParseAddr("10.1.1.9")
			icmph := packet.ICMP6Header{
				IP6Header: packet.IP6Header{
					IPProto: ipproto.ICMPv6,
					Src:     srcIP,
					Dst:     dst,
				},
				Type: packet.ICMP6EchoRequest,
				Code: packet.ICMP6NoCode,
			}
			_, payload := packet.ICMPEchoPayload(nil)
			icmpPing := packet.Generate(icmph, payload)
			pkt := &packet.Parsed{}
			pkt.Decode(icmpPing)

			impl := makeNetstack(t, func(impl *Impl) {
				impl.ProcessSubnets = subnets
			})
			pingDst, ok := impl.shouldHandlePing(pkt)

			// Handled due to being 4via6
			if !ok {
				t.Errorf("expected shouldHandlePing==true")
			} else if pingDst != expectedPingDst {
				t.Errorf("got dst %s; want %s", pingDst, expectedPingDst)
			}
		})
	}
}

// looksLikeATailscaleSelfAddress reports whether addr looks like
// a Tailscale self address, for tests.
func looksLikeATailscaleSelfAddress(addr netip.Addr) bool {
	return addr.Is4() && tsaddr.IsTailscaleIP(addr) ||
		addr.Is6() && tsaddr.Tailscale4To6Range().Contains(addr)
}

func TestShouldProcessInbound(t *testing.T) {
	testCases := []struct {
		name        string
		pkt         *packet.Parsed
		afterStart  func(*Impl) // optional; after Impl.Start is called
		beforeStart func(*Impl) // optional; before Impl.Start is called
		want        bool
		runOnGOOS   string
	}{
		{
			name: "ipv6-via",
			pkt: &packet.Parsed{
				IPVersion: 6,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),

				// $ tailscale debug via 7 10.1.1.9/24
				// fd7a:115c:a1e0:b1a:0:7:a01:109/120
				Dst:      netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
				TCPFlags: packet.TCPSyn,
			},
			afterStart: func(i *Impl) {
				prefs := ipn.NewPrefs()
				prefs.AdvertiseRoutes = []netip.Prefix{
					// $ tailscale debug via 7 10.1.1.0/24
					// fd7a:115c:a1e0:b1a:0:7:a01:100/120
					netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
				}
				i.lb.Start(ipn.Options{
					UpdatePrefs: prefs,
				})
				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			beforeStart: func(i *Impl) {
				// This should be handled even if we're
				// otherwise not processing local IPs or
				// subnets.
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			want: true,
		},
		{
			name: "ipv6-via-not-advertised",
			pkt: &packet.Parsed{
				IPVersion: 6,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),

				// $ tailscale debug via 7 10.1.1.9/24
				// fd7a:115c:a1e0:b1a:0:7:a01:109/120
				Dst:      netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
				TCPFlags: packet.TCPSyn,
			},
			afterStart: func(i *Impl) {
				prefs := ipn.NewPrefs()
				prefs.AdvertiseRoutes = []netip.Prefix{
					// tailscale debug via 7 10.1.2.0/24
					// fd7a:115c:a1e0:b1a:0:7:a01:200/120
					netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:200/120"),
				}
				i.lb.Start(ipn.Options{
					UpdatePrefs: prefs,
				})
			},
			want: false,
		},
		{
			name: "tailscale-ssh-enabled",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.101.102.104:22"),
				TCPFlags:  packet.TCPSyn,
			},
			afterStart: func(i *Impl) {
				prefs := ipn.NewPrefs()
				prefs.RunSSH = true
				i.lb.Start(ipn.Options{
					UpdatePrefs: prefs,
				})
				i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
					return addr.String() == "100.101.102.104" // Dst, above
				})
			},
			want:      true,
			runOnGOOS: "linux",
		},
		{
			name: "tailscale-ssh-disabled",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.101.102.104:22"),
				TCPFlags:  packet.TCPSyn,
			},
			afterStart: func(i *Impl) {
				prefs := ipn.NewPrefs()
				prefs.RunSSH = false // default, but to be explicit
				i.lb.Start(ipn.Options{
					UpdatePrefs: prefs,
				})
				i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
					return addr.String() == "100.101.102.104" // Dst, above
				})
			},
			want: false,
		},
		{
			name: "process-local-ips",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.101.102.104:4567"),
				TCPFlags:  packet.TCPSyn,
			},
			afterStart: func(i *Impl) {
				i.ProcessLocalIPs = true
				i.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
					return addr.String() == "100.101.102.104" // Dst, above
				})
			},
			want: true,
		},
		{
			name: "process-subnets",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("10.1.2.3:4567"),
				TCPFlags:  packet.TCPSyn,
			},
			beforeStart: func(i *Impl) {
				i.ProcessSubnets = true
			},
			afterStart: func(i *Impl) {
				// For testing purposes, assume all Tailscale
				// IPs are local; the Dst above is something
				// not in that range.
				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: true,
		},
		{
			name: "peerapi-port-subnet-router", // see #6235
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("10.0.0.23:5555"),
				TCPFlags:  packet.TCPSyn,
			},
			beforeStart: func(i *Impl) {
				// As if we were running on Linux where netstack isn't used.
				i.ProcessSubnets = false
				i.atomicIsLocalIPFunc.Store(func(netip.Addr) bool { return false })
			},
			afterStart: func(i *Impl) {
				prefs := ipn.NewPrefs()
				prefs.AdvertiseRoutes = []netip.Prefix{
					netip.MustParsePrefix("10.0.0.1/24"),
				}
				i.lb.Start(ipn.Options{
					UpdatePrefs: prefs,
				})

				// Set the PeerAPI port to the Dst port above.
				i.peerapiPort4Atomic.Store(5555)
				i.peerapiPort6Atomic.Store(5555)
			},
			want: false,
		},
		{
			name: "udp-on-service-vip-with-listener-ipv4",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.UDP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.100.100.100:8080"),
			},
			beforeStart: func(i *Impl) {
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			afterStart: func(i *Impl) {
				IPServiceMap := netmap.IPServiceMappings{
					serviceIP: "svc:test-service",
				}
				i.lb.ForTest().SetIPServiceMappings(IPServiceMap)

				i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceIP
				})

				// Register the service VIP address on the NIC so gVisor can route to it
				protocolAddr := tcpip.ProtocolAddress{
					Protocol:          header.IPv4ProtocolNumber,
					AddressWithPrefix: tcpip.AddrFrom4(serviceIP.As4()).WithPrefix(),
				}

				if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
					t.Fatalf("AddProtocolAddress: %v", err)
				}

				// Create a UDP listener on the service VIP
				pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
					NIC:  nicID,
					Addr: tcpip.AddrFrom4(serviceIP.As4()),
					Port: 8080,
				}, nil, header.IPv4ProtocolNumber)
				if err != nil {
					t.Fatalf("DialUDP: %v", err)
				}
				t.Cleanup(func() { pc.Close() })

				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: true,
		},
		{
			name: "udp-on-service-vip-no-listener-ipv4",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.UDP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.100.100.100:9999"),
			},
			beforeStart: func(i *Impl) {
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			afterStart: func(i *Impl) {
				IPServiceMap := netmap.IPServiceMappings{
					serviceIP: "svc:test-service",
				}
				i.lb.ForTest().SetIPServiceMappings(IPServiceMap)

				i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceIP
				})

				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: false,
		},
		{
			name: "udp-on-service-vip-with-listener-ipv6",
			pkt: &packet.Parsed{
				IPVersion: 6,
				IPProto:   ipproto.UDP,
				Src:       netip.MustParseAddrPort("[fd7a:115c:a1e0::1]:1234"),
				Dst:       netip.MustParseAddrPort("[fd7a:115c:a1e0::53]:8080"),
			},
			beforeStart: func(i *Impl) {
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			afterStart: func(i *Impl) {
				IPServiceMap := netmap.IPServiceMappings{
					serviceIPv6: "svc:test-service",
				}
				i.lb.ForTest().SetIPServiceMappings(IPServiceMap)

				i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceIPv6
				})

				protocolAddr := tcpip.ProtocolAddress{
					Protocol:          header.IPv6ProtocolNumber,
					AddressWithPrefix: tcpip.AddrFrom16(serviceIPv6.As16()).WithPrefix(),
				}
				if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
					t.Fatalf("AddProtocolAddress: %v", err)
				}

				pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
					NIC:  nicID,
					Addr: tcpip.AddrFrom16(serviceIPv6.As16()),
					Port: 8080,
				}, nil, header.IPv6ProtocolNumber)
				if err != nil {
					t.Fatalf("DialUDP: %v", err)
				}
				t.Cleanup(func() { pc.Close() })

				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: true,
		},
		{
			name: "udp-on-service-vip-no-listener-ipv6",
			pkt: &packet.Parsed{
				IPVersion: 6,
				IPProto:   ipproto.UDP,
				Src:       netip.MustParseAddrPort("[fd7a:115c:a1e0::1]:1234"),
				Dst:       netip.AddrPortFrom(serviceIPv6, 9999),
			},
			beforeStart: func(i *Impl) {
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			afterStart: func(i *Impl) {
				IPServiceMap := netmap.IPServiceMappings{
					serviceIPv6: "svc:test-service",
				}
				i.lb.ForTest().SetIPServiceMappings(IPServiceMap)

				i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceIPv6
				})

				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: false,
		},
		{
			name: "tcp-on-service-vip-with-udp-listener",
			pkt: &packet.Parsed{
				IPVersion: 4,
				IPProto:   ipproto.TCP,
				Src:       netip.MustParseAddrPort("100.101.102.103:1234"),
				Dst:       netip.MustParseAddrPort("100.100.100.100:8080"), // serviceIP
				TCPFlags:  packet.TCPSyn,
			},
			beforeStart: func(i *Impl) {
				i.ProcessLocalIPs = false
				i.ProcessSubnets = false
			},
			afterStart: func(i *Impl) {
				IPServiceMap := netmap.IPServiceMappings{
					serviceIP: "svc:test-service",
				}
				i.lb.ForTest().SetIPServiceMappings(IPServiceMap)

				i.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceIP
				})

				protocolAddr := tcpip.ProtocolAddress{
					Protocol:          header.IPv4ProtocolNumber,
					AddressWithPrefix: tcpip.AddrFrom4(serviceIP.As4()).WithPrefix(),
				}
				if err := i.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
					t.Fatalf("AddProtocolAddress: %v", err)
				}

				pc, err := gonet.DialUDP(i.ipstack, &tcpip.FullAddress{
					NIC:  nicID,
					Addr: tcpip.AddrFrom4(serviceIP.As4()),
					Port: 8080,
				}, nil, header.IPv4ProtocolNumber)
				if err != nil {
					t.Fatalf("DialUDP: %v", err)
				}
				t.Cleanup(func() { pc.Close() })

				i.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			want: false,
		},

		// TODO(andrew): test PeerAPI
		// TODO(andrew): test TCP packets without the SYN flag set
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			if tc.runOnGOOS != "" && runtime.GOOS != tc.runOnGOOS {
				t.Skipf("skipping on GOOS=%v", runtime.GOOS)
			}
			impl := makeNetstack(t, tc.beforeStart)
			if tc.afterStart != nil {
				tc.afterStart(impl)
			}

			got := impl.shouldProcessInbound(tc.pkt, nil)
			if got != tc.want {
				t.Errorf("got shouldProcessInbound()=%v; want %v", got, tc.want)
			} else {
				t.Logf("OK: shouldProcessInbound() = %v", got)
			}
		})
	}
}

func tcp4syn(tb testing.TB, src, dst netip.Addr, sport, dport uint16) []byte {
	ip := header.IPv4(make([]byte, header.IPv4MinimumSize+header.TCPMinimumSize))
	ip.Encode(&header.IPv4Fields{
		Protocol:    uint8(header.TCPProtocolNumber),
		TotalLength: header.IPv4MinimumSize + header.TCPMinimumSize,
		TTL:         64,
		SrcAddr:     tcpip.AddrFrom4Slice(src.AsSlice()),
		DstAddr:     tcpip.AddrFrom4Slice(dst.AsSlice()),
	})
	ip.SetChecksum(^ip.CalculateChecksum())
	if !ip.IsChecksumValid() {
		tb.Fatal("test broken; packet has incorrect IP checksum")
	}

	tcp := header.TCP(ip[header.IPv4MinimumSize:])
	tcp.Encode(&header.TCPFields{
		SrcPort:    sport,
		DstPort:    dport,
		SeqNum:     0,
		DataOffset: header.TCPMinimumSize,
		Flags:      header.TCPFlagSyn,
		WindowSize: 65535,
		Checksum:   0,
	})
	xsum := header.PseudoHeaderChecksum(
		header.TCPProtocolNumber,
		tcpip.AddrFrom4Slice(src.AsSlice()),
		tcpip.AddrFrom4Slice(dst.AsSlice()),
		uint16(header.TCPMinimumSize),
	)
	tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
	if !tcp.IsChecksumValid(tcpip.AddrFrom4Slice(src.AsSlice()), tcpip.AddrFrom4Slice(dst.AsSlice()), 0, 0) {
		tb.Fatal("test broken; packet has incorrect TCP checksum")
	}

	return ip
}

// makeHangDialer returns a dialer that notifies the returned channel when a
// connection is dialed and then hangs until the test finishes.
func makeHangDialer(tb testing.TB) (netx.DialFunc, chan struct{}) {
	done := make(chan struct{})
	tb.Cleanup(func() {
		close(done)
	})

	gotConn := make(chan struct{}, 1)
	fn := func(ctx context.Context, network, address string) (net.Conn, error) {
		// Signal that we have a new connection
		tb.Logf("hangDialer: called with network=%q address=%q", network, address)
		select {
		case gotConn <- struct{}{}:
		default:
		}

		// Hang until the test is done.
		select {
		case <-ctx.Done():
			tb.Logf("context done")
		case <-done:
			tb.Logf("function completed")
		}
		return nil, fmt.Errorf("canceled")
	}
	return fn, gotConn
}

// TestTCPForwardLimits verifies that the limits on the TCP forwarder work in a
// success case (i.e. when we don't hit the limit).
func TestTCPForwardLimits(t *testing.T) {
	tstest.AssertNotParallel(t) // calls envknob.Setenv
	envknob.Setenv("TS_DEBUG_NETSTACK", "true")
	t.Cleanup(func() { envknob.Setenv("TS_DEBUG_NETSTACK", "") })

	impl := makeNetstack(t, func(impl *Impl) {
		impl.ProcessSubnets = true
	})

	dialFn, gotConn := makeHangDialer(t)
	impl.forwardDialFunc = dialFn

	prefs := ipn.NewPrefs()
	prefs.AdvertiseRoutes = []netip.Prefix{
		// This is the TEST-NET-1 IP block for use in documentation,
		// and should never actually be routable.
		netip.MustParsePrefix("192.0.2.0/24"),
	}
	impl.lb.Start(ipn.Options{
		UpdatePrefs: prefs,
	})
	impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)

	// Inject an "outbound" packet that's going to an IP address that times
	// out. We need to re-parse from a byte slice so that the internal
	// buffer in the packet.Parsed type is filled out.
	client := netip.MustParseAddr("100.101.102.103")
	destAddr := netip.MustParseAddr("192.0.2.1")
	pkt := tcp4syn(t, client, destAddr, 1234, 4567)
	var parsed packet.Parsed
	parsed.Decode(pkt)

	// When injecting this packet, we want the outcome to be "drop
	// silently", which indicates that netstack is processing the
	// packet and not delivering it to the host system.
	if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
		t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Wait until we have an in-flight outgoing connection.
	select {
	case <-ctx.Done():
		t.Fatalf("timed out waiting for connection")
	case <-gotConn:
		t.Logf("got connection in progress")
	}

	// Inject another packet, which will be deduplicated and thus not
	// increment our counter.
	parsed.Decode(pkt)
	if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
		t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
	}

	// Verify that we now have a single in-flight address in our map.
	impl.mu.Lock()
	inFlight := maps.Clone(impl.connsInFlightByClient)
	impl.mu.Unlock()

	if got, ok := inFlight[client]; !ok || got != 1 {
		t.Errorf("expected 1 in-flight connection for %v, got: %v", client, inFlight)
	}

	// Get the expvar statistics and verify that we're exporting the
	// correct metric.
	metrics := impl.ExpVar().(*metrics.Set)

	const metricName = "gauge_tcp_forward_in_flight"
	if v := metrics.Get(metricName).String(); v != "1" {
		t.Errorf("got metric %q=%s, want 1", metricName, v)
	}
}

// TestTCPForwardLimits_PerClient verifies that the per-client limit for TCP
// forwarding works.
func TestTCPForwardLimits_PerClient(t *testing.T) {
	clientmetric.ResetForTest(t)
	tstest.AssertNotParallel(t) // calls envknob.Setenv
	envknob.Setenv("TS_DEBUG_NETSTACK", "true")
	t.Cleanup(func() { envknob.Setenv("TS_DEBUG_NETSTACK", "") })

	// Set our test override limits during this test.
	maxInFlightConnectionAttemptsForTest.Store(2)
	t.Cleanup(func() { maxInFlightConnectionAttemptsForTest.Store(0) })
	maxInFlightConnectionAttemptsPerClientForTest.Store(1)
	t.Cleanup(func() { maxInFlightConnectionAttemptsPerClientForTest.Store(0) })

	impl := makeNetstack(t, func(impl *Impl) {
		impl.ProcessSubnets = true
	})

	dialFn, gotConn := makeHangDialer(t)
	impl.forwardDialFunc = dialFn

	prefs := ipn.NewPrefs()
	prefs.AdvertiseRoutes = []netip.Prefix{
		// This is the TEST-NET-1 IP block for use in documentation,
		// and should never actually be routable.
		netip.MustParsePrefix("192.0.2.0/24"),
	}
	impl.lb.Start(ipn.Options{
		UpdatePrefs: prefs,
	})
	impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)

	// Inject an "outbound" packet that's going to an IP address that times
	// out. We need to re-parse from a byte slice so that the internal
	// buffer in the packet.Parsed type is filled out.
	client := netip.MustParseAddr("100.101.102.103")
	destAddr := netip.MustParseAddr("192.0.2.1")

	// Helpers
	var port uint16 = 1234
	mustInjectPacket := func() {
		pkt := tcp4syn(t, client, destAddr, port, 4567)
		port++ // to avoid deduplication based on endpoint

		var parsed packet.Parsed
		parsed.Decode(pkt)

		// When injecting this packet, we want the outcome to be "drop
		// silently", which indicates that netstack is processing the
		// packet and not delivering it to the host system.
		if resp, _ := impl.injectInbound(&parsed, impl.tundev, nil); resp != filter.DropSilently {
			t.Fatalf("got filter outcome %v, want filter.DropSilently", resp)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	waitPacket := func() {
		select {
		case <-ctx.Done():
			t.Fatalf("timed out waiting for connection")
		case <-gotConn:
			t.Logf("got connection in progress")
		}
	}

	// Inject the packet to start the TCP forward and wait until we have an
	// in-flight outgoing connection.
	mustInjectPacket()
	waitPacket()

	// Verify that we now have a single in-flight address in our map.
	impl.mu.Lock()
	inFlight := maps.Clone(impl.connsInFlightByClient)
	impl.mu.Unlock()

	if got, ok := inFlight[client]; !ok || got != 1 {
		t.Errorf("expected 1 in-flight connection for %v, got: %v", client, inFlight)
	}

	metrics := impl.ExpVar().(*metrics.Set)

	// One client should have reached the limit at this point.
	if v := metrics.Get("gauge_tcp_forward_in_flight_per_client_limit_reached").String(); v != "1" {
		t.Errorf("got limit reached expvar metric=%s, want 1", v)
	}

	// Inject another packet, and verify that we've incremented our
	// "dropped" metrics since this will have been dropped.
	mustInjectPacket()

	// expvar metric
	const metricName = "counter_tcp_forward_max_in_flight_per_client_drop"
	if v := metrics.Get(metricName).String(); v != "1" {
		t.Errorf("got expvar metric %q=%s, want 1", metricName, v)
	}

	// client metric
	if v := metricPerClientForwardLimit.Value(); v != 1 {
		t.Errorf("got clientmetric limit metric=%d, want 1", v)
	}
}

// TestHandleLocalPackets tests the handleLocalPackets function, ensuring that
// we are properly deciding to handle packets that are destined for "local"
// IPs–addresses that are either for this node, or that it is responsible for.
//
// See, e.g. #11304
func TestHandleLocalPackets(t *testing.T) {
	var (
		selfIP4 = netip.MustParseAddr("100.64.1.2")
		selfIP6 = netip.MustParseAddr("fd7a:115c:a1e0::123")
	)

	impl := makeNetstack(t, func(impl *Impl) {
		impl.ProcessSubnets = false
		impl.ProcessLocalIPs = false
		impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
			return addr == selfIP4 || addr == selfIP6
		})
	})

	prefs := ipn.NewPrefs()
	prefs.AdvertiseRoutes = []netip.Prefix{
		// $ tailscale debug via 7 10.1.1.0/24
		// fd7a:115c:a1e0:b1a:0:7:a01:100/120
		netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
	}
	prefs.AdvertiseServices = []string{"svc:test-service"}
	_, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
		Prefs:                *prefs,
		AdvertiseRoutesSet:   true,
		AdvertiseServicesSet: true,
	})
	if err != nil {
		t.Fatalf("EditPrefs: %v", err)
	}
	IPServiceMap := netmap.IPServiceMappings{
		netip.MustParseAddr("100.99.55.111"):        "svc:test-service",
		netip.MustParseAddr("fd7a:115c:a1e0::abcd"): "svc:test-service",
	}
	impl.lb.ForTest().SetIPServiceMappings(IPServiceMap)

	t.Run("ShouldHandleServiceIP", func(t *testing.T) {
		t.Parallel()
		pkt := &packet.Parsed{
			IPVersion: 4,
			IPProto:   ipproto.TCP,
			Src:       netip.MustParseAddrPort("127.0.0.1:9999"),
			Dst:       netip.MustParseAddrPort("100.100.100.100:53"),
			TCPFlags:  packet.TCPSyn,
		}
		resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
		if resp != filter.DropSilently {
			t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
		}
	})
	// Any port on the quad-100 service IP must be absorbed locally by
	// netstack and never leak out to the WireGuard / peer-routing
	// layers. Historically we only intercepted specific ports (UDP 53
	// and TCP 53/80/8080), causing stray traffic to other ports such
	// as 100.100.100.100:853 (DoT) to time out in wireguard-go and
	// produce "open-conn-track: timeout opening ...; no associated
	// peer node" log spam. See the handleLocalPackets comment.
	quad100LeakCases := []struct {
		name  string
		proto ipproto.Proto
		dst   string
	}{
		{"TCP-853-DoT-v4", ipproto.TCP, "100.100.100.100:853"},
		{"TCP-443-DoH-v4", ipproto.TCP, "100.100.100.100:443"},
		{"TCP-9000-stray-v4", ipproto.TCP, "100.100.100.100:9000"},
		{"UDP-853-DoQ-v4", ipproto.UDP, "100.100.100.100:853"},
		{"UDP-443-v4", ipproto.UDP, "100.100.100.100:443"},
		{"TCP-853-DoT-v6", ipproto.TCP, "[fd7a:115c:a1e0::53]:853"},
		{"UDP-443-v6", ipproto.UDP, "[fd7a:115c:a1e0::53]:443"},
	}
	for _, tc := range quad100LeakCases {
		t.Run("ShouldNotLeakQuad100_"+tc.name, func(t *testing.T) {
			t.Parallel()
			dst := netip.MustParseAddrPort(tc.dst)
			ipVersion := uint8(4)
			if dst.Addr().Is6() {
				ipVersion = 6
			}
			src := "127.0.0.1:9999"
			if ipVersion == 6 {
				src = "[::1]:9999"
			}
			pkt := &packet.Parsed{
				IPVersion: ipVersion,
				IPProto:   tc.proto,
				Src:       netip.MustParseAddrPort(src),
				Dst:       dst,
			}
			if tc.proto == ipproto.TCP {
				pkt.TCPFlags = packet.TCPSyn
			}
			resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
			if resp != filter.DropSilently {
				t.Errorf("quad-100 %s packet leaked: got filter outcome %v, want filter.DropSilently", tc.name, resp)
			}
		})
	}
	// Exhaustive sweep of all ports for both transport protocols and
	// both IP versions, confirming no port leaks. The quad-100 branch
	// of handleLocalPackets is port-independent by construction; this
	// test serves as a regression guard against accidental port-based
	// exemptions slipping back in.
	t.Run("ShouldNotLeakQuad100_AllPorts", func(t *testing.T) {
		t.Parallel()
		protos := []ipproto.Proto{ipproto.TCP, ipproto.UDP}
		dsts := []netip.Addr{
			netip.MustParseAddr("100.100.100.100"),
			netip.MustParseAddr("fd7a:115c:a1e0::53"),
		}
		for _, proto := range protos {
			for _, dstAddr := range dsts {
				ipVersion := uint8(4)
				srcStr := "127.0.0.1:9999"
				if dstAddr.Is6() {
					ipVersion = 6
					srcStr = "[::1]:9999"
				}
				src := netip.MustParseAddrPort(srcStr)
				for port := 1; port <= 65535; port++ {
					pkt := &packet.Parsed{
						IPVersion: ipVersion,
						IPProto:   proto,
						Src:       src,
						Dst:       netip.AddrPortFrom(dstAddr, uint16(port)),
					}
					if proto == ipproto.TCP {
						pkt.TCPFlags = packet.TCPSyn
					}
					resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
					if resp != filter.DropSilently {
						t.Fatalf("port=%d proto=%v dst=%v: got %v, want filter.DropSilently", port, proto, dstAddr, resp)
					}
				}
			}
		}
	})
	t.Run("ShouldHandle4via6", func(t *testing.T) {
		t.Parallel()
		pkt := &packet.Parsed{
			IPVersion: 6,
			IPProto:   ipproto.TCP,
			Src:       netip.MustParseAddrPort("[::1]:1234"),

			// This is an IP in the above 4via6 subnet that this node handles.
			//    $ tailscale debug via 7 10.1.1.9/24
			//    fd7a:115c:a1e0:b1a:0:7:a01:109/120
			Dst:      netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:109]:5678"),
			TCPFlags: packet.TCPSyn,
		}
		resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)

		// DropSilently is the outcome we expected, since we actually
		// handled this packet by injecting it into netstack, which
		// will handle creating the TCP forwarder. We drop it so we
		// don't process the packet outside of netstack.
		if resp != filter.DropSilently {
			t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
		}
	})
	t.Run("ShouldHandleLocalTailscaleServices", func(t *testing.T) {
		t.Parallel()
		pkt := &packet.Parsed{
			IPVersion: 4,
			IPProto:   ipproto.TCP,
			Src:       netip.MustParseAddrPort("127.0.0.1:9999"),
			Dst:       netip.MustParseAddrPort("100.99.55.111:80"),
			TCPFlags:  packet.TCPSyn,
		}
		resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
		if resp != filter.DropSilently {
			t.Errorf("got filter outcome %v, want filter.DropSilently", resp)
		}
	})
	t.Run("ShouldNotHandleInactiveVIPService", func(t *testing.T) {
		// Tests that packets to Tailscale Services we don't host are accepted.
		inactiveVIP := netip.MustParseAddr("100.99.55.222")
		impl.lb.ForTest().SetIPServiceMappings(netmap.IPServiceMappings{
			netip.MustParseAddr("100.99.55.111"):        "svc:test-service", // active (shared fixture)
			netip.MustParseAddr("fd7a:115c:a1e0::abcd"): "svc:test-service",
			inactiveVIP: "svc:inactive-elsewhere",
		})
		t.Cleanup(func() {
			// Restore the shared fixture for any later/parallel subtests.
			impl.lb.ForTest().SetIPServiceMappings(IPServiceMap)
		})
		pkt := &packet.Parsed{
			IPVersion: 4,
			IPProto:   ipproto.TCP,
			Src:       netip.MustParseAddrPort("127.0.0.1:9999"),
			Dst:       netip.AddrPortFrom(inactiveVIP, 80),
			TCPFlags:  packet.TCPSyn,
		}
		resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)
		if resp != filter.Accept {
			t.Errorf("inactive VIP service: got filter outcome %v, want filter.Accept (pass through to host)", resp)
		}
	})
	t.Run("OtherNonHandled", func(t *testing.T) {
		t.Parallel()
		pkt := &packet.Parsed{
			IPVersion: 6,
			IPProto:   ipproto.TCP,
			Src:       netip.MustParseAddrPort("[::1]:1234"),

			// This IP is *not* in the above 4via6 route
			//    $ tailscale debug via 99 10.1.1.9/24
			//    fd7a:115c:a1e0:b1a:0:63:a01:109/120
			Dst:      netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:63:a01:109]:5678"),
			TCPFlags: packet.TCPSyn,
		}
		resp, _ := impl.handleLocalPackets(pkt, impl.tundev, nil)

		// Accept means that handleLocalPackets does not handle this
		// packet, we "accept" it to continue further processing,
		// instead of dropping because it was already handled.
		if resp != filter.Accept {
			t.Errorf("got filter outcome %v, want filter.Accept", resp)
		}
	})
}

// TestAcceptTCPRoutingTailscaleIPRange tests how acceptTCP behaves for TCP SYN
// packets destined to IPs in the Tailscale range (100.64.0.0/10).
//
//   - Packets to the Tailscale IP should be forwarded to loopback if there is
//     no configured handler for the port.
//   - Packets to the service IP (100.100.100.100) on non-served ports should
//     never make it to the local host.
//   - Packets to a Tailscale Service VIP on non-served ports should never make
//     it to the local host.
func TestAcceptTCPLoopbackForwardVsRST(t *testing.T) {
	serviceVIP := netip.MustParseAddr("100.90.1.2")
	selfIP := netip.MustParseAddr("100.64.1.2")
	const serviceName = "svc:test"

	cases := []struct {
		name string
		// configure runs inside makeNetstack, before Start.
		configure func(*Impl)
		// afterStart runs after Start, for state that requires the backend to be
		// running (prefs, service IP maps, NIC address registration). Optional.
		afterStart func(t *testing.T, impl *Impl)
		// dst is the SYN's destination. Its port has no registered handler.
		dst netip.AddrPort
		// inbound selects the injection path: true => injectInbound (a packet
		// from a peer), false => handleLocalPackets (host-originated, or the
		// kernel-hairpin re-entry).
		inbound bool
		// wantForward is whether acceptTCP should forward the connection to the
		// host's loopback (true) or reject it with a RST (false).
		wantForward bool
	}{
		{
			name: "Quad-100UnservedPortIsRST",
			configure: func(impl *Impl) {
				impl.ProcessSubnets = false
				impl.ProcessLocalIPs = false
				impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
			},
			// 853 is DoT, the specific case called out in the original bug
			// report ("conntrack error no peer found for 100.100.100.100:853").
			// Before the fix, port 853 (and any non-{53,80,8080} port) leaked
			// out to WireGuard; after the fix it is absorbed and must NOT
			// trigger forwardTCP. handleLocalPackets absorbs all quad-100
			// traffic regardless of port to prevent it leaking to WireGuard
			// peers (which produced noisy "open-conn-track: timeout opening ...;
			// no associated peer node" log lines), leaving acceptTCP to reject
			// the unserved port with a RST rather than falling through to the
			// isTailscaleIP loopback rewrite.
			dst:         netip.AddrPortFrom(tsaddr.TailscaleServiceIP(), 853),
			wantForward: false,
		},
		{
			name: "VIPServiceUnservedPortIsRST",
			configure: func(impl *Impl) {
				impl.ProcessSubnets = false
				impl.ProcessLocalIPs = false
				impl.atomicIsLocalIPFunc.Store(looksLikeATailscaleSelfAddress)
				impl.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
					return addr == serviceVIP
				})
			},
			afterStart: func(t *testing.T, impl *Impl) {
				// Mark the service as one this node hosts and is actively
				// serving, so handleLocalPackets absorbs its traffic into
				// netstack (rather than letting a non-hosted VIP route through).
				// The service serves no TCP ports here, so every port is
				// "non-served". AdvertiseServices flows through to
				// UpdateActiveVIPServices, marking the service active.
				prefs := ipn.NewPrefs()
				prefs.AdvertiseServices = []string{serviceName}
				if _, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
					Prefs:                *prefs,
					AdvertiseServicesSet: true,
				}); err != nil {
					t.Fatalf("EditPrefs: %v", err)
				}
				impl.lb.ForTest().SetIPServiceMappings(netmap.IPServiceMappings{serviceVIP: serviceName})
			},
			dst:         netip.AddrPortFrom(serviceVIP, 8001), // 8001: a port the service doesn't serve
			wantForward: false,
		},
		{
			name: "LocalTailscaleIPUnhandledPortForwardsToLoopback",
			configure: func(impl *Impl) {
				impl.ProcessSubnets = false
				// ProcessLocalIPs=true so an inbound packet to a local Tailscale
				// IP is absorbed into netstack and dispatched to acceptTCP.
				impl.ProcessLocalIPs = true
				impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
					return addr == selfIP
				})
			},
			// 9999 has no SSH/webclient/peerapi/serve handler, so
			// TCPHandlerForDst returns nil and acceptTCP falls to the
			// isTailscaleIP case, which rewrites the dial to 127.0.0.1:9999 and
			// calls forwardTCP. This is how local handlers reach the host, and
			// the RST guards above must not swallow it.
			dst:         netip.AddrPortFrom(selfIP, 9999),
			inbound:     true,
			wantForward: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			impl := makeNetstack(t, tc.configure)
			if tc.afterStart != nil {
				tc.afterStart(t, impl)
			}

			dialFn, gotConn := makeHangDialer(t)
			impl.forwardDialFunc = dialFn

			// Use a client IP in the CGNAT range so shouldProcessInbound-adjacent
			// code paths treat this as plausibly-peer-sourced traffic, matching
			// what a real stray probe from a peer or the host OS would look like.
			client := netip.MustParseAddr("100.101.102.103")
			pkt := tcp4syn(t, client, tc.dst.Addr(), 1234, tc.dst.Port())
			var parsed packet.Parsed
			parsed.Decode(pkt)

			// Both injection paths absorb the packet into netstack, returning
			// filter.DropSilently (i.e. not handing it to the host); acceptTCP
			// is then dispatched with the packet.
			inject := impl.handleLocalPackets
			if tc.inbound {
				inject = impl.injectInbound
			}
			if resp, _ := inject(&parsed, impl.tundev, nil); resp != filter.DropSilently {
				t.Fatalf("inject for %v: got filter outcome %v, want filter.DropSilently", tc.dst, resp)
			}

			// acceptTCP runs asynchronously in the gVisor TCP dispatcher after
			// handleLocalPackets injects the packet into netstack. Use the
			// in-flight connection counter as a deterministic synchronization
			// point: wrapTCPProtocolHandler increments connsInFlightByClient
			// when the dispatcher hands the connection off to acceptTCP, and
			// acceptTCP's deferred decrementInFlightTCPForward decrements it
			// on return.
			//
			// On the green path (RST guard fires), acceptTCP returns promptly
			// and the counter reaches 0. On the red path (fall-through to
			// forwardTCP), acceptTCP blocks inside the forwardDialFunc call —
			// makeHangDialer signals gotConn on entry (buffered, non-blocking)
			// and then blocks forever — so the counter never reaches 0 but
			// gotConn fires synchronously from the dispatcher goroutine. A
			// select on both races those outcomes without real-time padding.
			//
			// testing/synctest is not usable here: gVisor's sleep package calls
			// the runtime's gopark directly rather than via the standard
			// library, so synctest.Wait() cannot observe those goroutines
			// becoming durably blocked and hangs indefinitely.
			inFlightZero := make(chan struct{})
			go func() {
				for {
					impl.mu.Lock()
					n := impl.connsInFlightByClient[client]
					impl.mu.Unlock()
					if n == 0 {
						close(inFlightZero)
						return
					}
					time.Sleep(time.Millisecond)
				}
			}()

			select {
			case <-gotConn:
				if !tc.wantForward {
					t.Fatalf("forwardDialFunc was called for %v; acceptTCP forwarded to the host's loopback instead of sending a RST", tc.dst)
				}
			case <-inFlightZero:
				if tc.wantForward {
					t.Fatalf("forwardDialFunc was NOT called for %v; acceptTCP rejected the connection instead of forwarding it to loopback", tc.dst)
				}
			case <-time.After(5 * time.Second):
				// Safety net so a regression in the in-flight counter plumbing
				// doesn't hang the whole test run; both outcomes above should
				// fire within milliseconds in practice.
				t.Fatalf("timed out waiting for acceptTCP to dispatch %v SYN", tc.dst)
			}
		})
	}
}

func TestShouldSendToHost(t *testing.T) {
	var (
		selfIP4             = netip.MustParseAddr("100.64.1.2")
		selfIP6             = netip.MustParseAddr("fd7a:115c:a1e0::123")
		tailscaleServiceIP4 = netip.MustParseAddr("100.99.55.111")
		tailscaleServiceIP6 = netip.MustParseAddr("fd7a:115c:a1e0::abcd")
	)

	makeTestNetstack := func(tb testing.TB) *Impl {
		impl := makeNetstack(tb, func(impl *Impl) {
			impl.ProcessSubnets = false
			impl.ProcessLocalIPs = false
			impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
				return addr == selfIP4 || addr == selfIP6
			})
			impl.atomicIsVIPServiceIPFunc.Store(func(addr netip.Addr) bool {
				return addr == tailscaleServiceIP4 || addr == tailscaleServiceIP6
			})
		})

		prefs := ipn.NewPrefs()
		prefs.AdvertiseRoutes = []netip.Prefix{
			// $ tailscale debug via 7 10.1.1.0/24
			// fd7a:115c:a1e0:b1a:0:7:a01:100/120
			netip.MustParsePrefix("fd7a:115c:a1e0:b1a:0:7:a01:100/120"),
		}
		_, err := impl.lb.EditPrefs(&ipn.MaskedPrefs{
			Prefs:              *prefs,
			AdvertiseRoutesSet: true,
		})
		if err != nil {
			tb.Fatalf("EditPrefs: %v", err)
		}
		return impl
	}

	testCases := []struct {
		name     string
		src, dst netip.AddrPort
		want     bool
	}{
		// Reply from service IP to localhost should be sent to host,
		// not over WireGuard.
		{
			name: "from_service_ip_to_localhost",
			src:  netip.AddrPortFrom(serviceIP, 53),
			dst:  netip.MustParseAddrPort("127.0.0.1:9999"),
			want: true,
		},
		{
			name: "from_service_ip_to_localhost_v6",
			src:  netip.AddrPortFrom(serviceIPv6, 53),
			dst:  netip.MustParseAddrPort("[::1]:9999"),
			want: true,
		},
		// A reply from the local IP to a remote host isn't sent to the
		// host, but rather over WireGuard.
		{
			name: "local_ip_to_remote",
			src:  netip.AddrPortFrom(selfIP4, 12345),
			dst:  netip.MustParseAddrPort("100.64.99.88:7777"),
			want: false,
		},
		{
			name: "local_ip_to_remote_v6",
			src:  netip.AddrPortFrom(selfIP6, 12345),
			dst:  netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
			want: false,
		},
		// A reply from a 4via6 address to a remote host isn't sent to
		// the local host, but rather over WireGuard. See:
		//     https://github.com/tailscale/tailscale/issues/12448
		{
			name: "4via6_to_remote",

			// $ tailscale debug via 7 10.1.1.99/24
			// fd7a:115c:a1e0:b1a:0:7:a01:163/120
			src:  netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:163]:12345"),
			dst:  netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
			want: false,
		},
		// However, a reply from a 4via6 address to the local Tailscale
		// IP for this host *is* sent to the local host. See:
		//     https://github.com/tailscale/tailscale/issues/11304
		{
			name: "4via6_to_local",

			// $ tailscale debug via 7 10.1.1.99/24
			// fd7a:115c:a1e0:b1a:0:7:a01:163/120
			src:  netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:7:a01:163]:12345"),
			dst:  netip.AddrPortFrom(selfIP6, 7777),
			want: true,
		},
		// Traffic from a 4via6 address that we're not handling to
		// either the local Tailscale IP or a remote host is sent
		// outbound.
		//
		// In most cases, we won't see this type of traffic in the
		// shouldSendToHost function, but let's confirm.
		{
			name: "other_4via6_to_local",

			// $ tailscale debug via 4444 10.1.1.88/24
			// fd7a:115c:a1e0:b1a:0:7:a01:163/120
			src:  netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:115c:a01:158]:12345"),
			dst:  netip.AddrPortFrom(selfIP6, 7777),
			want: false,
		},
		{
			name: "other_4via6_to_remote",

			// $ tailscale debug via 4444 10.1.1.88/24
			// fd7a:115c:a1e0:b1a:0:7:a01:163/120
			src:  netip.MustParseAddrPort("[fd7a:115c:a1e0:b1a:0:115c:a01:158]:12345"),
			dst:  netip.MustParseAddrPort("[fd7a:115:a1e0::99]:7777"),
			want: false,
		},
		// After accessing the Tailscale service from host, replies from Tailscale Service IPs
		// to the local Tailscale IPs should be sent to the host.
		{
			name: "from_service_ip_to_local_ip",
			src:  netip.AddrPortFrom(tailscaleServiceIP4, 80),
			dst:  netip.AddrPortFrom(selfIP4, 12345),
			want: true,
		},
		{
			name: "from_service_ip_to_local_ip_v6",
			src:  netip.AddrPortFrom(tailscaleServiceIP6, 80),
			dst:  netip.AddrPortFrom(selfIP6, 12345),
			want: true,
		},
		// Traffic from remote IPs to Tailscale Service IPs should be sent over WireGuard.
		{
			name: "from_service_ip_to_remote",
			src:  netip.AddrPortFrom(tailscaleServiceIP4, 80),
			dst:  netip.MustParseAddrPort("173.201.32.56:54321"),
			want: false,
		},
		{
			name: "from_service_ip_to_remote_v6",
			src:  netip.AddrPortFrom(tailscaleServiceIP6, 80),
			dst:  netip.MustParseAddrPort("[2001:4860:4860::8888]:54321"),
			want: false,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			var pkt *stack.PacketBuffer
			if tt.src.Addr().Is4() {
				pkt = makeUDP4PacketBuffer(tt.src, tt.dst)
			} else {
				pkt = makeUDP6PacketBuffer(tt.src, tt.dst)
			}

			ns := makeTestNetstack(t)
			if got := ns.shouldSendToHost(pkt); got != tt.want {
				t.Errorf("shouldSendToHost returned %v, want %v", got, tt.want)
			}
		})
	}
}

func makeUDP4PacketBuffer(src, dst netip.AddrPort) *stack.PacketBuffer {
	if !src.Addr().Is4() || !dst.Addr().Is4() {
		panic("src and dst must be IPv4")
	}

	data := []byte("hello world\n")

	packetLen := header.IPv4MinimumSize + header.UDPMinimumSize
	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		ReserveHeaderBytes: packetLen,
		Payload:            buffer.MakeWithData(data),
	})

	// Initialize the UDP header.
	udp := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
	pkt.TransportProtocolNumber = header.UDPProtocolNumber

	length := uint16(pkt.Size())
	udp.Encode(&header.UDPFields{
		SrcPort: src.Port(),
		DstPort: dst.Port(),
		Length:  length,
	})

	// Add IP header
	ipHdr := header.IPv4(pkt.NetworkHeader().Push(header.IPv4MinimumSize))
	pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber
	ipHdr.Encode(&header.IPv4Fields{
		TotalLength: uint16(packetLen),
		Protocol:    uint8(header.UDPProtocolNumber),
		SrcAddr:     tcpip.AddrFrom4(src.Addr().As4()),
		DstAddr:     tcpip.AddrFrom4(dst.Addr().As4()),
		Checksum:    0,
	})

	return pkt
}

func makeUDP6PacketBuffer(src, dst netip.AddrPort) *stack.PacketBuffer {
	if !src.Addr().Is6() || !dst.Addr().Is6() {
		panic("src and dst must be IPv6")
	}
	data := []byte("hello world\n")

	packetLen := header.IPv6MinimumSize + header.UDPMinimumSize
	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		ReserveHeaderBytes: packetLen,
		Payload:            buffer.MakeWithData(data),
	})

	srcAddr := tcpip.AddrFrom16(src.Addr().As16())
	dstAddr := tcpip.AddrFrom16(dst.Addr().As16())

	// Add IP header
	ipHdr := header.IPv6(pkt.NetworkHeader().Push(header.IPv6MinimumSize))
	pkt.NetworkProtocolNumber = header.IPv6ProtocolNumber
	ipHdr.Encode(&header.IPv6Fields{
		SrcAddr:           srcAddr,
		DstAddr:           dstAddr,
		PayloadLength:     uint16(header.UDPMinimumSize + len(data)),
		TransportProtocol: header.UDPProtocolNumber,
		HopLimit:          64,
	})

	// Initialize the UDP header.
	udp := header.UDP(pkt.TransportHeader().Push(header.UDPMinimumSize))
	pkt.TransportProtocolNumber = header.UDPProtocolNumber

	length := uint16(pkt.Size())
	udp.Encode(&header.UDPFields{
		SrcPort: src.Port(),
		DstPort: dst.Port(),
		Length:  length,
	})

	// Calculate the UDP pseudo-header checksum.
	xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, srcAddr, dstAddr, uint16(len(udp)))
	udp.SetChecksum(^udp.CalculateChecksum(xsum))

	return pkt
}

// TestIsSelfDst verifies that isSelfDst correctly identifies packets whose
// destination IP is a local Tailscale IP assigned to this node.
func TestIsSelfDst(t *testing.T) {
	var (
		selfIP4   = netip.MustParseAddr("100.64.1.2")
		selfIP6   = netip.MustParseAddr("fd7a:115c:a1e0::123")
		remoteIP4 = netip.MustParseAddr("100.64.99.88")
		remoteIP6 = netip.MustParseAddr("fd7a:115c:a1e0::99")
	)

	ns := makeNetstack(t, func(impl *Impl) {
		impl.ProcessLocalIPs = true
		impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
			return addr == selfIP4 || addr == selfIP6
		})
	})

	testCases := []struct {
		name     string
		src, dst netip.AddrPort
		want     bool
	}{
		{
			name: "self_to_self_v4",
			src:  netip.AddrPortFrom(selfIP4, 12345),
			dst:  netip.AddrPortFrom(selfIP4, 8081),
			want: true,
		},
		{
			name: "self_to_self_v6",
			src:  netip.AddrPortFrom(selfIP6, 12345),
			dst:  netip.AddrPortFrom(selfIP6, 8081),
			want: true,
		},
		{
			name: "remote_to_self_v4",
			src:  netip.AddrPortFrom(remoteIP4, 12345),
			dst:  netip.AddrPortFrom(selfIP4, 8081),
			want: true,
		},
		{
			name: "remote_to_self_v6",
			src:  netip.AddrPortFrom(remoteIP6, 12345),
			dst:  netip.AddrPortFrom(selfIP6, 8081),
			want: true,
		},
		{
			name: "self_to_remote_v4",
			src:  netip.AddrPortFrom(selfIP4, 12345),
			dst:  netip.AddrPortFrom(remoteIP4, 8081),
			want: false,
		},
		{
			name: "self_to_remote_v6",
			src:  netip.AddrPortFrom(selfIP6, 12345),
			dst:  netip.AddrPortFrom(remoteIP6, 8081),
			want: false,
		},
		{
			name: "remote_to_remote_v4",
			src:  netip.AddrPortFrom(remoteIP4, 12345),
			dst:  netip.MustParseAddrPort("100.64.77.66:7777"),
			want: false,
		},
		{
			name: "service_ip_to_self_v4",
			src:  netip.AddrPortFrom(serviceIP, 53),
			dst:  netip.AddrPortFrom(selfIP4, 9999),
			want: true,
		},
		{
			name: "service_ip_to_self_v6",
			src:  netip.AddrPortFrom(serviceIPv6, 53),
			dst:  netip.AddrPortFrom(selfIP6, 9999),
			want: true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			var pkt *stack.PacketBuffer
			if tt.src.Addr().Is4() {
				pkt = makeUDP4PacketBuffer(tt.src, tt.dst)
			} else {
				pkt = makeUDP6PacketBuffer(tt.src, tt.dst)
			}
			defer pkt.DecRef()

			if got := ns.isSelfDst(pkt); got != tt.want {
				t.Errorf("isSelfDst(%v -> %v) = %v, want %v", tt.src, tt.dst, got, tt.want)
			}
		})
	}
}

// TestDeliverLoopback verifies that DeliverLoopback correctly re-serializes an
// outbound packet and delivers it back into gVisor's inbound path.
func TestDeliverLoopback(t *testing.T) {
	ep := newLinkEndpoint(64, 1280, "", groNotSupported)

	// Track delivered packets via a mock dispatcher.
	type delivered struct {
		proto tcpip.NetworkProtocolNumber
		data  []byte
	}
	deliveredCh := make(chan delivered, 4)
	ep.Attach(&mockDispatcher{
		onDeliverNetworkPacket: func(proto tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
			// Capture the raw bytes from the delivered packet. At this
			// point the packet is unparsed — everything is in the
			// payload, no headers have been consumed yet.
			buf := pkt.ToBuffer()
			raw := buf.Flatten()
			deliveredCh <- delivered{proto: proto, data: raw}
		},
	})

	t.Run("ipv4", func(t *testing.T) {
		selfAddr := netip.MustParseAddrPort("100.64.1.2:8081")
		pkt := makeUDP4PacketBuffer(selfAddr, selfAddr)
		// Capture what the outbound bytes look like before loopback.
		wantLen := pkt.Size()
		wantProto := pkt.NetworkProtocolNumber

		if !ep.DeliverLoopback(pkt) {
			t.Fatal("DeliverLoopback returned false")
		}

		select {
		case got := <-deliveredCh:
			if got.proto != wantProto {
				t.Errorf("proto = %d, want %d", got.proto, wantProto)
			}
			if len(got.data) != wantLen {
				t.Errorf("data length = %d, want %d", len(got.data), wantLen)
			}
		case <-time.After(time.Second):
			t.Fatal("timeout waiting for loopback delivery")
		}
	})

	t.Run("ipv6", func(t *testing.T) {
		selfAddr := netip.MustParseAddrPort("[fd7a:115c:a1e0::123]:8081")
		pkt := makeUDP6PacketBuffer(selfAddr, selfAddr)
		wantLen := pkt.Size()
		wantProto := pkt.NetworkProtocolNumber

		if !ep.DeliverLoopback(pkt) {
			t.Fatal("DeliverLoopback returned false")
		}

		select {
		case got := <-deliveredCh:
			if got.proto != wantProto {
				t.Errorf("proto = %d, want %d", got.proto, wantProto)
			}
			if len(got.data) != wantLen {
				t.Errorf("data length = %d, want %d", len(got.data), wantLen)
			}
		case <-time.After(time.Second):
			t.Fatal("timeout waiting for loopback delivery")
		}
	})

	t.Run("nil_dispatcher", func(t *testing.T) {
		ep2 := newLinkEndpoint(64, 1280, "", groNotSupported)
		// Don't attach a dispatcher.
		selfAddr := netip.MustParseAddrPort("100.64.1.2:8081")
		pkt := makeUDP4PacketBuffer(selfAddr, selfAddr)
		if ep2.DeliverLoopback(pkt) {
			t.Error("DeliverLoopback should return false with nil dispatcher")
		}
		// pkt refcount was consumed by DeliverLoopback, so we don't DecRef.
	})
}

// mockDispatcher implements stack.NetworkDispatcher for testing.
type mockDispatcher struct {
	onDeliverNetworkPacket func(tcpip.NetworkProtocolNumber, *stack.PacketBuffer)
}

func (d *mockDispatcher) DeliverNetworkPacket(proto tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) {
	if d.onDeliverNetworkPacket != nil {
		d.onDeliverNetworkPacket(proto, pkt)
	}
}

func (d *mockDispatcher) DeliverLinkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) {}

// udp4raw constructs a valid raw IPv4+UDP packet with proper checksums.
func udp4raw(t testing.TB, src, dst netip.Addr, sport, dport uint16, payload []byte) []byte {
	t.Helper()
	totalLen := header.IPv4MinimumSize + header.UDPMinimumSize + len(payload)
	buf := make([]byte, totalLen)

	ip := header.IPv4(buf)
	ip.Encode(&header.IPv4Fields{
		TotalLength: uint16(totalLen),
		Protocol:    uint8(header.UDPProtocolNumber),
		TTL:         64,
		SrcAddr:     tcpip.AddrFrom4Slice(src.AsSlice()),
		DstAddr:     tcpip.AddrFrom4Slice(dst.AsSlice()),
	})
	ip.SetChecksum(^ip.CalculateChecksum())

	// Build UDP header + payload.
	u := header.UDP(buf[header.IPv4MinimumSize:])
	u.Encode(&header.UDPFields{
		SrcPort: sport,
		DstPort: dport,
		Length:  uint16(header.UDPMinimumSize + len(payload)),
	})
	copy(buf[header.IPv4MinimumSize+header.UDPMinimumSize:], payload)

	xsum := header.PseudoHeaderChecksum(
		header.UDPProtocolNumber,
		tcpip.AddrFrom4Slice(src.AsSlice()),
		tcpip.AddrFrom4Slice(dst.AsSlice()),
		uint16(header.UDPMinimumSize+len(payload)),
	)
	u.SetChecksum(^header.UDP(buf[header.IPv4MinimumSize:]).CalculateChecksum(xsum))
	return buf
}

func fragmentIPv4ForTest(t testing.TB, pkt []byte, firstPayloadLen uint16) (first, second []byte) {
	t.Helper()
	if firstPayloadLen%8 != 0 {
		t.Fatalf("firstPayloadLen %d is not 8-byte aligned", firstPayloadLen)
	}
	if len(pkt) < header.IPv4MinimumSize+int(firstPayloadLen) {
		t.Fatalf("packet length %d too short for firstPayloadLen %d", len(pkt), firstPayloadLen)
	}
	ip := header.IPv4(pkt)
	if ip.HeaderLength() != header.IPv4MinimumSize {
		t.Fatalf("test helper only supports 20-byte IPv4 headers; got %d", ip.HeaderLength())
	}
	ipPayloadLen := len(pkt) - header.IPv4MinimumSize
	if int(firstPayloadLen) >= ipPayloadLen {
		t.Fatalf("firstPayloadLen %d must be smaller than IP payload length %d", firstPayloadLen, ipPayloadLen)
	}

	first = make([]byte, header.IPv4MinimumSize+int(firstPayloadLen))
	copy(first, pkt[:len(first)])
	firstIP := header.IPv4(first)
	firstIP.SetTotalLength(uint16(len(first)))
	firstIP.SetFlagsFragmentOffset(header.IPv4FlagMoreFragments, 0)
	firstIP.SetChecksum(0)
	firstIP.SetChecksum(^firstIP.CalculateChecksum())

	secondPayloadLen := ipPayloadLen - int(firstPayloadLen)
	second = make([]byte, header.IPv4MinimumSize+secondPayloadLen)
	copy(second[:header.IPv4MinimumSize], pkt[:header.IPv4MinimumSize])
	copy(second[header.IPv4MinimumSize:], pkt[header.IPv4MinimumSize+int(firstPayloadLen):])
	secondIP := header.IPv4(second)
	secondIP.SetTotalLength(uint16(len(second)))
	secondIP.SetFlagsFragmentOffset(0, firstPayloadLen)
	secondIP.SetChecksum(0)
	secondIP.SetChecksum(^secondIP.CalculateChecksum())

	return first, second
}

// TestLinkEndpointInjectInboundIPv4Fragments verifies that Tailscale's inbound
// link endpoint path lets IPv4 fragments reach gVisor for reassembly.
// Previously (see https://github.com/tailscale/tailscale/issues/20320),
// gro.RXChecksumOffload validated L4 checksums before reassembly, so the first
// fragment was dropped and the UDP datagram never reached the socket.
func TestLinkEndpointInjectInboundIPv4Fragments(t *testing.T) {
	const nicID tcpip.NICID = 1
	localIP := netip.MustParseAddr("100.64.1.2")
	remoteIP := netip.MustParseAddr("100.64.1.3")
	payload := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz")
	raw := udp4raw(t, remoteIP, localIP, 12345, 8081, payload)
	first, second := fragmentIPv4ForTest(t, raw, 80)

	s := stack.New(stack.Options{
		NetworkProtocols: []stack.NetworkProtocolFactory{
			ipv4.NewProtocol,
		},
		TransportProtocols: []stack.TransportProtocolFactory{
			udp.NewProtocol,
		},
	})
	defer s.Close()

	ep := newLinkEndpoint(64, 1280, "", groNotSupported)
	if err := s.CreateNIC(nicID, ep); err != nil {
		t.Fatalf("CreateNIC: %v", err)
	}
	if err := s.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
		Protocol:          header.IPv4ProtocolNumber,
		AddressWithPrefix: tcpip.AddrFrom4(localIP.As4()).WithPrefix(),
	}, stack.AddressProperties{}); err != nil {
		t.Fatalf("AddProtocolAddress: %v", err)
	}

	pc, err := gonet.DialUDP(s, &tcpip.FullAddress{
		NIC:  nicID,
		Addr: tcpip.AddrFrom4(localIP.As4()),
		Port: 8081,
	}, nil, header.IPv4ProtocolNumber)
	if err != nil {
		t.Fatalf("DialUDP: %v", err)
	}
	defer pc.Close()

	var parsed packet.Parsed
	parsed.Decode(first)
	ep.injectInbound(&parsed)
	parsed.Decode(second)
	ep.injectInbound(&parsed)

	if err := pc.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
		t.Fatalf("SetReadDeadline: %v", err)
	}
	buf := make([]byte, 512)
	n, addr, err := pc.ReadFrom(buf)
	if err != nil {
		t.Fatalf("ReadFrom: %v (fragmented packet was not reassembled and delivered)", err)
	}
	if got := string(buf[:n]); got != string(payload) {
		t.Fatalf("payload = %q, want %q", got, payload)
	}
	udpAddr, ok := addr.(*net.UDPAddr)
	if !ok {
		t.Fatalf("remote addr = %T(%v), want *net.UDPAddr", addr, addr)
	}
	if got := udpAddr.AddrPort(); got != netip.MustParseAddrPort("100.64.1.3:12345") {
		t.Fatalf("remote addr = %v, want 100.64.1.3:12345", got)
	}
}

// TestInjectLoopback verifies that the inject goroutine delivers self-addressed
// packets back into gVisor (via DeliverLoopback) instead of sending them to
// WireGuard outbound. This is a regression test for a bug where self-dial
// packets were sent to WireGuard and silently dropped.
func TestInjectLoopback(t *testing.T) {
	selfIP4 := netip.MustParseAddr("100.64.1.2")

	ns := makeNetstack(t, func(impl *Impl) {
		impl.ProcessLocalIPs = true
		impl.atomicIsLocalIPFunc.Store(func(addr netip.Addr) bool {
			return addr == selfIP4
		})
	})

	// Register gVisor's NIC address so the stack accepts and routes
	// packets for this IP.
	protocolAddr := tcpip.ProtocolAddress{
		Protocol:          header.IPv4ProtocolNumber,
		AddressWithPrefix: tcpip.AddrFrom4(selfIP4.As4()).WithPrefix(),
	}
	if err := ns.ipstack.AddProtocolAddress(nicID, protocolAddr, stack.AddressProperties{}); err != nil {
		t.Fatalf("AddProtocolAddress: %v", err)
	}

	// Bind a UDP socket on the gVisor stack to receive the loopback packet.
	pc, err := gonet.DialUDP(ns.ipstack, &tcpip.FullAddress{
		NIC:  nicID,
		Addr: tcpip.AddrFrom4(selfIP4.As4()),
		Port: 8081,
	}, nil, header.IPv4ProtocolNumber)
	if err != nil {
		t.Fatalf("DialUDP: %v", err)
	}
	defer pc.Close()

	// Build a valid self-addressed UDP packet from raw bytes and wrap it
	// in a gVisor PacketBuffer with headers already pushed, as gVisor's
	// outbound path produces.
	payload := []byte("loopback test")
	raw := udp4raw(t, selfIP4, selfIP4, 12345, 8081, payload)

	pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
		ReserveHeaderBytes: header.IPv4MinimumSize + header.UDPMinimumSize,
		Payload:            buffer.MakeWithData(payload),
	})
	copy(pkt.TransportHeader().Push(header.UDPMinimumSize),
		raw[header.IPv4MinimumSize:header.IPv4MinimumSize+header.UDPMinimumSize])
	pkt.TransportProtocolNumber = header.UDPProtocolNumber
	copy(pkt.NetworkHeader().Push(header.IPv4MinimumSize), raw[:header.IPv4MinimumSize])
	pkt.NetworkProtocolNumber = header.IPv4ProtocolNumber

	if err := ns.linkEP.q.Write(pkt); err != nil {
		t.Fatalf("queue.Write: %v", err)
	}

	// The inject goroutine should detect the self-addressed packet via
	// isSelfDst and deliver it back into gVisor via DeliverLoopback.
	pc.SetReadDeadline(time.Now().Add(5 * time.Second))
	buf := make([]byte, 256)
	n, _, err := pc.ReadFrom(buf)
	if err != nil {
		t.Fatalf("ReadFrom: %v (self-addressed packet was not looped back)", err)
	}
	if got := string(buf[:n]); got != "loopback test" {
		t.Errorf("got %q, want %q", got, "loopback test")
	}
}
