From 6bc0d19f25774a055c9bf9ce36c5fd0ddb1c2f44 Mon Sep 17 00:00:00 2001 From: FExB17 Date: Tue, 22 Sep 2026 20:33:08 +0200 Subject: [PATCH] android: dial peer DNS upstreams through netstack Without UseNetstackForIP, forwarder.dialUDP uses a kernel UDP socket to peer Tailscale IPs. The VPN captures that socket so replies never match the outstanding query and lookups time out (tailscale/tailscale#20983). Wire the same PeerForIP netstack dial path as Linux tailscaled, and set CheckLocalTransportEndpoints so replies reach those sockets while ProcessLocalIPs stays false. Fixes tailscale/tailscale#20983 Signed-off-by: FExB17 --- libtailscale/backend.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/libtailscale/backend.go b/libtailscale/backend.go index bfd0a110da..8927572c9f 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -9,7 +9,9 @@ import ( "errors" "fmt" "log" + "net" "net/http" + "net/netip" "os" "path/filepath" "reflect" @@ -356,6 +358,10 @@ func (a *App) newBackend(dataDir string, appCtx AppContext, store *stateStore, sys.Set(ns) ns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up ns.ProcessSubnets = true // for Android-being-an-exit-node support + // Replies to sockets we dial below are destined for this node's Tailscale + // IP. ProcessLocalIPs is false, so without this flag those replies are + // handed to the kernel and the userspace socket never sees them. + ns.CheckLocalTransportEndpoints = true sys.NetstackRouter.Set(true) if w, ok := sys.Tun.GetOK(); ok { w.Start() @@ -369,6 +375,29 @@ func (a *App) newBackend(dataDir string, appCtx AppContext, store *stateStore, engine.Close() return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err) } + // DNS upstreams on a peer's Tailscale IP are dialed by this process. A + // kernel socket to that IP is captured by the VPN service and the reply + // is never matched, so lookups time out (tailscale/tailscale#20983). + // Dial those addresses through netstack instead. Do not return the + // *gonet conn directly: a nil pointer would become a non-nil interface. + dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) { + tcpConn, err := ns.DialContextTCP(ctx, dst) + if err != nil { + return nil, err + } + return tcpConn, nil + } + dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) { + udpConn, err := ns.DialContextUDP(ctx, dst) + if err != nil { + return nil, err + } + return udpConn, nil + } + dialer.UseNetstackForIP = func(ip netip.Addr) bool { + _, ok := lb.PeerForIP(ip) + return ok + } if err := ns.Start(lb); err != nil { return nil, fmt.Errorf("startNetstack: %w", err) }