net/dns,ipn/ipnlocal: add nodecap to resolve subdomains (#18258)

This adds a new node capability 'dns-subdomain-resolve' that signals
that all of hosts' subdomains should resolve to the same IP address.
It allows wildcard matching on any node marked with this capability.

This change also includes an util/dnsname utility function that lets
us access the parent of a full qualified domain name. MagicDNS takes
this function and recursively searchs for a matching real node name.

One important thing to observe is that, in this context, a subdomain
can have multiple sub labels. This means that for a given node named
machine, both my.machine and be.my.machine will be a positive match.

Updates #1196

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
This commit is contained in:
Fernando Serboncini
2026-01-30 13:32:34 -05:00
committed by GitHub
parent 214b70cc1a
commit f48cd46662
11 changed files with 186 additions and 4 deletions
+33
View File
@@ -106,6 +106,39 @@ func TestDNSConfigForNetmap(t *testing.T) {
},
},
},
{
name: "subdomain_resolve_capability",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("100.101.101.101"),
}).View(),
AllCaps: set.SetOf([]tailcfg.NodeCapability{tailcfg.NodeAttrDNSSubdomainResolve}),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peer-with-cap.net.",
Addresses: ipps("100.102.0.1"),
CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil},
},
{
ID: 2,
Name: "peer-without-cap.net.",
Addresses: ipps("100.102.0.2"),
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{
"myname.net.": ips("100.101.101.101"),
"peer-with-cap.net.": ips("100.102.0.1"),
"peer-without-cap.net.": ips("100.102.0.2"),
},
SubdomainHosts: set.Of[dnsname.FQDN]("myname.net.", "peer-with-cap.net."),
},
},
{
// An ephemeral node with only an IPv6 address
// should get IPv6 records for all its peers,
+12
View File
@@ -751,8 +751,20 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
dcfg.Hosts[fqdn] = ips
}
set(nm.SelfName(), nm.GetAddresses())
if nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) {
if fqdn, err := dnsname.ToFQDN(nm.SelfName()); err == nil {
dcfg.SubdomainHosts.Make()
dcfg.SubdomainHosts.Add(fqdn)
}
}
for _, peer := range peers {
set(peer.Name(), peer.Addresses())
if peer.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve) {
if fqdn, err := dnsname.ToFQDN(peer.Name()); err == nil {
dcfg.SubdomainHosts.Make()
dcfg.SubdomainHosts.Add(fqdn)
}
}
}
for _, rec := range nm.DNS.ExtraRecords {
switch rec.Type {