ipn/ipnlocal,net/dns/resolver: prevent bare name resolution when MagicDNS is disabled

nodeBackend.nodeByName should always contain both FQDNs and short names,
as it is used in different contexts, including UserDial DNS resolution, which should
be able to resolve unqualified DNS names regardless of the MagicDNS state.

However, net/dns/resolver.Resolver and, by extension, MagicDNSHosts
implementations should only resolve fully qualified domain names,
skipping short names when MagicDNS is disabled for the tailnet.

This fixes it in (*nodeBackend).nodeByFQDNLocked, which is only used
in the MagicDNS paths, and updates the tests.

Fixes #20789

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl authored and Nick Khyl committed 2026-08-10 17:54:32 -05:00
1 parent 25877455e7
commit 1ec348784b
4 files changed
+32 -2

No files matched your search

+13 -2
View File
@@ -1282,12 +1282,23 @@ func (nb *nodeBackend) magicDNSSubdomainHost(fqdn dnsname.FQDN) bool {
}
// nodeByFQDNLocked returns the node (peer or self) with the given
// MagicDNS FQDN. nb.mu must be held.
// MagicDNS FQDN. If fqdn is a short name (has no suffix),
// it is resolved only if MagicDNS is enabled.
// nb.mu must be held.
func (nb *nodeBackend) nodeByFQDNLocked(fqdn dnsname.FQDN) (_ tailcfg.NodeView, ok bool) {
nm := nb.netMap
if nm == nil {
return tailcfg.NodeView{}, false
}
// The resolver already lowercases query names, but lowercase
// again (nearly free when already lowercase) so that no other
// caller of the [resolver.MagicDNSHosts] hook can miss on case.
nid, ok := nb.nodeByName[strings.ToLower(strings.TrimSuffix(string(fqdn), "."))]
canon := strings.ToLower(strings.TrimSuffix(string(fqdn), "."))
// Don't resolve bare hostnames (e.g. "foo") if MagicDNS is disabled.
if !nm.DNS.Proxied && !strings.Contains(canon, ".") {
return tailcfg.NodeView{}, false
}
nid, ok := nb.nodeByName[canon]
if !ok {
return tailcfg.NodeView{}, false
}
+14
View File
@@ -570,6 +570,11 @@ func TestNodeBackendRouteManagerExtras(t *testing.T) {
// lookups must serve from the node indexes and stay correct across
// netmap deltas without any full Hosts map rebuild.
func TestNodeBackendMagicDNSHosts(t *testing.T) {
t.Run("MagicDNS-enabled", func(t *testing.T) { testNodeBackendMagicDNSHosts(t, true) })
t.Run("MagicDNS-disabled", func(t *testing.T) { testNodeBackendMagicDNSHosts(t, false) })
}
func testNodeBackendMagicDNSHosts(t *testing.T, magicDNSEnabled bool) {
nb := newNodeBackend(t.Context(), tstest.WhileTestRunningLogger(t), eventbus.New())
self := &tailcfg.Node{
@@ -590,6 +595,7 @@ func TestNodeBackendMagicDNSHosts(t *testing.T) {
nb.SetNetMap(&netmap.NetworkMap{
SelfNode: self.View(),
Peers: []tailcfg.NodeView{p1.View()},
DNS: tailcfg.DNSConfig{Proxied: magicDNSEnabled},
})
wantHost := func(fqdn dnsname.FQDN, want ...netip.Addr) {
@@ -612,6 +618,14 @@ func TestNodeBackendMagicDNSHosts(t *testing.T) {
wantHost("self.example.ts.net.", netip.MustParseAddr("100.64.0.1"))
wantHost("unknown.example.ts.net.")
// Short names are only resolved if MagicDNS is enabled.
// Otherwise, the resolver should not serve any short names.
var shortNameAddr []netip.Addr
if magicDNSEnabled {
shortNameAddr = []netip.Addr{netip.MustParseAddr("100.64.0.2")}
}
wantHost("p1.", shortNameAddr...)
if fqdn, ok := nb.magicDNSPTR(netip.MustParseAddr("100.64.0.2")); !ok || fqdn != "p1.example.ts.net." {
t.Errorf("magicDNSPTR(100.64.0.2) = %q, %v; want p1's name", fqdn, ok)
}
+2
View File
@@ -81,6 +81,8 @@ func (b *LocalBackend) resolveMagicDNS(hostname, network string) (_ netip.Addr,
// 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.
// Short names ("foo") are only resolved if MagicDNS is enabled
// for the current tailnet.
type magicDNSHosts struct{ b *LocalBackend }
func (m magicDNSHosts) LookupHost(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
+3
View File
@@ -264,6 +264,9 @@ type Resolver struct {
// methods are called on the DNS query serving path. Name lookups are
// case-insensitive: the resolver passes lowercase names, but
// implementations must not rely on that.
//
// Short names ("foo") are only resolved if MagicDNS is enabled
// for the current tailnet.
type MagicDNSHosts interface {
// LookupHost returns the IPs to answer for the node with the
// given MagicDNS FQDN, and whether the name is known. It returns