Skip to content

Answer fake DNS queries directly from the tunnel path - #544

Closed
Stanislav-Povolotsky wants to merge 6 commits into
xjasonlyu:mainfrom
Stanislav-Povolotsky:fakedns-tunnel-hijack
Closed

Stanislav-Povolotsky wants to merge 6 commits into
xjasonlyu:mainfrom
Stanislav-Povolotsky:fakedns-tunnel-hijack

Conversation

@Stanislav-Povolotsky

Copy link
Copy Markdown

Summary

dns.ReCreateServer (added by the fake DNS feature) binds a real OS-level
UDP socket for -fakedns-listen-addr. That only works when DNS queries
reach it outside the tunnel — e.g. a desktop system resolver pointed at
127.0.0.1. On platforms where the VPN/TUN implementation captures all
app traffic, including DNS, into the TUN device (notably Android's
VpnService), queries to the configured fake DNS address never reach that
socket, so fake DNS silently does nothing there.

This PR adds a second, tunnel-native path: tunnel/udp.go and
tunnel/tcp.go now recognize UDP/TCP flows addressed to the configured fake
DNS listen address and answer them locally via the same fake IP pool
(dns.HandleQuery), instead of dialing out through the proxy. This works
independently of whether the real OS-level socket bind in ReCreateServer
succeeded, so:

  • Platforms that route DNS around the tunnel (e.g. desktop, resolver set to
    127.0.0.1:*) keep working exactly as before, via the real socket.
  • Platforms that route DNS through the tunnel (e.g. Android VpnService,
    where net.ListenUDP on the fake DNS address isn't even a real bindable
    local interface) now get correct answers too, via the new in-tunnel path.

Both paths share the same DNS message handling and fake IP pool — the
tunnel path just unpacks/repacks the raw query itself instead of going
through miekg/dns's D.Server.

What changed

  • dns/fakedns.go: track the resolved fakeListenAddr (guarded by the
    existing mutex); add IsFakeDNSQuery(dst netip.AddrPort) bool and
    HandleQuery(data []byte) ([]byte, error) for callers that intercept raw
    DNS traffic directly from the tunnel.
  • dns/server.go: ReCreateServer now records fakeListenAddr as soon as
    the address resolves, before attempting the real socket bind — so it's
    set even when the bind can't succeed (e.g. binding a virtual/non-local
    address on Android). Normalizes the address with .Unmap(), since
    net.ResolveUDPAddr(...).AddrPort() can return an IPv4-in-6 form while
    addresses coming from the gVisor-based tunnel are always plain IPv4 —
    without this, IsFakeDNSQuery's == comparison would never match.
  • tunnel/udp.go / tunnel/tcp.go: before dialing out, check
    dns.IsFakeDNSQuery(metadata.DestinationAddrPort()); if true, hand the
    connection to a new handleFakeDNSUDP/handleFakeDNSTCP that answers
    locally instead. DNS-over-TCP follows RFC 1035 §4.2.2 (2-byte
    big-endian length prefix per message).
  • dns/fakedns_test.go: new tests covering IsFakeDNSQuery (including the
    IPv4-in-6 normalization) and the HandleQuery round trip (query → fake
    IP → LookBack resolves back to the original hostname).

Stanislav-Povolotsky and others added 6 commits July 11, 2026 01:54
This commit implements fake DNS.

Fake DNS implements a UDP listener DNS A record queries on port 53. It
replies with an unused IP address from an address pool, 198.18.0.0/15 by
default. When obtaining a new address from the pool, tun2socks needs to
memorize which name the address belongs to, so that when a client
connects to the address, it can instruct the proxy to connect to the
FQDN. To implement this IP to name mapping, the FakeIP module from clash
is used.
- fakeip.Pool.get(): fix off-by-one in the address-cycling modulo that
  made the pool's top address unreachable (collapsed a /30 pool to a
  single address for every hostname).
- tunnel/udp.go: fix symmetric-NAT source check that was permanently
  disabled for every UDP flow, not just fake-DNS ones, because
  net.ParseIP() was called on a "host:port" string and always failed.
- dns package: guard fakePool/fakeDNSenabled with a mutex instead of
  unsynchronized globals, close the nil-pool panic window on startup,
  and drop the dead TTL=600 record header immediately overwritten by
  TTL=1.
- engine: validate fake DNS config and build its pool before bringing
  up the TUN device/stack, so a bad config can't leave a half-started
  stack running; wire dns.DisableFakeDNS() into engine.Stop().
Remote DNS intercepts UDP DNS queries for A records on port 53. It replies with an unused IP address from an address pool, 198.18.0.0/15 by default. When obtaining a new address from the pool, tun2socks needs to memorize which name the address belongs to, so that when a client connects to the address, it can instruct the proxy to connect to the FQDN. To implement this IP to name mapping, ttlcache is used. To prevent using multiple addresses for the same name, ttlcache is also used to implement a name to IP mapping. If an IP address is already cached for a name, that address is returned instread. When building a connection, the connection metadata is inspected and if the destination address is associated with a DNS name, the proxy is instructed to use this name instead of the IP address.
ReCreateServer's fake DNS listener binds a real OS-level UDP socket,
which only receives traffic on platforms where DNS queries reach it
outside the VPN tunnel (e.g. a desktop system resolver pointed at
127.0.0.1). On platforms like Android, VpnService captures all app
traffic -- including DNS -- into the TUN device, so it never reaches
that socket.

Recognize UDP/TCP flows addressed to the configured fake DNS listen
address in tunnel/udp.go and tunnel/tcp.go, and answer them locally via
the same fake IP pool (dns.HandleQuery), independent of whether the
real socket bind succeeded. This mirrors how ordinary TCP/UDP flows are
already intercepted in the tunnel, and lets fake DNS work wherever the
TUN device captures the traffic, not just when a real loopback socket
is reachable.
@Stanislav-Povolotsky

Copy link
Copy Markdown
Author

Missed the repository again, sorry

@Stanislav-Povolotsky
Stanislav-Povolotsky deleted the fakedns-tunnel-hijack branch July 12, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants