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 {