Compare commits

...

1 Commits

Author SHA1 Message Date
Richard Castro
f4ea422175 net/dns/resolver: add subdomain resolver support in MagicDNS
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>
2023-10-27 16:49:31 -07:00
6 changed files with 175 additions and 13 deletions

View File

@@ -3383,7 +3383,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
// Ignore.
continue
}
fqdn, err := dnsname.ToFQDN(rec.Name)
fqdn, err := dnsname.NewFQDN(rec.Name)
if err != nil {
continue
}

View File

@@ -36,6 +36,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
)
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
@@ -71,7 +72,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 maps either domain names or wildcards to IP(s).
// A domain name map key would be "record.foo.com.".
// If the map key begins with a dot (e.g. ".foo.com.") then it's considered a wildcard,
// matching *.foo.com. "foo.com" would not get matched with ".foo.com" since the label "foo"
// would be stripped before looking for a match.
Hosts map[dnsname.FQDN][]netip.Addr
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
@@ -196,6 +201,7 @@ type Resolver struct {
mu sync.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netip.Addr
wildcards map[dnsname.FQDN][]netip.Addr
ipToHost map[netip.Addr]dnsname.FQDN
}
@@ -246,6 +252,15 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
// Extract wildcard hosts
var keys map[dnsname.FQDN][]netip.Addr
for k, v := range r.hostToIP {
if strings.HasPrefix(string(k), ".") {
mak.Set(&keys, k, v)
}
}
r.wildcards = keys
return nil
}
@@ -596,11 +611,24 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
r.mu.Lock()
hosts := r.hostToIP
wildcards := r.wildcards
localDomains := r.localDomains
r.mu.Unlock()
addrs, found := hosts[domain]
if !found {
addrs, ok := hosts[domain]
if !ok {
// Look for segment in map when '.' is found
d := domain.WithTrailingDot()
for ix := strings.IndexRune(d, '.'); ix >= 0; ix = strings.IndexRune(d, '.') {
h := dnsname.FQDN(d[ix:]) // include the dot
d = d[ix+1:]
if addrs, ok = wildcards[h]; ok {
break
}
}
}
if !ok {
for _, suffix := range localDomains {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.

View File

@@ -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,11 @@ 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},
".domain.test.": {testipv4alt2},
"nonwild.subdomain.test.": {testipv4alt1},
".sub.domain.test.": {testipv4alt3},
},
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
}
@@ -361,6 +367,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 +386,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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {