package tailcore import ( "encoding/json" "log" "net/netip" "sync" "tailscale.com/net/dns" "tailscale.com/util/dnsname" ) // MagicDNS — the piece that makes short names like `alpha` resolve. // // The resolver itself lives in the Tailscale core and answers on 100.100.100.100; netstack routes // queries to it, but only if a dns.Manager exists. Ours never created one, so that address answered // nothing and pointing the OS at it black-holed every lookup. See OFFTAIL_ANALYSIS_2026-08-02.md. // // The manager needs an OSConfigurator. On desktop that writes /etc/resolv.conf or talks to // NetworkManager; on mobile the OS applies DNS itself (VpnService.Builder / NEPacketTunnelNetworkSettings), // so the platform-specific ones are wrong here — manager_darwin.go would try to rewrite the phone's // resolv.conf. Hence this no-op. // // One method is NOT a no-op and is the whole reason this file is subtle: GetBaseConfig supplies the // UPSTREAM resolvers the tailnet resolver forwards non-tailnet queries to. Return nothing and only // tailnet names resolve while the rest of the internet goes dark — the exact failure we just spent an // evening removing. The platform hands them in via SetUpstreamDNS before Start. var ( upstreamMu sync.Mutex upstreamDNS []netip.Addr ) // SetUpstreamDNS gives the core the resolvers of the underlying network, as a JSON array of IP // strings. Call BEFORE Start. Mirrors SetAndroidInterfaces: gomobile-friendly, JSON in, nothing out. // // The platform already knows these — Android reads them off the non-VPN network's LinkProperties, // iOS gets them from the system — and the core cannot discover them itself once the tunnel is up, // because by then it would only see its own. func SetUpstreamDNS(jsonArray string) { var ips []string if err := json.Unmarshal([]byte(jsonArray), &ips); err != nil { log.Printf("[tailcore] SetUpstreamDNS: %v", err) return } var addrs []netip.Addr for _, s := range ips { if a, err := netip.ParseAddr(s); err == nil { addrs = append(addrs, a) } } upstreamMu.Lock() upstreamDNS = addrs upstreamMu.Unlock() log.Printf("[tailcore] upstream DNS = %v", addrs) } func baseResolvers() []netip.Addr { upstreamMu.Lock() defer upstreamMu.Unlock() return append([]netip.Addr(nil), upstreamDNS...) } // mobileOSConfigurator applies nothing: the OS already has the DNS settings, handed to it by // VpnService.Builder / NEPacketTunnelNetworkSettings alongside the routes. type mobileOSConfigurator struct{} func (mobileOSConfigurator) SetDNS(dns.OSConfig) error { return nil } // False: we cannot split DNS at the OS level on mobile, so the core must handle every query itself — // answering tailnet names and forwarding the rest to the base config below. Claiming true would have // the core assume the OS routes only tailnet names to it, and everything else would be dropped. func (mobileOSConfigurator) SupportsSplitDNS() bool { return false } func (mobileOSConfigurator) GetBaseConfig() (dns.OSConfig, error) { return dns.OSConfig{Nameservers: baseResolvers()}, nil } func (mobileOSConfigurator) Close() error { return nil } // searchDomainsFor is a convenience for callers that want the tailnet suffix as a search domain. func searchDomainsFor(suffix string) []dnsname.FQDN { if suffix == "" { return nil } f, err := dnsname.ToFQDN(suffix) if err != nil { return nil } return []dnsname.FQDN{f} }