From e2c17df7969fac4c96ba9767bf122211768c7ad7 Mon Sep 17 00:00:00 2001 From: yusuke8h Date: Sun, 19 Jul 2026 19:45:51 +0900 Subject: [PATCH] fix: run (Linux): rewrite child UDP/53 to :53 in the daemon as belt-and-suspenders (#773) --- cmd/clawpatrol/daemon_session_linux.go | 13 ++++++++++++- cmd/clawpatrol/daemon_session_linux_test.go | 17 +++++++++++++++++ cmd/clawpatrol/daemon_transport_tsnet_linux.go | 10 ++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cmd/clawpatrol/daemon_session_linux.go b/cmd/clawpatrol/daemon_session_linux.go index ee073ca8..ce7e3e7b 100644 --- a/cmd/clawpatrol/daemon_session_linux.go +++ b/cmd/clawpatrol/daemon_session_linux.go @@ -193,6 +193,17 @@ func newRunUDPProtocolHandler(s *stack.Stack, handler udp.ForwarderHandler) func } } +type udpDNSGateway interface { + udpDNSGatewayAddr() netip.Addr +} + +func udpDialAddr(transport daemonTransport, dstIP string, dstPort uint16) string { + if gateway, ok := transport.(udpDNSGateway); ok && dstPort == 53 { + dstIP = gateway.udpDNSGatewayAddr().String() + } + return net.JoinHostPort(dstIP, strconv.Itoa(int(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 { @@ -214,7 +225,7 @@ func newTransportUDPProtocolHandler(ctx context.Context, s *stack.Stack, transpo return true } local := gonet.NewUDPConn(&wq, ep) - dstAddr := net.JoinHostPort(id.LocalAddress.String(), strconv.Itoa(int(id.LocalPort))) + dstAddr := udpDialAddr(transport, id.LocalAddress.String(), id.LocalPort) go func() { dialCtx, cancel := context.WithTimeout(ctx, transportDialTimeout) defer cancel() diff --git a/cmd/clawpatrol/daemon_session_linux_test.go b/cmd/clawpatrol/daemon_session_linux_test.go index 7cde7aa5..88045f8f 100644 --- a/cmd/clawpatrol/daemon_session_linux_test.go +++ b/cmd/clawpatrol/daemon_session_linux_test.go @@ -149,6 +149,23 @@ func TestRunUDPProtocolHandlerFlowLimitRejectionDoesNotClonePacket(t *testing.T) } } +func TestUDPDialAddrRewritesTSnetDNS(t *testing.T) { + tsnet := &tsnetTransport{gatewayAddr: netip.MustParseAddr("100.64.0.1")} + if got, want := udpDialAddr(tsnet, "8.8.8.8", 53), "100.64.0.1:53"; got != want { + t.Fatalf("udpDialAddr(tsnet DNS) = %q, want %q", got, want) + } + if got, want := udpDialAddr(tsnet, "8.8.8.8", 443), "8.8.8.8:443"; got != want { + t.Fatalf("udpDialAddr(tsnet non-DNS) = %q, want %q", got, want) + } + plain := &fakeTransport{} + if got, want := udpDialAddr(plain, "8.8.8.8", 53), "8.8.8.8:53"; got != want { + t.Fatalf("udpDialAddr(non-tsnet DNS) = %q, want %q", got, want) + } + if got, want := udpDialAddr(plain, "2001:db8::53", 53), "[2001:db8::53]:53"; got != want { + t.Fatalf("udpDialAddr(non-tsnet IPv6 DNS) = %q, want %q", got, want) + } +} + // 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) { diff --git a/cmd/clawpatrol/daemon_transport_tsnet_linux.go b/cmd/clawpatrol/daemon_transport_tsnet_linux.go index bcb3ded9..fb947699 100644 --- a/cmd/clawpatrol/daemon_transport_tsnet_linux.go +++ b/cmd/clawpatrol/daemon_transport_tsnet_linux.go @@ -26,6 +26,7 @@ import ( type tsnetTransport struct { s *tsnet.Server localAddr netip.Addr + gatewayAddr netip.Addr bootWarning string } @@ -33,9 +34,10 @@ func (t *tsnetTransport) Dial(ctx context.Context, network, addr string) (net.Co return t.s.Dial(ctx, network, addr) } -func (t *tsnetTransport) LocalAddr() netip.Addr { return t.localAddr } -func (t *tsnetTransport) BootWarning() string { return t.bootWarning } -func (t *tsnetTransport) Close() error { return t.s.Close() } +func (t *tsnetTransport) LocalAddr() netip.Addr { return t.localAddr } +func (t *tsnetTransport) udpDNSGatewayAddr() netip.Addr { return t.gatewayAddr } +func (t *tsnetTransport) BootWarning() string { return t.bootWarning } +func (t *tsnetTransport) Close() error { return t.s.Close() } // startTsnetTransport reads persisted join state (auth-key, control-url, // gateway-ip), starts a tsnet.Server, waits for it to come up, points @@ -126,7 +128,7 @@ func startTsnetTransport() (daemonTransport, error) { // daemon restart. daemonRegisterTsnetPeer(s, tsIP) - return &tsnetTransport{s: s, localAddr: tsIP, bootWarning: bootWarning}, nil + return &tsnetTransport{s: s, localAddr: tsIP, gatewayAddr: gwIP, bootWarning: bootWarning}, nil } // daemonRegisterTsnetPeer POSTs this daemon's tsnet IP to the