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:
committed by
GitHub
parent
214b70cc1a
commit
f48cd46662
@@ -39,6 +39,7 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
|
||||
@@ -79,6 +80,12 @@ type Config struct {
|
||||
// LocalDomains is a list of DNS name suffixes that should not be
|
||||
// routed to upstream resolvers.
|
||||
LocalDomains []dnsname.FQDN
|
||||
// SubdomainHosts is a set of FQDNs from Hosts that should also
|
||||
// resolve subdomain queries to the same IPs. If a query like
|
||||
// "sub.node.tailnet.ts.net" doesn't match Hosts directly, and
|
||||
// "node.tailnet.ts.net" is in SubdomainHosts, the query resolves
|
||||
// to the IPs for "node.tailnet.ts.net".
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
@@ -214,10 +221,11 @@ type Resolver struct {
|
||||
closed chan struct{}
|
||||
|
||||
// mu guards the following fields from being updated while used.
|
||||
mu syncs.Mutex
|
||||
localDomains []dnsname.FQDN
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
mu syncs.Mutex
|
||||
localDomains []dnsname.FQDN
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
subdomainHosts set.Set[dnsname.FQDN]
|
||||
}
|
||||
|
||||
type ForwardLinkSelector interface {
|
||||
@@ -278,6 +286,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
r.localDomains = cfg.LocalDomains
|
||||
r.hostToIP = cfg.Hosts
|
||||
r.ipToHost = reverse
|
||||
r.subdomainHosts = cfg.SubdomainHosts
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -642,9 +651,18 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
r.mu.Lock()
|
||||
hosts := r.hostToIP
|
||||
localDomains := r.localDomains
|
||||
subdomainHosts := r.subdomainHosts
|
||||
r.mu.Unlock()
|
||||
|
||||
addrs, found := hosts[domain]
|
||||
if !found {
|
||||
for parent := domain.Parent(); parent != ""; parent = parent.Parent() {
|
||||
if subdomainHosts.Contains(parent) {
|
||||
addrs, found = hosts[parent]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
for _, suffix := range localDomains {
|
||||
if suffix.Contains(domain) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -429,6 +430,56 @@ func TestResolveLocal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLocalSubdomain(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
// Configure with SubdomainHosts set for test1.ipn.dev
|
||||
cfg := Config{
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"test1.ipn.dev.": {testipv4},
|
||||
"test2.ipn.dev.": {testipv6},
|
||||
},
|
||||
LocalDomains: []dnsname.FQDN{"ipn.dev."},
|
||||
SubdomainHosts: set.Of[dnsname.FQDN]("test1.ipn.dev."),
|
||||
}
|
||||
r.SetConfig(cfg)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
qname dnsname.FQDN
|
||||
qtype dns.Type
|
||||
ip netip.Addr
|
||||
code dns.RCode
|
||||
}{
|
||||
// Exact matches still work
|
||||
{"exact-ipv4", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess},
|
||||
{"exact-ipv6", "test2.ipn.dev.", dns.TypeAAAA, testipv6, dns.RCodeSuccess},
|
||||
|
||||
// Subdomain of test1 resolves (test1 has SubdomainHosts set)
|
||||
{"subdomain-ipv4", "foo.test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess},
|
||||
{"subdomain-deep", "bar.foo.test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess}, // Multi-level subdomain
|
||||
|
||||
// Subdomain of test2 does NOT resolve (test2 lacks SubdomainHosts)
|
||||
{"subdomain-no-cap", "foo.test2.ipn.dev.", dns.TypeAAAA, netip.Addr{}, dns.RCodeNameError},
|
||||
|
||||
// Non-existent parent still returns NXDOMAIN
|
||||
{"subdomain-no-parent", "foo.test3.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, code := r.resolveLocal(tt.qname, tt.qtype)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLocalReverse(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
Reference in New Issue
Block a user