mirror of
https://github.com/tailscale/tailscale.git
synced 2026-07-15 10:03:09 -04:00
Every netmap change, including an incremental delta of a single peer, rebuilt the full MagicDNS state twice: dnsConfigForNetmap walked all peers to build the dns.Config.Hosts map, and resolver.SetConfig then walked that map again to build its reverse (PTR) index. On a tailnet with 10k peers that is a lot of garbage per delta. Instead, add a resolver.MagicDNSHosts hook, installed once by LocalBackend, that the quad-100 resolver consults on demand at query time. It is backed by nodeBackend's nodeByName, nodeByAddr, and peers indexes, which are already maintained incrementally as netmap deltas arrive. The subdomain-resolve capability check also moves to the hook (checking the node's CapMap at query time), so dns.Config's SubdomainHosts is no longer populated. dns.Config.Hosts remains for control's DNS.ExtraRecords, which are few and which feed the split-DNS decisions in dns.Manager's compileConfig, and on Windows it still carries every node's records because the hosts-file fallback path (compileHostEntries) needs the complete enumerable set. Those compileConfig decisions also consulted the per-node Hosts entries (hasHostsWithoutSplitDNSRoutes), so a new Config.MagicDNSHostsUnrouted bit preserves that signal now that node records are not listed: with MagicDNS names present but MagicDNS domain routing off, quad-100 stays in the OS resolver path. One small behavior change: reverse (PTR) lookups now also answer for node addresses whose forward records are filtered out by the IPv6-suppression rule (issue #1152), since nodeByAddr indexes all node addresses. Previously such addresses were absent from the pushed Hosts map and thus from the reverse index. Updates #12542 Updates tailscale/corp#43949 Change-Id: I63b99199c2b3b124c08cb8bbaea1f63165095294 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
196 lines
6.1 KiB
Go
196 lines
6.1 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"tailscale.com/feature/buildfeatures"
|
|
"tailscale.com/net/dns/resolver"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/wgengine"
|
|
)
|
|
|
|
// lookupPeerByIP returns the node public key for the peer that should
|
|
// handle traffic to the given IP address. It is installed as the
|
|
// [wgengine.Engine.SetPeerByIPPacketFunc] callback: exact node
|
|
// addresses hit the nodeByAddr fast path, and subnet routes and
|
|
// exit-node default routes fall back to the RouteManager's outbound
|
|
// table, so it stays correct under incremental netmap deltas.
|
|
//
|
|
// It is called by wireguard-go on every outbound packet (not cached),
|
|
// so it must be fast.
|
|
func (b *LocalBackend) lookupPeerByIP(ip netip.Addr) (key.NodePublic, bool) {
|
|
nb := b.currentNode()
|
|
if nid, ok := nb.NodeByAddr(ip); ok {
|
|
peer, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
return key.NodePublic{}, false
|
|
}
|
|
return peer.Key(), true
|
|
}
|
|
if pr, ok := nb.routeMgr.Outbound().Lookup(ip); ok {
|
|
return pr.Key, true
|
|
}
|
|
return key.NodePublic{}, false
|
|
}
|
|
|
|
// peerAllowedIPs returns the prefixes from which the peer with the
|
|
// given public key is currently allowed to originate traffic, or
|
|
// ok=false if the peer is unknown (or currently routable via no
|
|
// prefix at all). It is installed as the
|
|
// [wgengine.Engine.SetPeerConfigFunc] callback, backing wireguard-go's
|
|
// lazy peer creation and per-delta peer sync.
|
|
func (b *LocalBackend) peerAllowedIPs(k key.NodePublic) (_ []netip.Prefix, ok bool) {
|
|
return b.currentNode().PeerAllowedIPs(k)
|
|
}
|
|
|
|
// resolveMagicDNS resolves a MagicDNS hostname to the owning node's IP
|
|
// address, respecting the requested network address family ("tcp4",
|
|
// "tcp6", "tcp", etc.). It accepts peer FQDNs ("foo.tail-scale.ts.net"),
|
|
// short names ("foo"), and DNS.ExtraRecords entries (service VIPs).
|
|
// The hostname must be lowercase with no trailing dot. It is installed
|
|
// as the [tsdial.Dialer.SetResolveMagicDNS] callback.
|
|
func (b *LocalBackend) resolveMagicDNS(hostname, network string) (_ netip.Addr, ok bool) {
|
|
nb := b.currentNode()
|
|
if nid, ok := nb.NodeByName(hostname); ok {
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
b.logf("[unexpected] resolveMagicDNS: NodeByName(%q) returned node %v but NodeByID failed", hostname, nid)
|
|
return netip.Addr{}, false
|
|
}
|
|
if ip, ok := nodeAddrForNetwork(n, network); ok {
|
|
return ip, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
if ip, ok := nb.ExtraDNSByName(hostname); ok && addrFamilyMatch(ip, network) {
|
|
return ip, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// magicDNSHosts implements [resolver.MagicDNSHosts] on top of the
|
|
// current nodeBackend's live node indexes, so the quad-100 resolver
|
|
// pulls each MagicDNS answer on demand rather than LocalBackend
|
|
// pushing a Hosts map of every node into it on every netmap change.
|
|
// It is installed once at LocalBackend construction; going through
|
|
// currentNode makes profile switches take effect immediately.
|
|
type magicDNSHosts struct{ b *LocalBackend }
|
|
|
|
func (m magicDNSHosts) LookupHost(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
|
|
if !buildfeatures.HasDNS {
|
|
return nil, false
|
|
}
|
|
return m.b.currentNode().magicDNSHostAddrs(fqdn)
|
|
}
|
|
|
|
func (m magicDNSHosts) LookupPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
|
|
if !buildfeatures.HasDNS {
|
|
return "", false
|
|
}
|
|
return m.b.currentNode().magicDNSPTR(ip)
|
|
}
|
|
|
|
func (m magicDNSHosts) SubdomainHost(fqdn dnsname.FQDN) bool {
|
|
if !buildfeatures.HasDNS {
|
|
return false
|
|
}
|
|
return m.b.currentNode().magicDNSSubdomainHost(fqdn)
|
|
}
|
|
|
|
var _ resolver.MagicDNSHosts = magicDNSHosts{}
|
|
|
|
// nodeAddrForNetwork returns the best address from n for the given
|
|
// network ("tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"). For
|
|
// unqualified networks ("tcp", "udp"), it prefers IPv4.
|
|
func nodeAddrForNetwork(n tailcfg.NodeView, network string) (_ netip.Addr, ok bool) {
|
|
addrs := n.Addresses()
|
|
if addrs.Len() == 0 {
|
|
return netip.Addr{}, false
|
|
}
|
|
want4 := strings.HasSuffix(network, "4")
|
|
want6 := strings.HasSuffix(network, "6")
|
|
var v6 netip.Addr
|
|
for _, pfx := range addrs.All() {
|
|
ip := pfx.Addr()
|
|
if want4 && ip.Is4() {
|
|
return ip, true
|
|
}
|
|
if want6 && ip.Is6() {
|
|
return ip, true
|
|
}
|
|
if !want4 && !want6 {
|
|
if ip.Is4() {
|
|
return ip, true
|
|
}
|
|
if !v6.IsValid() {
|
|
v6 = ip
|
|
}
|
|
}
|
|
}
|
|
if v6.IsValid() {
|
|
return v6, true
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// addrFamilyMatch reports whether ip is compatible with the requested
|
|
// network address family.
|
|
func addrFamilyMatch(ip netip.Addr, network string) bool {
|
|
if strings.HasSuffix(network, "4") {
|
|
return ip.Is4()
|
|
}
|
|
if strings.HasSuffix(network, "6") {
|
|
return ip.Is6()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// PeerForIP returns which peer is responsible for a given IP address.
|
|
// Despite the name, it can also return the self node (with IsSelf set).
|
|
// It handles both Tailscale IPs (returning the owning peer or self) and
|
|
// non-Tailscale addresses like subnet-routed IPs or exit-node global
|
|
// internet IPs (returning whichever peer would route that traffic).
|
|
// It is installed as the [wgengine.Engine.SetPeerForIPFunc] callback,
|
|
// serving the engine's internal cold-path lookups (Ping, TSMP, pendopen
|
|
// diagnostics).
|
|
func (b *LocalBackend) PeerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool) {
|
|
nb := b.currentNode()
|
|
|
|
if tsaddr.IsTailscaleIP(ip) {
|
|
if nid, ok := nb.NodeByAddr(ip); ok {
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
b.logf("[unexpected] peerForIP: NodeByAddr(%v) returned node %v but NodeByID failed", ip, nid)
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
self := nb.Self()
|
|
return wgengine.PeerForIP{
|
|
Node: n,
|
|
IsSelf: self.Valid() && self.ID() == nid,
|
|
Route: netip.PrefixFrom(ip, ip.BitLen()),
|
|
}, true
|
|
}
|
|
}
|
|
|
|
route, pr, ok := nb.routeMgr.Outbound().LookupPrefixLPM(netip.PrefixFrom(ip, ip.BitLen()))
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
nid, ok := nb.NodeByKey(pr.Key)
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
n, ok := nb.NodeByID(nid)
|
|
if !ok {
|
|
return wgengine.PeerForIP{}, false
|
|
}
|
|
return wgengine.PeerForIP{Node: n, Route: route}, true
|
|
}
|