Compare commits
1 Commits
awly/cli-j
...
richard/15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754cda3b9a |
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user