From eb8756bbe5fa899b115335c51a2ccce572299e48 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 9 Jul 2026 16:59:54 +0700 Subject: [PATCH] fix: treat 464XLAT CLAT source as local, not WAN On networks using 464XLAT (common on IPv6-only cellular carriers and iPhone hotspots), the local machine's DNS queries can reach ctrld's listener with a source address in the RFC 7335 IPv4 Service Continuity Prefix (192.0.0.0/29, e.g. 192.0.0.2 on the CLAT/host side). isWanClient classified 192.0.0.x as a WAN client, so with allow_wan_clients unset (the default) the query was refused, breaking DNS resolution entirely on the affected connection even though it originated from the local host. Recognize the IPv4 Service Continuity Prefix as a local range, mirroring the existing CGNAT special case: - Add ipv4ServiceContinuityPrefix (192.0.0.0/29) and an isServiceContinuityAddr helper (single definition, reused by both call sites). - isWanClient excludes the range, so 464XLAT/CLAT queries are served normally. - isPrivatePtrLookup treats the range as private so reverse lookups are handled consistently. Scoped to 192.0.0.0/29 (the exact 464XLAT range); this does not weaken allow_wan_clients since no globally routable remote client can appear from it. --- cmd/cli/dns_proxy.go | 20 ++++++++++++++++++-- cmd/cli/dns_proxy_test.go | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 040d468..5b8955e 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -1523,7 +1523,8 @@ func isPrivatePtrLookup(m *dns.Msg) bool { return addr.IsPrivate() || addr.IsLoopback() || addr.IsLinkLocalUnicast() || - tsaddr.CGNATRange().Contains(addr) + tsaddr.CGNATRange().Contains(addr) || + isServiceContinuityAddr(addr) } } return false @@ -1561,6 +1562,20 @@ func isLanHostname(name string) bool { strings.HasSuffix(name, ".local") } +// ipv4ServiceContinuityPrefix is the RFC 7335 IPv4 Service Continuity Prefix +// (192.0.0.0/29), used by the CLAT in 464XLAT/DS-Lite transition setups. On such +// networks (common on IPv6-only cellular carriers and iPhone hotspots) the local +// machine's DNS queries reach ctrld with a source in this range (e.g. 192.0.0.2), +// so they must be treated as local, not WAN. Go's netip.IsPrivate does not cover +// this range — the same reason the CGNAT range is special-cased below. See #552. +var ipv4ServiceContinuityPrefix = netip.MustParsePrefix("192.0.0.0/29") + +// isServiceContinuityAddr reports whether ip is in the RFC 7335 IPv4 Service +// Continuity Prefix (464XLAT/DS-Lite CLAT). +func isServiceContinuityAddr(ip netip.Addr) bool { + return ipv4ServiceContinuityPrefix.Contains(ip) +} + // isWanClient reports whether the input is a WAN address. func isWanClient(na net.Addr) bool { var ip netip.Addr @@ -1571,7 +1586,8 @@ func isWanClient(na net.Addr) bool { !ip.IsPrivate() && !ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast() && - !tsaddr.CGNATRange().Contains(ip) + !tsaddr.CGNATRange().Contains(ip) && + !isServiceContinuityAddr(ip) } // resolveInternalDomainTestQuery resolves internal test domain query, returning the answer to the caller. diff --git a/cmd/cli/dns_proxy_test.go b/cmd/cli/dns_proxy_test.go index 5095552..9b8b164 100644 --- a/cmd/cli/dns_proxy_test.go +++ b/cmd/cli/dns_proxy_test.go @@ -491,6 +491,8 @@ func Test_isPrivatePtrLookup(t *testing.T) { {"CGNAT", newDnsMsgPtr("100.66.27.28", t), true}, {"Loopback", newDnsMsgPtr("127.0.0.1", t), true}, {"Link Local Unicast", newDnsMsgPtr("fe80::69f6:e16e:8bdb:433f", t), true}, + // RFC 7335 IPv4 Service Continuity Prefix (464XLAT/DS-Lite CLAT), see #552. + {"464XLAT CLAT host", newDnsMsgPtr("192.0.0.2", t), true}, {"Public IP", newDnsMsgPtr("8.8.8.8", t), false}, } for _, tc := range tests { @@ -538,6 +540,11 @@ func Test_isWanClient(t *testing.T) { {"CGNAT", &net.UDPAddr{IP: net.ParseIP("100.66.27.28")}, false}, {"Loopback", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}, false}, {"Link Local Unicast", &net.UDPAddr{IP: net.ParseIP("fe80::69f6:e16e:8bdb:433f")}, false}, + // RFC 7335 IPv4 Service Continuity Prefix (464XLAT/DS-Lite CLAT), see #552. + {"464XLAT PLAT side", &net.UDPAddr{IP: net.ParseIP("192.0.0.1")}, false}, + {"464XLAT CLAT host", &net.UDPAddr{IP: net.ParseIP("192.0.0.2")}, false}, + // Outside the /29 but inside 192.0.0.0/24: still WAN (fix is scoped to /29). + {"192.0.0.0/24 outside /29", &net.UDPAddr{IP: net.ParseIP("192.0.0.100")}, true}, {"Public", &net.UDPAddr{IP: net.ParseIP("8.8.8.8")}, true}, } for _, tc := range tests {