package cli import ( "context" "net" "net/netip" "sync" "time" "github.com/miekg/dns" "tailscale.com/net/netmon" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" ) // DNS64 synthesis for IPv6-only networks WITHOUT client-side 464XLAT (no // CLAT). On such networks the carrier's DNS64 resolver is load-bearing: it // synthesizes AAAA records mapping IPv4-only destinations into the NAT64 // prefix, and there is no CLAT interface to carry real IPv4 traffic. When // ctrld answers with genuine A records there, IPv4-only destinations become // unreachable โ€” DNS resolves but connectivity fails (issue companion to // #533; tethering is unaffected because Apple/Android always provide CLAT). // // ctrld therefore performs its own RFC 6147-style synthesis after filtering: // when the network is IPv6-only with no CLAT and a NAT64 prefix is known, // an AAAA query whose (policy-approved) answer contains no AAAA records is // re-resolved as an A query through the same upstream, and the A records are // mapped into the NAT64 prefix. Blocked answers are never synthesized โ€” // synthesis runs on the answer the policy engine already approved. // // NAT64 prefix discovery uses RFC 7050: resolve AAAA for ipv4only.arpa // through the network's own resolvers and derive the prefix from the // embedded well-known IPv4 addresses (192.0.0.170/171). PREF64 router // advertisements (RFC 8781) are not parsed; RFC 7050 covers the same // networks without OS-specific RA plumbing. const ( // dns64RecheckInterval bounds how often network state (CLAT presence, // IPv4 availability, NAT64 prefix) is re-evaluated. dns64RecheckInterval = 5 * time.Minute // dns64WellKnownName is the RFC 7050 discovery name. dns64WellKnownName = "ipv4only.arpa." // dns64DiscoverTimeout bounds one background discovery attempt. dns64DiscoverTimeout = 5 * time.Second ) // rfc7050WellKnown are the IPv4 addresses embedded in ipv4only.arpa AAAA // answers on DNS64 networks (RFC 7050). var rfc7050WellKnown = []netip.Addr{ netip.AddrFrom4([4]byte{192, 0, 0, 170}), netip.AddrFrom4([4]byte{192, 0, 0, 171}), } var dns64WellKnownPrefix = netip.MustParsePrefix("64:ff9b::/96") // clatPrefix is the RFC 7335 IPv4 service-continuity prefix used by // client-side translators (CLAT). var clatPrefix = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 0, 0}), 29) type dns64State struct { mu sync.Mutex checkedAt time.Time active bool // network is v6-only, no CLAT, prefix known prefix netip.Prefix // discovered NAT64 prefix (/96) discovering bool generation uint64 } // dns64NetworkClassFn is a seam for tests. var dns64NetworkClassFn = currentDNS64NetworkClass func addrFromNetAddr(a net.Addr) (netip.Addr, bool) { var ip net.IP switch v := a.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP default: return netip.Addr{}, false } nip, ok := netip.AddrFromSlice(ip) if !ok { return netip.Addr{}, false } return nip.Unmap(), true } // dns64NetworkClass classifies the host addressing state from interface // addresses. Only IPv4 on the default-route interface counts as usable, so // RFC1918 addresses owned by Docker, Parallels, VMware, and similar virtual // interfaces do not disable DNS64. CLAT is detected across all interfaces. func dns64NetworkClass(defaultRouteAddrs, allAddrs []net.Addr) (hasUsableIPv4, hasCLAT bool) { for _, a := range allAddrs { nip, ok := addrFromNetAddr(a) if !ok || !nip.Is4() { continue } if clatPrefix.Contains(nip) { hasCLAT = true } } for _, a := range defaultRouteAddrs { nip, ok := addrFromNetAddr(a) if !ok || !nip.Is4() || clatPrefix.Contains(nip) { continue } if nip.IsLoopback() || nip.IsLinkLocalUnicast() || nip.IsUnspecified() { continue } hasUsableIPv4 = true } return hasUsableIPv4, hasCLAT } func currentDNS64NetworkClass() (hasUsableIPv4, hasCLAT bool, err error) { defaultRouteInterface, err := netmon.DefaultRouteInterface() if err != nil { return false, false, err } iface, err := net.InterfaceByName(defaultRouteInterface) if err != nil { return false, false, err } defaultRouteAddrs, err := iface.Addrs() if err != nil { return false, false, err } allAddrs, err := net.InterfaceAddrs() if err != nil { return false, false, err } hasUsableIPv4, hasCLAT = dns64NetworkClass(defaultRouteAddrs, allAddrs) return hasUsableIPv4, hasCLAT, nil } // nat64PrefixFromAnswer derives the NAT64 prefix from an ipv4only.arpa AAAA // answer per RFC 7050: find an AAAA embedding a well-known IPv4 address in // its last 4 bytes and take the leading /96. func nat64PrefixFromAnswer(answer *dns.Msg) (netip.Prefix, bool) { if answer == nil { return netip.Prefix{}, false } for _, rr := range answer.Answer { aaaa, ok := rr.(*dns.AAAA) if !ok { continue } v6, ok := netip.AddrFromSlice(aaaa.AAAA.To16()) if !ok || v6.Is4() { continue } b := v6.As16() embedded := netip.AddrFrom4([4]byte{b[12], b[13], b[14], b[15]}) for _, wk := range rfc7050WellKnown { if embedded == wk { var p [16]byte copy(p[:12], b[:12]) return netip.PrefixFrom(netip.AddrFrom16(p), 96), true } } } return netip.Prefix{}, false } // synthesizeAAAAFromA returns a copy of aAnswer converted into an AAAA // answer for the original AAAA request: every A record is mapped into the // NAT64 prefix; other records (CNAMEs etc.) are preserved. func synthesizeAAAAFromA(req *dns.Msg, aAnswer *dns.Msg, prefix netip.Prefix) *dns.Msg { if aAnswer == nil { return nil } out := aAnswer.Copy() out.SetReply(req) out.Rcode = aAnswer.Rcode out.Compress = true answers := make([]dns.RR, 0, len(aAnswer.Answer)) pb := prefix.Addr().As16() for _, rr := range aAnswer.Answer { a, ok := rr.(*dns.A) if !ok { // Preserve CNAME chain records unchanged. answers = append(answers, dns.Copy(rr)) continue } v4 := a.A.To4() if v4 == nil { continue } v4Addr, ok := netip.AddrFromSlice(v4) if !ok || !v4Addr.IsGlobalUnicast() || (prefix == dns64WellKnownPrefix && v4Addr.IsPrivate()) { continue } var b [16]byte copy(b[:12], pb[:12]) copy(b[12:], v4) aaaa := &dns.AAAA{ Hdr: dns.RR_Header{ Name: a.Hdr.Name, Rrtype: dns.TypeAAAA, Class: a.Hdr.Class, Ttl: a.Hdr.Ttl, }, AAAA: net.IP(b[:]), } answers = append(answers, aaaa) } out.Answer = answers return out } // answerHasAAAA reports whether the answer section contains any AAAA record. func answerHasAAAA(answer *dns.Msg) bool { if answer == nil { return false } for _, rr := range answer.Answer { if _, ok := rr.(*dns.AAAA); ok { return true } } return false } // dns64Eligible reports whether an answer qualifies for DNS64 synthesis: // an AAAA query answered NOERROR with no AAAA records. NXDOMAIN and error // rcodes are never synthesized (RFC 6147 ยง5.1.2: the name genuinely does // not exist or the query failed). func dns64Eligible(req, answer *dns.Msg) bool { if req == nil || answer == nil || len(req.Question) == 0 || req.CheckingDisabled { return false } if req.Question[0].Qtype != dns.TypeAAAA { return false } if answer.Rcode != dns.RcodeSuccess { return false } return !answerHasAAAA(answer) } // dns64Active reports whether synthesis should currently run, re-evaluating // network class and (if needed) kicking off background prefix discovery at // most every dns64RecheckInterval. func (p *prog) dns64Active() bool { s := &p.dns64 s.mu.Lock() if time.Since(s.checkedAt) < dns64RecheckInterval { active := s.active s.mu.Unlock() return active } s.checkedAt = time.Now() generation := s.generation s.mu.Unlock() hasV4, hasCLAT, err := dns64NetworkClassFn() s.mu.Lock() defer s.mu.Unlock() if generation != s.generation { return s.active } if err != nil { s.active = false return false } if hasV4 || hasCLAT { // Dual-stack or 464XLAT: the OS/CLAT handles IPv4 reachability; // synthesis would be unnecessary (and on CLAT networks, harmful โ€” // real A records are preferable so traffic uses the CLAT). s.generation++ s.active = false s.prefix = netip.Prefix{} s.discovering = false return false } s.active = s.prefix.IsValid() if !s.discovering { s.discovering = true generation := s.generation go p.discoverNAT64Prefix(generation) } return s.active } func (p *prog) activeDNS64Prefix() (netip.Prefix, bool) { if !p.dns64Active() { return netip.Prefix{}, false } p.dns64.mu.Lock() defer p.dns64.mu.Unlock() return p.dns64.prefix, p.dns64.prefix.IsValid() } func dns64CacheVariant(prefix netip.Prefix) string { return "dns64:" + prefix.String() } func dns64CacheKey(msg *dns.Msg, upstream string, prefix netip.Prefix) dnscache.Key { return dnscache.NewVariantKey(msg, upstream, dns64CacheVariant(prefix)) } func (p *prog) resetDNS64State() { p.dns64.mu.Lock() p.dns64.generation++ p.dns64.checkedAt = time.Time{} p.dns64.active = false p.dns64.prefix = netip.Prefix{} p.dns64.discovering = false p.dns64.mu.Unlock() } func dns64RouteStateChanged(delta *netmon.ChangeDelta) bool { if delta == nil || delta.Old == nil || delta.New == nil { return true } if delta.Old.DefaultRouteInterface != delta.New.DefaultRouteInterface || delta.Old.HaveV4 != delta.New.HaveV4 || delta.Old.HaveV6 != delta.New.HaveV6 { return true } if dns64StateHasCLAT(delta.Old) != dns64StateHasCLAT(delta.New) { return true } iface := delta.New.DefaultRouteInterface oldPrefixes := delta.Old.InterfaceIPs[iface] newPrefixes := delta.New.InterfaceIPs[iface] if len(oldPrefixes) != len(newPrefixes) { return true } newPrefixSet := make(map[netip.Prefix]struct{}, len(newPrefixes)) for _, prefix := range newPrefixes { newPrefixSet[prefix] = struct{}{} } for _, prefix := range oldPrefixes { if _, ok := newPrefixSet[prefix]; !ok { return true } } return false } func dns64StateHasCLAT(state *netmon.State) bool { for _, prefixes := range state.InterfaceIPs { for _, prefix := range prefixes { if clatPrefix.Contains(prefix.Addr().Unmap()) { return true } } } return false } func (p *prog) handleDNS64NetworkChange(delta *netmon.ChangeDelta, major bool) { if major || dns64RouteStateChanged(delta) { p.resetDNS64State() } } func (p *prog) storeDiscoveredNAT64Prefix(generation uint64, prefix netip.Prefix) bool { p.dns64.mu.Lock() defer p.dns64.mu.Unlock() if p.dns64.generation != generation { return false } p.dns64.prefix = prefix p.dns64.active = true p.dns64.checkedAt = time.Now() return true } // discoverNAT64Prefix resolves ipv4only.arpa AAAA through the OS-discovered // resolvers (the network's own DNS64 resolver) and stores the derived // prefix. Runs in the background; failures leave synthesis inactive until // the next recheck window. func (p *prog) discoverNAT64Prefix(generation uint64) { defer func() { p.dns64.mu.Lock() if p.dns64.generation == generation { p.dns64.discovering = false } p.dns64.mu.Unlock() }() ctx, cancel := context.WithTimeout(context.Background(), dns64DiscoverTimeout) defer cancel() ctx = ctrld.LoggerCtx(ctx, mainLog.Load()) msg := new(dns.Msg) msg.SetQuestion(dns64WellKnownName, dns.TypeAAAA) resolver, err := ctrld.NewResolver(ctx, osUpstreamConfig) if err != nil { mainLog.Load().Debug().Err(err).Msg("dns64: could not create OS resolver for NAT64 discovery") return } answer, err := resolver.Resolve(ctx, msg) if err != nil { mainLog.Load().Debug().Err(err).Msg("dns64: NAT64 prefix discovery query failed") return } prefix, ok := nat64PrefixFromAnswer(answer) if !ok { mainLog.Load().Debug().Msg("dns64: no NAT64 prefix present (not a DNS64 network)") return } if !p.storeDiscoveredNAT64Prefix(generation, prefix) { return } mainLog.Load().Info().Msgf("dns64: discovered NAT64 prefix %s; enabling AAAA synthesis for IPv6-only network without CLAT", prefix) } // maybeDNS64 applies DNS64 synthesis to an already-filtered answer when the // network requires it. resolveA re-resolves the question as an A query // through the same upstream that produced the answer. func (p *prog) maybeDNS64(ctx context.Context, req *dns.Msg, answer *dns.Msg, resolveA func(*dns.Msg) *dns.Msg) (*dns.Msg, netip.Prefix) { if !dns64Eligible(req, answer) || !p.dns64Active() { return answer, netip.Prefix{} } p.dns64.mu.Lock() prefix := p.dns64.prefix generation := p.dns64.generation p.dns64.mu.Unlock() if !prefix.IsValid() { return answer, netip.Prefix{} } aReq := req.Copy() aReq.Question[0].Qtype = dns.TypeA aAnswer := resolveA(aReq) if aAnswer == nil || aAnswer.Rcode != dns.RcodeSuccess || !sameQuestion(aReq, aAnswer) { return answer, netip.Prefix{} } synth := synthesizeAAAAFromA(req, aAnswer, prefix) p.dns64.mu.Lock() current := p.dns64.active && p.dns64.generation == generation && p.dns64.prefix == prefix p.dns64.mu.Unlock() if !current { return answer, netip.Prefix{} } if synth == nil || !answerHasAAAA(synth) { // The companion A lookup completed successfully, so this passthrough // answer is definitive for the current prefix and may be cached in the // DNS64 variant to avoid repeating both upstream lookups. return answer, prefix } ctrld.Log(ctx, mainLog.Load().Debug(), "dns64: synthesized AAAA from A records via NAT64 prefix %s", prefix) return synth, prefix }