diff --git a/cmd/clawpatrol/daemon_linux.go b/cmd/clawpatrol/daemon_linux.go index 10301e38..82964190 100644 --- a/cmd/clawpatrol/daemon_linux.go +++ b/cmd/clawpatrol/daemon_linux.go @@ -566,8 +566,11 @@ func (d *daemon) handle(c net.Conn) { return } defer gvStack.Close() - startTunBridge(tunFile, gvEp, d.transport) + sessionCtx, cancelSession := context.WithCancel(context.Background()) + defer cancelSession() + startTunBridge(tunFile, gvEp) enableTransportTCPForwarder(gvStack, d.transport) + enableTransportUDPForwarder(sessionCtx, gvStack, d.transport, runUDPIdleTimeout) // 4. Tell the client the bridge is up. if _, err := io.WriteString(c, "ATTACHED\n"); err != nil { diff --git a/cmd/clawpatrol/daemon_session_linux.go b/cmd/clawpatrol/daemon_session_linux.go index 384740a0..ee073ca8 100644 --- a/cmd/clawpatrol/daemon_session_linux.go +++ b/cmd/clawpatrol/daemon_session_linux.go @@ -10,11 +10,14 @@ package main import ( "context" + "errors" "fmt" "net" "net/netip" "os" + "strconv" "sync" + "sync/atomic" "time" "gvisor.dev/gvisor/pkg/buffer" @@ -26,6 +29,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/waiter" ) @@ -35,6 +39,27 @@ import ( // child-side TUN doesn't need to cap. const runStackTunMTU = 65535 +const runUDPIdleTimeout = 2 * time.Minute + +// runUDPFlowLimit bounds endpoint, connection, and goroutine state per run +// session. 256 permits substantial DNS/QUIC concurrency while limiting a +// session to at most 512 checked-out 64 KiB relay buffers (32 MiB). +const runUDPFlowLimit = 256 + +var runUDPRelayBufferPool = sync.Pool{New: func() any { + buf := make([]byte, runStackTunMTU) + return &buf +}} + +func getRunUDPRelayBuffer() []byte { + return *runUDPRelayBufferPool.Get().(*[]byte) +} + +func putRunUDPRelayBuffer(buf []byte) { + buf = buf[:runStackTunMTU] + runUDPRelayBufferPool.Put(&buf) +} + // newRunStack creates a gVisor TCP/IP stack bound to localIP, which // is the transport's underlay address (tsnet 100.x.x.x or wg /32). // Promiscuous + spoofing enabled so the stack accepts inbound @@ -47,7 +72,7 @@ func newRunStack(localIP netip.Addr) (*stack.Stack, *channel.Endpoint, error) { ipv4.NewProtocol, ipv6.NewProtocol, }, TransportProtocols: []stack.TransportProtocolFactory{ - tcp.NewProtocol, + tcp.NewProtocol, udp.NewProtocol, }, HandleLocal: false, }) @@ -105,19 +130,26 @@ func (b *runTunBridge) WriteNotify() { } } +// injectRunTunPacket takes ownership of pkt. InjectInbound is synchronous and +// does not consume the caller's reference, so every dispatch and drop path +// releases it here. +func injectRunTunPacket(ep *channel.Endpoint, version byte, pkt *stack.PacketBuffer) { + defer pkt.DecRef() + switch version { + case 4: + ep.InjectInbound(header.IPv4ProtocolNumber, pkt) + case 6: + ep.InjectInbound(header.IPv6ProtocolNumber, pkt) + default: + // Drop packets with an unknown IP version. + } +} + // startTunBridge registers the outbound notification and starts the -// inbound read loop (TUN fd → gVisor InjectInbound). IPv4 UDP is -// intercepted before injection and forwarded directly via the -// transport so DNS / quic-style flows work without a UDP forwarder -// inside the per-session gVisor stack. -func startTunBridge(tunFile *os.File, ep *channel.Endpoint, transport daemonTransport) { +// inbound read loop (TUN fd → gVisor InjectInbound). +func startTunBridge(tunFile *os.File, ep *channel.Endpoint) { br := &runTunBridge{tunFile: tunFile, ep: ep} ep.AddNotify(br) - uf := &runUDPForwarder{ - transport: transport, - tunFile: tunFile, - flows: map[udpFlowKey]net.Conn{}, - } go func() { buf := make([]byte, runStackTunMTU) @@ -131,132 +163,178 @@ func startTunBridge(tunFile *os.File, ep *channel.Endpoint, transport daemonTran } pkt := make([]byte, n) copy(pkt, buf[:n]) - // Intercept IPv4 UDP before injecting into gVisor TCP stack. - if pkt[0]>>4 == 4 && n > 20 && pkt[9] == 17 { - uf.handle(pkt) - continue - } pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{ Payload: buffer.MakeWithData(pkt), }) - switch pkt[0] >> 4 { - case 4: - ep.InjectInbound(header.IPv4ProtocolNumber, pkb) - case 6: - ep.InjectInbound(header.IPv6ProtocolNumber, pkb) - default: - pkb.DecRef() - } + injectRunTunPacket(ep, pkt[0]>>4, pkb) } }() } -// runUDPForwarder maintains per-flow transport UDP connections for -// the child netns. Each unique (srcIP:srcPort → dstIP:dstPort) -// 4-tuple gets one transport.Dial("udp", ...) conn. -type runUDPForwarder struct { - transport daemonTransport - tunFile *os.File - mu sync.Mutex - flows map[udpFlowKey]net.Conn +// enableTransportUDPForwarder installs gVisor's dual-stack UDP +// forwarder. gVisor owns packet parsing, checksums and full-tuple flow +// demultiplexing; each accepted endpoint is relayed as datagrams over +// one transport connection. +func enableTransportUDPForwarder(ctx context.Context, s *stack.Stack, transport daemonTransport, idleTimeout time.Duration) { + enableTransportUDPForwarderWithLimit(ctx, s, transport, idleTimeout, runUDPFlowLimit) } -type udpFlowKey struct { - srcIP, dstIP [4]byte - srcPort, dstPort uint16 +func enableTransportUDPForwarderWithLimit(ctx context.Context, s *stack.Stack, transport daemonTransport, idleTimeout time.Duration, flowLimit int) { + slots := make(chan struct{}, flowLimit) + s.SetTransportProtocolHandler(udp.ProtocolNumber, newTransportUDPProtocolHandler(ctx, s, transport, idleTimeout, slots)) } -func (f *runUDPForwarder) handle(pkt []byte) { - ihl := int(pkt[0]&0xf) * 4 - if len(pkt) < ihl+8 { - return +// newRunUDPProtocolHandler borrows the stack-owned pkt only for the synchronous +// callback. CreateEndpoint must happen before the callback returns so its queue +// receives the endpoint's own packet clone. +func newRunUDPProtocolHandler(s *stack.Stack, handler udp.ForwarderHandler) func(stack.TransportEndpointID, *stack.PacketBuffer) bool { + return func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { + return handler(udp.NewForwarderRequest(s, id, pkt)) } - var srcIP, dstIP [4]byte - copy(srcIP[:], pkt[12:16]) - copy(dstIP[:], pkt[16:20]) - srcPort := uint16(pkt[ihl])<<8 | uint16(pkt[ihl+1]) - dstPort := uint16(pkt[ihl+2])<<8 | uint16(pkt[ihl+3]) - udpLen := int(pkt[ihl+4])<<8 | int(pkt[ihl+5]) - if udpLen < 8 || ihl+udpLen > len(pkt) { - return - } - payload := pkt[ihl+8 : ihl+udpLen] +} - key := udpFlowKey{srcIP, dstIP, srcPort, dstPort} +func newTransportUDPProtocolHandler(ctx context.Context, s *stack.Stack, transport daemonTransport, idleTimeout time.Duration, slots chan struct{}) func(stack.TransportEndpointID, *stack.PacketBuffer) bool { + return newRunUDPProtocolHandler(s, func(req *udp.ForwarderRequest) bool { + select { + case slots <- struct{}{}: + default: + // Returning false lets gVisor generate the appropriate unreachable. + return false + } + release := func() { <-slots } - f.mu.Lock() - conn, ok := f.flows[key] - if !ok { - dstAddr := fmt.Sprintf("%d.%d.%d.%d:%d", - dstIP[0], dstIP[1], dstIP[2], dstIP[3], dstPort) - var err error - conn, err = f.transport.Dial(context.Background(), "udp", dstAddr) - if err != nil { - f.mu.Unlock() - return + // CreateEndpoint must remain in the callback, but transport.Dial may + // block for its full timeout and must not stall the sole TUN ingress + // loop. Capture the request ID by value before starting the goroutine. + id := req.ID() + var wq waiter.Queue + ep, terr := req.CreateEndpoint(&wq) + if terr != nil { + release() + return true } - f.flows[key] = conn + local := gonet.NewUDPConn(&wq, ep) + dstAddr := net.JoinHostPort(id.LocalAddress.String(), strconv.Itoa(int(id.LocalPort))) go func() { - f.readResponses(conn, dstIP, srcIP, dstPort, srcPort) - f.mu.Lock() - delete(f.flows, key) - f.mu.Unlock() - _ = conn.Close() + dialCtx, cancel := context.WithTimeout(ctx, transportDialTimeout) + defer cancel() + remote, err := transport.Dial(dialCtx, "udp", dstAddr) + if err != nil { + // The callback has already returned true, so no ICMP unreachable + // can be requested here without fabricating a packet. Closing the + // endpoint avoids retaining a black hole and permits tuple redial. + _ = local.Close() + release() + return + } + relayUDPDatagrams(ctx, local, remote, idleTimeout, release) }() - } - f.mu.Unlock() - - _, _ = conn.Write(payload) + return true + }) } -func (f *runUDPForwarder) readResponses(conn net.Conn, srcIP, dstIP [4]byte, srcPort, dstPort uint16) { - buf := make([]byte, 65535) - for { - _ = conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - n, err := conn.Read(buf) - if err != nil { - return - } - _, _ = f.tunFile.Write(buildUDPPacket(srcIP, dstIP, srcPort, dstPort, buf[:n])) +func udpIdleRemaining(lastActivity int64, now time.Time, idleTimeout time.Duration) time.Duration { + elapsed := now.Sub(time.Unix(0, lastActivity)) + if elapsed < 0 { + return idleTimeout } + return idleTimeout - elapsed } -// buildUDPPacket constructs a raw IPv4+UDP packet. UDP checksum is zero -// (optional for IPv4; Linux accepts these from TUN devices). -func buildUDPPacket(srcIP, dstIP [4]byte, srcPort, dstPort uint16, payload []byte) []byte { - udpLen := 8 + len(payload) - ipLen := 20 + udpLen - pkt := make([]byte, ipLen) - pkt[0] = 0x45 // IPv4, IHL=5 - pkt[2] = byte(ipLen >> 8) - pkt[3] = byte(ipLen) - pkt[8] = 64 // TTL - pkt[9] = 17 // UDP - copy(pkt[12:16], srcIP[:]) - copy(pkt[16:20], dstIP[:]) - cs := ipv4Checksum(pkt[:20]) - pkt[10] = byte(cs >> 8) - pkt[11] = byte(cs) - pkt[20] = byte(srcPort >> 8) - pkt[21] = byte(srcPort) - pkt[22] = byte(dstPort >> 8) - pkt[23] = byte(dstPort) - pkt[24] = byte(udpLen >> 8) - pkt[25] = byte(udpLen) - // pkt[26:28] = 0 (checksum omitted) - copy(pkt[28:], payload) - return pkt -} - -func ipv4Checksum(b []byte) uint16 { - var sum uint32 - for i := 0; i+1 < len(b); i += 2 { - sum += uint32(b[i])<<8 | uint32(b[i+1]) +func relayUDPDatagrams(ctx context.Context, local, remote net.Conn, idleTimeout time.Duration, release func()) { + defer release() + closeBoth := func() { + _ = local.Close() + _ = remote.Close() + } + activity := make(chan struct{}, 1) + done := make(chan struct{}, 1) + var lastActivity atomic.Int64 + lastActivity.Store(time.Now().UnixNano()) + signalDone := func() { + select { + case done <- struct{}{}: + default: + } } - for sum>>16 != 0 { - sum = (sum & 0xffff) + (sum >> 16) + copyDatagrams := func(dst, src net.Conn, boundRead bool) { + buf := getRunUDPRelayBuffer() + defer putRunUDPRelayBuffer(buf) + for { + if boundRead { + now := time.Now() + remaining := udpIdleRemaining(lastActivity.Load(), now, idleTimeout) + if remaining <= 0 { + signalDone() + return + } + if err := src.SetReadDeadline(now.Add(remaining)); err != nil { + signalDone() + return + } + } + n, err := src.Read(buf) + if err != nil { + if boundRead { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() && + udpIdleRemaining(lastActivity.Load(), time.Now(), idleTimeout) > 0 { + // Activity in the opposite direction raced this deadline. + // Recalculate from the shared timestamp and keep reading. + continue + } + } + signalDone() + return + } + if _, err := dst.Write(buf[:n]); err != nil { + signalDone() + return + } + lastActivity.Store(time.Now().UnixNano()) + select { + case activity <- struct{}{}: + default: + } + } + } + go copyDatagrams(remote, local, false) + go copyDatagrams(local, remote, true) + timer := time.NewTimer(idleTimeout) + defer timer.Stop() + defer closeBoth() + resetFromLastActivity := func() bool { + remaining := udpIdleRemaining(lastActivity.Load(), time.Now(), idleTimeout) + if remaining <= 0 { + return false + } + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(remaining) + return true + } + for { + select { + case <-ctx.Done(): + return + case <-done: + return + case <-timer.C: + // A successful write can race delivery of timer.C. Re-read the + // race-safe timestamp before deciding the flow is idle. + if !resetFromLastActivity() { + return + } + case <-activity: + if !resetFromLastActivity() { + return + } + } } - return ^uint16(sum) } // transportDialTimeout bounds the upstream transport.Dial while the diff --git a/cmd/clawpatrol/daemon_session_linux_test.go b/cmd/clawpatrol/daemon_session_linux_test.go index 445306d3..7cde7aa5 100644 --- a/cmd/clawpatrol/daemon_session_linux_test.go +++ b/cmd/clawpatrol/daemon_session_linux_test.go @@ -9,14 +9,19 @@ package main import ( "context" + "encoding/binary" + "fmt" "net" "net/netip" + "os" "slices" + "strconv" "strings" "sync" "testing" "time" + "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -25,6 +30,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" ) // fakeTransport implements daemonTransport with a pluggable Dial. @@ -50,6 +57,98 @@ func (f *fakeTransport) dialedAddrs() []string { return slices.Clone(f.dialed) } +type recordingNetworkDispatcher struct { + protocols []tcpip.NetworkProtocolNumber +} + +func (d *recordingNetworkDispatcher) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, _ *stack.PacketBuffer) { + d.protocols = append(d.protocols, protocol) +} + +func (*recordingNetworkDispatcher) DeliverLinkPacket(tcpip.NetworkProtocolNumber, *stack.PacketBuffer) { +} + +func TestInjectRunTunPacketReleasesCallerReference(t *testing.T) { + tests := []struct { + name string + version byte + wantProtocol tcpip.NetworkProtocolNumber + }{ + {name: "IPv4", version: 4, wantProtocol: header.IPv4ProtocolNumber}, + {name: "IPv6", version: 6, wantProtocol: header.IPv6ProtocolNumber}, + {name: "unknown", version: 15}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := channel.New(1, uint32(runStackTunMTU), "") + dispatcher := &recordingNetworkDispatcher{} + ep.Attach(dispatcher) + releases := 0 + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{OnRelease: func() { releases++ }}) + + injectRunTunPacket(ep, tt.version, pkt) + + if got := pkt.ReadRefs(); got != 0 { + t.Fatalf("PacketBuffer refs = %d, want 0", got) + } + if releases != 1 { + t.Fatalf("PacketBuffer releases = %d, want exactly 1", releases) + } + if tt.wantProtocol == 0 { + if len(dispatcher.protocols) != 0 { + t.Fatalf("unknown packet dispatched as protocols %v", dispatcher.protocols) + } + return + } + if !slices.Equal(dispatcher.protocols, []tcpip.NetworkProtocolNumber{tt.wantProtocol}) { + t.Fatalf("dispatched protocols = %v, want [%d]", dispatcher.protocols, tt.wantProtocol) + } + }) + } +} + +func TestRunUDPProtocolHandlerDoesNotCloneRejectedPacket(t *testing.T) { + s := stack.New(stack.Options{}) + t.Cleanup(s.Close) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{}) + handler := newRunUDPProtocolHandler(s, func(*udp.ForwarderRequest) bool { return false }) + + if handler(stack.TransportEndpointID{}, pkt) { + t.Fatal("handler accepted rejected request") + } + if got := pkt.ReadRefs(); got != 1 { + t.Fatalf("PacketBuffer refs after rejection = %d, want original caller ref only", got) + } + pkt.DecRef() + if got := pkt.ReadRefs(); got != 0 { + t.Fatalf("PacketBuffer refs after caller release = %d, want 0", got) + } +} + +func TestRunUDPProtocolHandlerFlowLimitRejectionDoesNotClonePacket(t *testing.T) { + s := stack.New(stack.Options{}) + t.Cleanup(s.Close) + ft := &fakeTransport{dial: func(string, string) (net.Conn, error) { + t.Fatal("flow-limit rejection must not dial") + return nil, context.Canceled + }} + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{}) + slots := make(chan struct{}, 1) + slots <- struct{}{} + handler := newTransportUDPProtocolHandler(context.Background(), s, ft, time.Minute, slots) + + if handler(stack.TransportEndpointID{}, pkt) { + t.Fatal("handler accepted request with no available flow slots") + } + if got := pkt.ReadRefs(); got != 1 { + t.Fatalf("PacketBuffer refs after flow-limit rejection = %d, want original caller ref only", got) + } + pkt.DecRef() + if got := pkt.ReadRefs(); got != 0 { + t.Fatalf("PacketBuffer refs after caller release = %d, want 0", got) + } +} + // pump copies outbound packets from src's channel endpoint into dst's // inbound path until ctx is done. func pump(ctx context.Context, src, dst *channel.Endpoint) { @@ -239,3 +338,774 @@ func readFull(c net.Conn, buf []byte) (int, error) { } return n, nil } + +type udpForwarderHarness struct { + cli *stack.Stack + cancel context.CancelFunc + responses chan []byte +} + +func newUDPForwarderHarness(t *testing.T, transport daemonTransport, idleTimeout time.Duration) *udpForwarderHarness { + return newUDPForwarderHarnessWithLimit(t, transport, idleTimeout, runUDPFlowLimit) +} + +func newUDPForwarderHarnessWithLimit(t *testing.T, transport daemonTransport, idleTimeout time.Duration, flowLimit int) *udpForwarderHarness { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0) + if err != nil { + t.Fatalf("socketpair: %v", err) + } + daemonTun := os.NewFile(uintptr(fds[0]), "daemon-tun") + clientTun := os.NewFile(uintptr(fds[1]), "client-tun") + t.Cleanup(func() { _ = daemonTun.Close() }) + t.Cleanup(func() { _ = clientTun.Close() }) + + srv, srvEp, err := newRunStack(netip.MustParseAddr("100.64.0.5")) + if err != nil { + t.Fatalf("server stack: %v", err) + } + t.Cleanup(srv.Close) + enableTransportUDPForwarderWithLimit(ctx, srv, transport, idleTimeout, flowLimit) + startTunBridge(daemonTun, srvEp) + + cliEp := channel.New(netstackQueueSize, uint32(runStackTunMTU), "") + cli := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}, + }) + t.Cleanup(cli.Close) + if e := cli.CreateNIC(1, cliEp); e != nil { + t.Fatalf("client CreateNIC: %v", e) + } + for _, addr := range []netip.Addr{netip.MustParseAddr("192.0.2.2"), netip.MustParseAddr(runTunAddr6)} { + proto := ipv4.ProtocolNumber + if addr.Is6() { + proto = ipv6.ProtocolNumber + } + pa := tcpip.ProtocolAddress{Protocol: proto, AddressWithPrefix: tcpip.AddrFromSlice(addr.AsSlice()).WithPrefix()} + if e := cli.AddProtocolAddress(1, pa, stack.AddressProperties{}); e != nil { + t.Fatalf("client address %s: %v", addr, e) + } + } + cli.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1}) + cli.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1}) + + responses := make(chan []byte, 16) + go func() { + for { + pkt := cliEp.ReadContext(ctx) + if pkt == nil { + return + } + view := pkt.ToView() + pkt.DecRef() + _, _ = clientTun.Write(view.AsSlice()) + } + }() + go func() { + buf := make([]byte, runStackTunMTU) + for { + n, err := clientTun.Read(buf) + if err != nil { + return + } + raw := slices.Clone(buf[:n]) + select { + case responses <- slices.Clone(raw): + default: + } + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(raw)}) + proto := header.IPv4ProtocolNumber + if raw[0]>>4 == 6 { + proto = header.IPv6ProtocolNumber + } + cliEp.InjectInbound(proto, pkt) + pkt.DecRef() + } + }() + return &udpForwarderHarness{cli: cli, cancel: cancel, responses: responses} +} + +func startUDPEcho(t *testing.T) *net.UDPAddr { + t.Helper() + c, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("listen UDP echo: %v", err) + } + t.Cleanup(func() { _ = c.Close() }) + go func() { + buf := make([]byte, 65535) + for { + n, addr, err := c.ReadFromUDP(buf) + if err != nil { + return + } + _, _ = c.WriteToUDP(buf[:n], addr) + } + }() + return c.LocalAddr().(*net.UDPAddr) +} + +func dialUDPThroughHarness(t *testing.T, cli *stack.Stack, dst netip.Addr, port uint16) *gonet.UDPConn { + t.Helper() + proto := ipv4.ProtocolNumber + if dst.Is6() { + proto = ipv6.ProtocolNumber + } + c, err := gonet.DialUDP(cli, nil, &tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(dst.AsSlice()), Port: port}, proto) + if err != nil { + t.Fatalf("DialUDP %s: %v", dst, err) + } + t.Cleanup(func() { _ = c.Close() }) + return c +} + +func checksum16(parts ...[]byte) uint16 { + var sum uint32 + for _, part := range parts { + for len(part) >= 2 { + sum += uint32(binary.BigEndian.Uint16(part)) + part = part[2:] + } + if len(part) == 1 { + sum += uint32(part[0]) << 8 + } + } + for sum>>16 != 0 { + sum = (sum & 0xffff) + (sum >> 16) + } + return uint16(sum) +} + +func assertIPv6UDPResponse(t *testing.T, raw []byte, src, dst netip.Addr, srcPort, dstPort uint16, payload []byte) { + t.Helper() + if len(raw) != 48+len(payload) || raw[0]>>4 != 6 || raw[6] != byte(header.UDPProtocolNumber) { + t.Fatalf("unexpected IPv6 UDP packet: len=%d", len(raw)) + } + if got := netip.AddrFrom16([16]byte(raw[8:24])); got != src { + t.Fatalf("source IP = %s, want %s", got, src) + } + if got := netip.AddrFrom16([16]byte(raw[24:40])); got != dst { + t.Fatalf("destination IP = %s, want %s", got, dst) + } + udpPacket := raw[40:] + if got := binary.BigEndian.Uint16(udpPacket[0:2]); got != srcPort { + t.Fatalf("source port = %d, want %d", got, srcPort) + } + if got := binary.BigEndian.Uint16(udpPacket[2:4]); got != dstPort { + t.Fatalf("destination port = %d, want %d", got, dstPort) + } + if got := binary.BigEndian.Uint16(udpPacket[6:8]); got == 0 { + t.Fatal("IPv6 UDP checksum is zero") + } + if !slices.Equal(udpPacket[8:], payload) { + t.Fatalf("payload = %q, want %q", udpPacket[8:], payload) + } + pseudo := make([]byte, 40) + copy(pseudo[0:16], raw[8:24]) + copy(pseudo[16:32], raw[24:40]) + binary.BigEndian.PutUint32(pseudo[32:36], uint32(len(udpPacket))) + pseudo[39] = byte(header.UDPProtocolNumber) + if got := checksum16(pseudo, udpPacket); got != 0xffff { + t.Fatalf("IPv6 UDP checksum folds to %#04x, want 0xffff", got) + } +} + +type deadlineObservedConn struct { + net.Conn + mu sync.Mutex + readDeadline time.Time + readWithoutDeadline bool + readStarted chan struct{} + readOnce sync.Once +} + +func (c *deadlineObservedConn) SetReadDeadline(deadline time.Time) error { + c.mu.Lock() + c.readDeadline = deadline + c.mu.Unlock() + return c.Conn.SetReadDeadline(deadline) +} + +func (c *deadlineObservedConn) Read(buf []byte) (int, error) { + c.mu.Lock() + if c.readDeadline.IsZero() { + c.readWithoutDeadline = true + } + c.mu.Unlock() + c.readOnce.Do(func() { close(c.readStarted) }) + return c.Conn.Read(buf) +} + +func (c *deadlineObservedConn) readState() (time.Time, bool) { + c.mu.Lock() + defer c.mu.Unlock() + return c.readDeadline, c.readWithoutDeadline +} + +func TestRelayUDPDatagramsBoundsUpstreamRead(t *testing.T) { + localRelay, localPeer := net.Pipe() + remoteRelay, remotePeer := net.Pipe() + defer func() { _ = localPeer.Close() }() + defer func() { _ = remotePeer.Close() }() + + observed := &deadlineObservedConn{Conn: remoteRelay, readStarted: make(chan struct{})} + ctx, cancel := context.WithCancel(context.Background()) + released := make(chan struct{}) + const idleTimeout = 250 * time.Millisecond + startedAt := time.Now() + go relayUDPDatagrams(ctx, localRelay, observed, idleTimeout, func() { close(released) }) + + select { + case <-observed.readStarted: + case <-time.After(time.Second): + t.Fatal("upstream UDP Read did not start") + } + deadline, readWithoutDeadline := observed.readState() + if readWithoutDeadline || deadline.IsZero() { + t.Fatal("upstream UDP Read started without an idle-bounded deadline") + } + if deadline.Before(startedAt) || deadline.After(startedAt.Add(idleTimeout+100*time.Millisecond)) { + t.Fatalf("upstream read deadline = %v, want within one idle timeout of %v", deadline, startedAt) + } + cancel() + select { + case <-released: + case <-time.After(time.Second): + t.Fatal("relay did not stop after cancellation") + } +} + +type closeNotifyConn struct { + net.Conn + closed chan struct{} + once sync.Once +} + +func (c *closeNotifyConn) Close() error { + c.once.Do(func() { close(c.closed) }) + return c.Conn.Close() +} + +func TestRunStackUDPForwarderDialFailureDoesNotRetainFlow(t *testing.T) { + for _, dst := range []netip.Addr{netip.MustParseAddr("192.0.2.53"), netip.MustParseAddr("fd78::53")} { + t.Run(dst.String(), func(t *testing.T) { + ft := &fakeTransport{dial: func(string, string) (net.Conn, error) { return nil, context.DeadlineExceeded }} + h := newUDPForwarderHarness(t, ft, time.Minute) + c := dialUDPThroughHarness(t, h.cli, dst, 5353) + if _, err := c.Write([]byte("fail")); err != nil { + t.Fatalf("first write: %v", err) + } + deadline := time.Now().Add(500 * time.Millisecond) + for len(ft.dialedAddrs()) < 1 && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + _, portText, err := net.SplitHostPort(c.LocalAddr().String()) + if err != nil { + t.Fatalf("local address: %v", err) + } + localPort, err := strconv.Atoi(portText) + if err != nil { + t.Fatalf("local port: %v", err) + } + _ = c.Close() + + proto := ipv4.ProtocolNumber + localIP := netip.MustParseAddr("192.0.2.2") + if dst.Is6() { + proto = ipv6.ProtocolNumber + localIP = netip.MustParseAddr(runTunAddr6) + } + local := &tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(localIP.AsSlice()), Port: uint16(localPort)} + remote := &tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(dst.AsSlice()), Port: 5353} + retry, terr := gonet.DialUDP(h.cli, local, remote, proto) + if terr != nil { + t.Fatalf("recreate same tuple: %v", terr) + } + defer func() { _ = retry.Close() }() + if _, err := retry.Write([]byte("fresh request")); err != nil { + t.Fatalf("fresh write: %v", err) + } + for len(ft.dialedAddrs()) < 2 && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + wantDial := "udp|" + net.JoinHostPort(dst.String(), "5353") + want := []string{wantDial, wantDial} + if got := ft.dialedAddrs(); !slices.Equal(got, want) { + t.Fatalf("dialed = %v, want %v; failed request was retained as a black hole", got, want) + } + }) + } +} + +func TestRunStackUDPForwarderBlockedDialDoesNotBlockIngress(t *testing.T) { + block := make(chan struct{}) + started := make(chan string, 2) + ft := &fakeTransport{dial: func(_ string, addr string) (net.Conn, error) { + started <- addr + if strings.HasSuffix(addr, ":5301") { + <-block + } + return nil, context.DeadlineExceeded + }} + h := newUDPForwarderHarnessWithLimit(t, ft, time.Minute, 2) + first := dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("192.0.2.53"), 5301) + second := dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("192.0.2.54"), 5302) + if _, err := first.Write([]byte("block")); err != nil { + t.Fatal(err) + } + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("first dial did not start") + } + if _, err := second.Write([]byte("must progress")); err != nil { + t.Fatal(err) + } + select { + case got := <-started: + if !strings.HasSuffix(got, ":5302") { + t.Fatalf("second dial = %q", got) + } + case <-time.After(300 * time.Millisecond): + t.Fatal("second UDP flow was blocked behind transport.Dial") + } + close(block) +} + +func TestRunStackUDPForwarderFlowLimitReleasesAfterDialFailure(t *testing.T) { + block := make(chan struct{}) + started := make(chan string, 2) + ft := &fakeTransport{dial: func(_ string, addr string) (net.Conn, error) { + started <- addr + if strings.HasSuffix(addr, ":5301") { + <-block + } + return nil, context.DeadlineExceeded + }} + h := newUDPForwarderHarnessWithLimit(t, ft, time.Minute, 1) + first := dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("192.0.2.53"), 5301) + second := dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("192.0.2.54"), 5302) + _, _ = first.Write([]byte("occupy")) + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("first dial did not start") + } + _, _ = second.Write([]byte("full")) + select { + case got := <-started: + t.Fatalf("dial %q admitted above limit", got) + case <-time.After(50 * time.Millisecond): + } + close(block) + deadline := time.NewTimer(time.Second) + defer deadline.Stop() + retry := time.NewTicker(time.Millisecond) + defer retry.Stop() + for { + select { + case <-started: + return + case <-retry.C: + _ = second.Close() + second = dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("192.0.2.54"), 5302) + _, _ = second.Write([]byte("released")) + case <-deadline.C: + t.Fatal("slot not released after dial failure") + } + } +} + +func TestRunUDPRelayBufferPoolNormalizesFullDatagramBuffer(t *testing.T) { + first := getRunUDPRelayBuffer() + if len(first) != runStackTunMTU || cap(first) != runStackTunMTU { + t.Fatalf("buffer len/cap = %d/%d, want %d/%d", len(first), cap(first), runStackTunMTU, runStackTunMTU) + } + putRunUDPRelayBuffer(first[:1]) + second := getRunUDPRelayBuffer() + defer putRunUDPRelayBuffer(second) + if len(second) != runStackTunMTU || cap(second) != runStackTunMTU { + t.Fatalf("buffer after shortened Put has len/cap = %d/%d, want %d/%d", len(second), cap(second), runStackTunMTU, runStackTunMTU) + } +} + +func TestUDPIdleRemainingHonorsActivityAtExpiry(t *testing.T) { + now := time.Unix(100, 0) + const timeout = 50 * time.Millisecond + last := now.Add(-timeout + time.Nanosecond).UnixNano() + if got := udpIdleRemaining(last, now, timeout); got != time.Nanosecond { + t.Fatalf("remaining = %v, want 1ns; timer expiry must re-check successful activity", got) + } +} + +func TestRunStackUDPForwarderSessionCleanup(t *testing.T) { + echo := startUDPEcho(t) + closed := make(chan chan struct{}, 2) + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { + c, err := net.DialUDP(network, nil, echo) + if err != nil { + return nil, err + } + notify := make(chan struct{}) + closed <- notify + return &closeNotifyConn{Conn: c, closed: notify}, nil + }} + h := newUDPForwarderHarness(t, ft, time.Minute) + for _, dst := range []netip.Addr{netip.MustParseAddr("192.0.2.53"), netip.MustParseAddr("fd78::53")} { + c := dialUDPThroughHarness(t, h.cli, dst, 5353) + _ = c.SetDeadline(time.Now().Add(time.Second)) + if _, err := c.Write([]byte("establish")); err != nil { + t.Fatalf("write %s: %v", dst, err) + } + buf := make([]byte, 32) + if _, err := c.Read(buf); err != nil { + t.Fatalf("read %s: %v", dst, err) + } + } + notifications := []chan struct{}{<-closed, <-closed} + h.cancel() + for i, notify := range notifications { + select { + case <-notify: + case <-time.After(500 * time.Millisecond): + t.Fatalf("flow %d remained open after session cancellation", i) + } + } +} + +func TestRunStackUDPForwarderActivityRefreshesIdleTimeout(t *testing.T) { + for _, direction := range []string{"child-to-upstream", "upstream-to-child"} { + t.Run(direction, func(t *testing.T) { + server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("listen: %v", err) + } + defer func() { _ = server.Close() }() + peerCh := make(chan *net.UDPAddr, 1) + go func() { + buf := make([]byte, 32) + for { + _, peer, err := server.ReadFromUDP(buf) + if err != nil { + return + } + select { + case peerCh <- peer: + default: + } + } + }() + closed := make(chan struct{}) + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { + c, err := net.DialUDP(network, nil, server.LocalAddr().(*net.UDPAddr)) + if err != nil { + return nil, err + } + return &closeNotifyConn{Conn: c, closed: closed}, nil + }} + h := newUDPForwarderHarness(t, ft, 60*time.Millisecond) + c := dialUDPThroughHarness(t, h.cli, netip.MustParseAddr("fd78::53"), 5353) + _ = c.SetDeadline(time.Now().Add(time.Second)) + if _, err := c.Write([]byte("establish")); err != nil { + t.Fatalf("establish: %v", err) + } + peer := <-peerCh + for i := 0; i < 4; i++ { + time.Sleep(35 * time.Millisecond) + if direction == "child-to-upstream" { + if _, err := c.Write([]byte("active")); err != nil { + t.Fatalf("child activity: %v", err) + } + } else { + if _, err := server.WriteToUDP([]byte("active"), peer); err != nil { + t.Fatalf("upstream activity: %v", err) + } + if _, err := c.Read(make([]byte, 32)); err != nil { + t.Fatalf("client read activity: %v", err) + } + } + select { + case <-closed: + t.Fatal("flow closed despite successful one-way activity") + default: + } + } + select { + case <-closed: + case <-time.After(300 * time.Millisecond): + t.Fatal("flow did not close after activity stopped") + } + }) + } +} + +func TestRunStackUDPForwarderIdleCleanup(t *testing.T) { + echo := startUDPEcho(t) + closeNotifications := make(chan chan struct{}, 2) + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { + c, err := net.DialUDP(network, nil, echo) + if err != nil { + return nil, err + } + closed := make(chan struct{}) + closeNotifications <- closed + return &closeNotifyConn{Conn: c, closed: closed}, nil + }} + h := newUDPForwarderHarness(t, ft, 80*time.Millisecond) + dst := netip.MustParseAddr("fd78::53") + c := dialUDPThroughHarness(t, h.cli, dst, 5353) + _ = c.SetDeadline(time.Now().Add(time.Second)) + _, portText, err := net.SplitHostPort(c.LocalAddr().String()) + if err != nil { + t.Fatalf("local address: %v", err) + } + localPort, err := strconv.Atoi(portText) + if err != nil { + t.Fatalf("local port: %v", err) + } + if _, err := c.Write([]byte("establish")); err != nil { + t.Fatalf("write: %v", err) + } + buf := make([]byte, 32) + if _, err := c.Read(buf); err != nil { + t.Fatalf("read: %v", err) + } + firstClosed := <-closeNotifications + select { + case <-firstClosed: + case <-time.After(500 * time.Millisecond): + t.Fatal("idle flow did not close upstream connection") + } + _ = c.Close() + + local := &tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(netip.MustParseAddr(runTunAddr6).AsSlice()), Port: uint16(localPort)} + remote := &tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFromSlice(dst.AsSlice()), Port: 5353} + retry, terr := gonet.DialUDP(h.cli, local, remote, ipv6.ProtocolNumber) + if terr != nil { + t.Fatalf("recreate expired tuple: %v", terr) + } + defer func() { _ = retry.Close() }() + if _, err := retry.Write([]byte("fresh")); err != nil { + t.Fatalf("fresh write: %v", err) + } + select { + case <-closeNotifications: + case <-time.After(500 * time.Millisecond): + t.Fatal("expired endpoint retained tuple; no fresh transport dial") + } + if got := len(ft.dialedAddrs()); got != 2 { + t.Fatalf("transport dials = %d, want 2 after recreating expired tuple", got) + } +} + +func TestRunStackUDPForwarderIsolatesConcurrentFlows(t *testing.T) { + var ( + serversMu sync.Mutex + servers []*net.UDPConn + ) + t.Cleanup(func() { + serversMu.Lock() + defer serversMu.Unlock() + for _, server := range servers { + _ = server.Close() + } + }) + ft := &fakeTransport{dial: func(network, addr string) (net.Conn, error) { + server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + return nil, err + } + serversMu.Lock() + servers = append(servers, server) + serversMu.Unlock() + go func() { + buf := make([]byte, 256) + n, peer, err := server.ReadFromUDP(buf) + if err == nil { + _, _ = server.WriteToUDP(append([]byte(addr+"|"), buf[:n]...), peer) + } + }() + return net.DialUDP(network, nil, server.LocalAddr().(*net.UDPAddr)) + }} + h := newUDPForwarderHarness(t, ft, time.Minute) + flows := []struct { + dst netip.Addr + port uint16 + }{ + {netip.MustParseAddr("192.0.2.53"), 5301}, + {netip.MustParseAddr("192.0.2.54"), 5302}, + {netip.MustParseAddr("fd78::53"), 5303}, + {netip.MustParseAddr("2001:db8:781::54"), 5304}, + } + start := make(chan struct{}) + errs := make(chan error, len(flows)) + conns := make([]*gonet.UDPConn, len(flows)) + for i, flow := range flows { + conns[i] = dialUDPThroughHarness(t, h.cli, flow.dst, flow.port) + } + for i, flow := range flows { + go func(i int, flow struct { + dst netip.Addr + port uint16 + }) { + c := conns[i] + _ = c.SetDeadline(time.Now().Add(3 * time.Second)) + <-start + payload := []byte(strconv.Itoa(i)) + if _, err := c.Write(payload); err != nil { + errs <- err + return + } + buf := make([]byte, 256) + n, err := c.Read(buf) + want := net.JoinHostPort(flow.dst.String(), strconv.Itoa(int(flow.port))) + "|" + string(payload) + if err != nil || string(buf[:n]) != want { + errs <- fmt.Errorf("flow %s read %q, %w; want %q", flow.dst, buf[:n], err, want) + return + } + errs <- nil + }(i, flow) + } + close(start) + for range flows { + if err := <-errs; err != nil { + t.Fatal(err) + } + } + got := ft.dialedAddrs() + slices.Sort(got) + want := make([]string, 0, len(flows)) + for _, flow := range flows { + want = append(want, "udp|"+net.JoinHostPort(flow.dst.String(), strconv.Itoa(int(flow.port)))) + } + slices.Sort(want) + if !slices.Equal(got, want) { + t.Fatalf("dialed = %v, want %v", got, want) + } +} + +func TestRunStackUDPForwarderZeroLengthDatagram(t *testing.T) { + for _, dst := range []netip.Addr{netip.MustParseAddr("192.0.2.53"), netip.MustParseAddr("fd78::53")} { + t.Run(dst.String(), func(t *testing.T) { + echo, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatalf("listen: %v", err) + } + defer func() { _ = echo.Close() }() + observed := make(chan int, 1) + go func() { + buf := make([]byte, 1) + n, addr, err := echo.ReadFromUDP(buf) + if err == nil { + observed <- n + _, _ = echo.WriteToUDP(buf[:n], addr) + } + }() + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { + return net.DialUDP(network, nil, echo.LocalAddr().(*net.UDPAddr)) + }} + h := newUDPForwarderHarness(t, ft, time.Minute) + c := dialUDPThroughHarness(t, h.cli, dst, 5353) + _ = c.SetDeadline(time.Now().Add(3 * time.Second)) + if n, err := c.Write(nil); err != nil || n != 0 { + t.Fatalf("zero write = %d, %v", n, err) + } + select { + case n := <-observed: + if n != 0 { + t.Fatalf("upstream observed length %d, want 0", n) + } + case <-time.After(3 * time.Second): + t.Fatal("upstream did not observe zero-length datagram") + } + buf := make([]byte, 1) + if n, err := c.Read(buf); err != nil || n != 0 { + t.Fatalf("zero read = %d, %v", n, err) + } + }) + } +} + +func TestRunStackUDPForwarderIPv4RoundTrip(t *testing.T) { + echo := startUDPEcho(t) + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { return net.DialUDP(network, nil, echo) }} + h := newUDPForwarderHarness(t, ft, time.Minute) + dst := netip.MustParseAddr("192.0.2.53") + const dstPort = 5353 + payload := []byte("ipv4 survives") + c := dialUDPThroughHarness(t, h.cli, dst, dstPort) + _ = c.SetDeadline(time.Now().Add(3 * time.Second)) + if _, err := c.Write(payload); err != nil { + t.Fatalf("write: %v", err) + } + buf := make([]byte, 64) + n, err := c.Read(buf) + if err != nil || !slices.Equal(buf[:n], payload) { + t.Fatalf("read = %q, %v; want %q", buf[:n], err, payload) + } + raw := <-h.responses + if len(raw) != 28+len(payload) || raw[0]>>4 != 4 { + t.Fatalf("unexpected IPv4 packet length/version: len=%d", len(raw)) + } + ihl := int(raw[0]&0xf) * 4 + if got := netip.AddrFrom4([4]byte(raw[12:16])); got != dst { + t.Fatalf("source IP = %s, want %s", got, dst) + } + if got := netip.AddrFrom4([4]byte(raw[16:20])); got != netip.MustParseAddr("192.0.2.2") { + t.Fatalf("destination IP = %s", got) + } + if got := binary.BigEndian.Uint16(raw[ihl : ihl+2]); got != dstPort { + t.Fatalf("source port = %d, want %d", got, dstPort) + } + if !slices.Equal(raw[ihl+8:], payload) { + t.Fatalf("raw payload = %q, want %q", raw[ihl+8:], payload) + } + want := []string{"udp|192.0.2.53:5353"} + if got := ft.dialedAddrs(); !slices.Equal(got, want) { + t.Fatalf("dialed = %v, want %v", got, want) + } +} + +func TestRunStackUDPForwarderIPv6RoundTrip(t *testing.T) { + echo := startUDPEcho(t) + ft := &fakeTransport{dial: func(network, _ string) (net.Conn, error) { return net.DialUDP(network, nil, echo) }} + h := newUDPForwarderHarness(t, ft, time.Minute) + for _, dst := range []netip.Addr{netip.MustParseAddr("fd78::1234"), netip.MustParseAddr("2001:db8:781::53")} { + t.Run(dst.String(), func(t *testing.T) { + const dstPort = 5353 + payload := []byte("odd-length") + c := dialUDPThroughHarness(t, h.cli, dst, dstPort) + _ = c.SetDeadline(time.Now().Add(3 * time.Second)) + if _, err := c.Write(payload); err != nil { + t.Fatalf("write: %v", err) + } + buf := make([]byte, 64) + n, err := c.Read(buf) + if err != nil || !slices.Equal(buf[:n], payload) { + t.Fatalf("read = %q, %v; want %q", buf[:n], err, payload) + } + var raw []byte + select { + case raw = <-h.responses: + case <-time.After(3 * time.Second): + t.Fatal("timed out waiting for raw response") + } + host, portText, err := net.SplitHostPort(c.LocalAddr().String()) + if err != nil || host != runTunAddr6 { + t.Fatalf("client local address = %v, split error %v", c.LocalAddr(), err) + } + localPort, err := net.LookupPort("udp", portText) + if err != nil { + t.Fatalf("client local port: %v", err) + } + assertIPv6UDPResponse(t, raw, dst, netip.MustParseAddr(runTunAddr6), dstPort, uint16(localPort), payload) + }) + } + want := []string{"udp|[fd78::1234]:5353", "udp|[2001:db8:781::53]:5353"} + if got := ft.dialedAddrs(); !slices.Equal(got, want) { + t.Fatalf("dialed = %v, want %v", got, want) + } +} diff --git a/cmd/clawpatrol/run_linux.go b/cmd/clawpatrol/run_linux.go index d8ae541d..ad5c16b0 100644 --- a/cmd/clawpatrol/run_linux.go +++ b/cmd/clawpatrol/run_linux.go @@ -926,15 +926,12 @@ type netnsStep struct { // else lives on the TUN and duplicate-address detection would stall // the address for a second. // -// Deliberately NOT a v6 default route: the TUN bridge forwards TCP -// (v4+v6) and v4 UDP only, so a default route would advertise -// reachability for every AAAA destination while IPv6 UDP (QUIC/HTTP3) -// entering the TUN is silently dropped — a stall instead of the -// instant network-unreachable that lets clients fall back to IPv4. -// Scoping the route to the VIP prefix keeps fd78:: endpoints -// reachable (they are TCP by construction — DNS rides the v4 -// nameserver) and leaves every other v6 destination, TCP and UDP -// alike, failing fast to the v4 path. +// Deliberately NOT a v6 default route: IPv6 UDP is forwarded, but +// daemonTransport's connected net.Conn does not provide structured +// ICMPv6/Packet Too Big/PMTU error translation. A ::/0 route would +// therefore over-advertise reliable global IPv6/QUIC reachability. +// Scoping the route to the fd78::/64 VIP prefix preserves the intended +// tunnel-backed endpoint reachability without changing that contract. // // The v6 steps are optional: a host booted with ipv6.disable=1 can't // add them, and the v4 VIP path must keep working there — a failed