summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRichard Castro <richard@tailscale.com>2023-10-12 15:20:43 -0700
committerRichard Castro <richard@tailscale.com>2023-10-27 17:41:31 -0700
commit754cda3b9aaab27f91adcf950a9a94056dbb0dd7 (patch)
treea1a85fea7a39527bc2ae47979bd2c3c3560cdd30
parentb4247fabecfc5f1536943f027e660887364fc6e4 (diff)
downloadtailscale-richard/15037-2.tar.xz
tailscale-richard/15037-2.zip
net/dns/resolver: add subdomain resolver support in MagicDNSrichard/15037-2
This PR adds processing of subdomains that if there is an entry for a wildcard entry in hosts for a domain, MagicDNS will resolve with the host IP. Fixes #15037 Signed-off-by: Richard Castro <richard@tailscale.com>
-rw-r--r--ipn/ipnlocal/local.go15
-rw-r--r--net/dns/config.go8
-rw-r--r--net/dns/resolver/tsdns.go31
-rw-r--r--net/dns/resolver/tsdns_test.go29
-rw-r--r--tailcfg/tailcfg.go9
-rw-r--r--util/dnsname/dnsname.go35
-rw-r--r--util/dnsname/dnsname_test.go86
7 files changed, 196 insertions, 17 deletions
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 682add38e..6ea0e4dd1 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -3383,11 +3383,22 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// Ignore.
continue
}
- fqdn, err := dnsname.ToFQDN(rec.Name)
+ // If the name has a leading dot, but is not exactly '.'.
+ var isSuffix bool
+ // Assume upstream provides either a suffix or an FQDN that are
+ // respectively well formed.
+ if len(rec.Name) > 1 && rec.Name[0] == '.' {
+ isSuffix = true
+ }
+ fqdn, err := dnsname.NewFQDN(rec.Name)
if err != nil {
continue
}
- dcfg.Hosts[fqdn] = append(dcfg.Hosts[fqdn], ip)
+ if isSuffix {
+ mak.Set(&dcfg.Suffixes, fqdn, append(dcfg.Suffixes[fqdn], ip))
+ } else {
+ dcfg.Hosts[fqdn] = append(dcfg.Hosts[fqdn], ip)
+ }
}
if !prefs.CorpDNS() {
diff --git a/net/dns/config.go b/net/dns/config.go
index 9c55f6d73..7361a74b3 100644
--- a/net/dns/config.go
+++ b/net/dns/config.go
@@ -41,6 +41,14 @@ type Config struct {
// it to resolve, you also need to add appropriate routes to
// Routes.
Hosts map[dnsname.FQDN][]netip.Addr
+ // Suffixes maps DNS FQDNs and all subdomains to their IPs, which can be a
+ // mix of IPv4 and IPv6.
+ // Queries matching entries in Suffixes are resolved locally by
+ // 100.100.100.100 without leaving the machine.
+ // Adding an entry to Suffixes merely creates the record. If you want
+ // it to resolve, you also need to add appropriate routes to
+ // Routes.
+ Suffixes map[dnsname.FQDN][]netip.Addr
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
// instead of the IPv4 version (100.100.100.100).
OnlyIPv6 bool
diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go
index ddb7b6cdb..92cf5bc3c 100644
--- a/net/dns/resolver/tsdns.go
+++ b/net/dns/resolver/tsdns.go
@@ -71,8 +71,11 @@ type Config struct {
// Queries only match the most specific suffix.
// To register a "default route", add an entry for ".".
Routes map[dnsname.FQDN][]*dnstype.Resolver
- // LocalHosts is a map of FQDNs to corresponding IPs.
+ // Hosts is a map of FQDNs to corresponding IPs.
Hosts map[dnsname.FQDN][]netip.Addr
+ // Suffixes is a map of FQDNs to corresponding IPs that should match all
+ // subdomains and the suffix itself.
+ Suffixes map[dnsname.FQDN][]netip.Addr
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
@@ -195,8 +198,11 @@ type Resolver struct {
// mu guards the following fields from being updated while used.
mu sync.Mutex
localDomains []dnsname.FQDN
- hostToIP map[dnsname.FQDN][]netip.Addr
- ipToHost map[netip.Addr]dnsname.FQDN
+ // hostToIP maps a single FQDN to one or more IPs.
+ hostToIP map[dnsname.FQDN][]netip.Addr
+ // suffixes maps and FQDN and all subdomains to one or more IPs.
+ suffixes map[dnsname.FQDN][]netip.Addr
+ ipToHost map[netip.Addr]dnsname.FQDN
}
type ForwardLinkSelector interface {
@@ -246,6 +252,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
+ r.suffixes = cfg.Suffixes
return nil
}
@@ -596,11 +603,25 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
r.mu.Lock()
hosts := r.hostToIP
+ suffixes := r.suffixes
localDomains := r.localDomains
r.mu.Unlock()
- addrs, found := hosts[domain]
- if !found {
+ addrs, ok := hosts[domain]
+ if !ok {
+ // Look for a matching suffix in suffixes. This implementation prefers
+ // the case where the number of domain labels is typically few, and the
+ // list of suffix candidates to match is arbitrarily sized.
+ d := domain.WithTrailingDot()
+ for ix := strings.IndexRune(d, '.'); ix >= 0; ix = strings.IndexRune(d, '.') {
+ d = d[ix+1:]
+ if addrs, ok = suffixes[dnsname.FQDN(d)]; ok {
+ break
+ }
+ }
+ }
+
+ if !ok {
for _, suffix := range localDomains {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.
diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go
index 882462012..af3861deb 100644
--- a/net/dns/resolver/tsdns_test.go
+++ b/net/dns/resolver/tsdns_test.go
@@ -32,8 +32,11 @@ import (
)
var (
- testipv4 = netip.MustParseAddr("1.2.3.4")
- testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f")
+ testipv4 = netip.MustParseAddr("1.2.3.4")
+ testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f")
+ testipv4alt1 = netip.MustParseAddr("2.2.3.4")
+ testipv4alt2 = netip.MustParseAddr("12.2.3.4")
+ testipv4alt3 = netip.MustParseAddr("21.2.3.4")
testipv4Arpa = dnsname.FQDN("4.3.2.1.in-addr.arpa.")
testipv6Arpa = dnsname.FQDN("f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa.")
@@ -43,8 +46,13 @@ var (
var dnsCfg = Config{
Hosts: map[dnsname.FQDN][]netip.Addr{
- "test1.ipn.dev.": {testipv4},
- "test2.ipn.dev.": {testipv6},
+ "test1.ipn.dev.": {testipv4},
+ "test2.ipn.dev.": {testipv6},
+ "nonwild.subdomain.test.": {testipv4alt1},
+ },
+ Suffixes: map[dnsname.FQDN][]netip.Addr{
+ "domain.test.": {testipv4alt2},
+ "sub.domain.test.": {testipv4alt3},
},
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
}
@@ -361,6 +369,11 @@ func TestResolveLocal(t *testing.T) {
// suffixes are currently hard-coded and not plumbed via the netmap)
{"via_form3_dec_example.com", dnsname.FQDN("1-2-3-4-via-1.example.com."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused},
{"via_form3_dec_examplets.net", dnsname.FQDN("1-2-3-4-via-1.examplets.net."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused},
+
+ // subdomain entry for app connectors
+ {"subdomain", dnsname.FQDN(".domain.test."), dns.TypeA, testipv4alt2, dns.RCodeSuccess},
+ {"exact subdomain", dnsname.FQDN("deep.sub.domain.test."), dns.TypeA, testipv4alt3, dns.RCodeSuccess},
+ {"priority subdomain", dnsname.FQDN("priority.sub.domain.test."), dns.TypeA, testipv4alt3, dns.RCodeSuccess},
}
for _, tt := range tests {
@@ -375,6 +388,14 @@ func TestResolveLocal(t *testing.T) {
}
})
}
+
+ // Wilcard paths should have 0 allocs
+ allocs := testing.AllocsPerRun(1000, func() {
+ r.resolveLocal(dnsname.FQDN(".domain.test."), dns.TypeA)
+ })
+ if allocs > 0 {
+ t.Errorf("allocs per run = %v; want 0", allocs)
+ }
}
func TestResolveLocalReverse(t *testing.T) {
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index fd45108bc..ec11bd3d6 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -120,7 +120,9 @@ type CapabilityVersion int
// - 77: 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer
// - 78: 2023-10-05: can handle c2n Wake-on-LAN sending
// - 79: 2023-10-05: Client understands UrgentSecurityUpdate in ClientVersion
-const CurrentCapabilityVersion CapabilityVersion = 79
+// - 80: 2023-10-16: wildcards are supported as entries in Config.Hosts
+
+const CurrentCapabilityVersion CapabilityVersion = 80
type StableID string
@@ -1553,7 +1555,10 @@ type DNSConfig struct {
// DNSRecord is an extra DNS record to add to MagicDNS.
type DNSRecord struct {
// Name is the fully qualified domain name of
- // the record to add. The trailing dot is optional.
+ // the record to add. If a leading dot is
+ // present, the record will serve all
+ // subomdains as well as the fully qualified
+ // domain name. The trailing dot is optional.
Name string
// Type is the DNS record type.
diff --git a/util/dnsname/dnsname.go b/util/dnsname/dnsname.go
index 6481a5867..f3c4f0ea8 100644
--- a/util/dnsname/dnsname.go
+++ b/util/dnsname/dnsname.go
@@ -20,14 +20,12 @@ const (
// A FQDN is a fully-qualified DNS name or name suffix.
type FQDN string
-func ToFQDN(s string) (FQDN, error) {
+// NewFQDN converts a string to a FQDN that retains any leading '.' in the case of wildcards.
+func NewFQDN(s string) (FQDN, error) {
if len(s) == 0 || s == "." {
return FQDN("."), nil
}
- if s[0] == '.' {
- s = s[1:]
- }
raw := s
totalLen := len(s)
if s[len(s)-1] == '.' {
@@ -41,6 +39,11 @@ func ToFQDN(s string) (FQDN, error) {
st := 0
for i := 0; i < len(s); i++ {
+ // Ignore leading '.' from wildcards for label processing
+ if i == 0 && s[i] == '.' {
+ st = i + 1
+ continue
+ }
if s[i] != '.' {
continue
}
@@ -65,6 +68,30 @@ func ToFQDN(s string) (FQDN, error) {
return FQDN(raw), nil
}
+// ToFQDN strips a leading '.' on a string before converting to a FQDN.
+func ToFQDN(s string) (FQDN, error) {
+ if len(s) == 0 || s == "." {
+ return FQDN("."), nil
+ }
+
+ if s[0] == '.' {
+ s = s[1:]
+ }
+ return NewFQDN(s)
+}
+
+// ToFQDNSuffix returns an FQDN with a leading '.'.
+func ToFQDNSuffix(s string) (FQDN, error) {
+ if len(s) == 0 || s == "." {
+ return FQDN("."), nil
+ }
+
+ if s[0] != '.' {
+ s = "." + s
+ }
+ return NewFQDN(s)
+}
+
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
diff --git a/util/dnsname/dnsname_test.go b/util/dnsname/dnsname_test.go
index 563959d33..0666d966a 100644
--- a/util/dnsname/dnsname_test.go
+++ b/util/dnsname/dnsname_test.go
@@ -210,6 +210,92 @@ func TestValidHostname(t *testing.T) {
}
}
+func TestNewFQDN(t *testing.T) {
+ tests := []struct {
+ in string
+ want FQDN
+ wantErr bool
+ wantLabels int
+ }{
+ {"", ".", false, 0},
+ {".", ".", false, 0},
+ {".foo.com", ".foo.com.", false, 3},
+ {"foo.com.", "foo.com.", false, 2},
+ }
+
+ for _, test := range tests {
+ t.Run(test.in, func(t *testing.T) {
+ got, err := NewFQDN(test.in)
+ if got != test.want {
+ t.Errorf("NewFQDN(%q) got %q, want %q", test.in, got, test.want)
+ }
+ if (err != nil) != test.wantErr {
+ t.Errorf("NewFQDN(%q) err %v, wantErr=%v", test.in, err, test.wantErr)
+ }
+ if err != nil {
+ return
+ }
+
+ gotDot := got.WithTrailingDot()
+ if gotDot != string(test.want) {
+ t.Errorf("NewFQDN(%q).WithTrailingDot() got %q, want %q", test.in, gotDot, test.want)
+ }
+ gotNoDot := got.WithoutTrailingDot()
+ wantNoDot := string(test.want)[:len(test.want)-1]
+ if gotNoDot != wantNoDot {
+ t.Errorf("NewFQDN(%q).WithoutTrailingDot() got %q, want %q", test.in, gotNoDot, wantNoDot)
+ }
+
+ if gotLabels := got.NumLabels(); gotLabels != test.wantLabels {
+ t.Errorf("NewFQDN(%q).NumLabels() got %v, want %v", test.in, gotLabels, test.wantLabels)
+ }
+ })
+ }
+}
+
+func TestToFQDNSuffix(t *testing.T) {
+ tests := []struct {
+ in string
+ want FQDN
+ wantErr bool
+ wantLabels int
+ }{
+ {"", ".", false, 0},
+ {".", ".", false, 0},
+ {".foo.com", ".foo.com.", false, 3},
+ {"foo.com.", ".foo.com.", false, 3},
+ }
+
+ for _, test := range tests {
+ t.Run(test.in, func(t *testing.T) {
+ got, err := ToFQDNSuffix(test.in)
+ if got != test.want {
+ t.Errorf("ToFQDNSuffix(%q) got %q, want %q", test.in, got, test.want)
+ }
+ if (err != nil) != test.wantErr {
+ t.Errorf("ToFQDNSuffix(%q) err %v, wantErr=%v", test.in, err, test.wantErr)
+ }
+ if err != nil {
+ return
+ }
+
+ gotDot := got.WithTrailingDot()
+ if gotDot != string(test.want) {
+ t.Errorf("ToFQDNSuffix(%q).WithTrailingDot() got %q, want %q", test.in, gotDot, test.want)
+ }
+ gotNoDot := got.WithoutTrailingDot()
+ wantNoDot := string(test.want)[:len(test.want)-1]
+ if gotNoDot != wantNoDot {
+ t.Errorf("ToFQDNSuffix(%q).WithoutTrailingDot() got %q, want %q", test.in, gotNoDot, wantNoDot)
+ }
+
+ if gotLabels := got.NumLabels(); gotLabels != test.wantLabels {
+ t.Errorf("ToFQDNSuffix(%q).NumLabels() got %v, want %v", test.in, gotLabels, test.wantLabels)
+ }
+ })
+ }
+}
+
var sinkFQDN FQDN
func BenchmarkToFQDN(b *testing.B) {