util/dnsname: add FQDN type, use throughout codebase.

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2021-04-09 15:24:47 -07:00 committed by Dave Anderson
parent 7a1813fd24
commit 1a371b93be
13 changed files with 449 additions and 214 deletions

View File

@ -43,6 +43,7 @@
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/wgkey" "tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/util/systemd" "tailscale.com/util/systemd"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/wgengine" "tailscale.com/wgengine"
@ -1529,12 +1530,12 @@ func (b *LocalBackend) authReconfig() {
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res) dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res)
} }
if len(nm.DNS.Routes) > 0 { if len(nm.DNS.Routes) > 0 {
dcfg.Routes = map[string][]netaddr.IPPort{} dcfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
} }
for suffix, resolvers := range nm.DNS.Routes { for suffix, resolvers := range nm.DNS.Routes {
if !strings.HasSuffix(suffix, ".") || strings.HasPrefix(suffix, ".") { fqdn, err := dnsname.ToFQDN(suffix)
b.logf("[unexpected] malformed DNS route suffix %q", suffix) if err != nil {
continue b.logf("[unexpected] non-FQDN route suffix %q", suffix)
} }
for _, resolver := range resolvers { for _, resolver := range resolvers {
res, err := parseResolver(resolver) res, err := parseResolver(resolver)
@ -1542,23 +1543,33 @@ func (b *LocalBackend) authReconfig() {
b.logf(err.Error()) b.logf(err.Error())
continue continue
} }
dcfg.Routes[suffix] = append(dcfg.Routes[suffix], res) dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], res)
} }
} }
dcfg.SearchDomains = nm.DNS.Domains for _, dom := range nm.DNS.Domains {
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
b.logf("[unexpected] non-FQDN search domain %q", dom)
}
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
}
dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm) dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm)
set := func(name string, addrs []netaddr.IPPrefix) { set := func(name string, addrs []netaddr.IPPrefix) {
if len(addrs) == 0 || name == "" { if len(addrs) == 0 || name == "" {
return return
} }
fqdn, err := dnsname.ToFQDN(name)
if err != nil {
return // TODO: propagate error?
}
var ips []netaddr.IP var ips []netaddr.IP
for _, addr := range addrs { for _, addr := range addrs {
ips = append(ips, addr.IP) ips = append(ips, addr.IP)
} }
dcfg.Hosts[name] = ips dcfg.Hosts[fqdn] = ips
} }
if nm.DNS.Proxied { // actually means "enable MagicDNS" if nm.DNS.Proxied { // actually means "enable MagicDNS"
dcfg.Hosts = map[string][]netaddr.IP{} dcfg.Hosts = map[dnsname.FQDN][]netaddr.IP{}
set(nm.Name, nm.Addresses) set(nm.Name, nm.Addresses)
for _, peer := range nm.Peers { for _, peer := range nm.Peers {
set(peer.Name, peer.Addresses) set(peer.Name, peer.Addresses)
@ -1691,9 +1702,14 @@ func (b *LocalBackend) initPeerAPIListener() {
} }
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
func magicDNSRootDomains(nm *netmap.NetworkMap) []string { func magicDNSRootDomains(nm *netmap.NetworkMap) []dnsname.FQDN {
if v := nm.MagicDNSSuffix(); v != "" { if v := nm.MagicDNSSuffix(); v != "" {
return []string{strings.Trim(v, ".")} fqdn, err := dnsname.ToFQDN(v)
if err != nil {
// TODO: propagate error
return nil
}
return []dnsname.FQDN{fqdn}
} }
return nil return nil
} }

View File

@ -6,9 +6,9 @@
import ( import (
"sort" "sort"
"strings"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/util/dnsname"
) )
// Config is a DNS configuration. // Config is a DNS configuration.
@ -22,21 +22,21 @@ type Config struct {
// for queries that fall within that suffix. // for queries that fall within that suffix.
// If a query doesn't match any entry in Routes, the // If a query doesn't match any entry in Routes, the
// DefaultResolvers are used. // DefaultResolvers are used.
Routes map[string][]netaddr.IPPort Routes map[dnsname.FQDN][]netaddr.IPPort
// SearchDomains are DNS suffixes to try when expanding // SearchDomains are DNS suffixes to try when expanding
// single-label queries. // single-label queries.
SearchDomains []string SearchDomains []dnsname.FQDN
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4 // Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
// and IPv6. // and IPv6.
// Queries matching entries in Hosts are resolved locally without // Queries matching entries in Hosts are resolved locally without
// recursing off-machine. // recursing off-machine.
Hosts map[string][]netaddr.IP Hosts map[dnsname.FQDN][]netaddr.IP
// AuthoritativeSuffixes is a list of fully-qualified DNS suffixes // AuthoritativeSuffixes is a list of fully-qualified DNS suffixes
// for which the in-process Tailscale resolver is authoritative. // for which the in-process Tailscale resolver is authoritative.
// Queries for names within AuthoritativeSuffixes can only be // Queries for names within AuthoritativeSuffixes can only be
// fulfilled by entries in Hosts. Queries with no match in Hosts // fulfilled by entries in Hosts. Queries with no match in Hosts
// return NXDOMAIN. // return NXDOMAIN.
AuthoritativeSuffixes []string AuthoritativeSuffixes []dnsname.FQDN
} }
// needsAnyResolvers reports whether c requires a resolver to be set // needsAnyResolvers reports whether c requires a resolver to be set
@ -85,24 +85,26 @@ func (c Config) hasHosts() bool {
// matchDomains returns the list of match suffixes needed by Routes, // matchDomains returns the list of match suffixes needed by Routes,
// AuthoritativeSuffixes. Hosts is not considered as we assume that // AuthoritativeSuffixes. Hosts is not considered as we assume that
// they're covered by AuthoritativeSuffixes for now. // they're covered by AuthoritativeSuffixes for now.
func (c Config) matchDomains() []string { func (c Config) matchDomains() []dnsname.FQDN {
ret := make([]string, 0, len(c.Routes)+len(c.AuthoritativeSuffixes)) ret := make([]dnsname.FQDN, 0, len(c.Routes)+len(c.AuthoritativeSuffixes))
seen := map[string]bool{} seen := map[dnsname.FQDN]bool{}
for _, suffix := range c.AuthoritativeSuffixes { for _, suffix := range c.AuthoritativeSuffixes {
if seen[suffix] { if seen[suffix] {
continue continue
} }
ret = append(ret, strings.TrimSuffix(suffix, ".")) ret = append(ret, suffix)
seen[suffix] = true seen[suffix] = true
} }
for suffix := range c.Routes { for suffix := range c.Routes {
if seen[suffix] { if seen[suffix] {
continue continue
} }
ret = append(ret, strings.TrimSuffix(suffix, ".")) ret = append(ret, suffix)
seen[suffix] = true seen[suffix] = true
} }
sort.Strings(ret) sort.Slice(ret, func(i, j int) bool {
return ret[i].WithTrailingDot() < ret[j].WithTrailingDot()
})
return ret return ret
} }

View File

@ -9,6 +9,7 @@
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -18,6 +19,7 @@
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/atomicfile" "tailscale.com/atomicfile"
"tailscale.com/util/dnsname"
) )
const ( const (
@ -26,7 +28,7 @@
) )
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer. // writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) { func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []dnsname.FQDN) {
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n") io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n") io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range servers { for _, ns := range servers {
@ -38,7 +40,7 @@ func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
io.WriteString(w, "search") io.WriteString(w, "search")
for _, domain := range domains { for _, domain := range domains {
io.WriteString(w, " ") io.WriteString(w, " ")
io.WriteString(w, domain) io.WriteString(w, domain.WithoutTrailingDot())
} }
io.WriteString(w, "\n") io.WriteString(w, "\n")
} }
@ -70,7 +72,11 @@ func readResolvFile(path string) (OSConfig, error) {
if strings.HasPrefix(line, "search") { if strings.HasPrefix(line, "search") {
domain := strings.TrimPrefix(line, "search") domain := strings.TrimPrefix(line, "search")
domain = strings.TrimSpace(domain) domain = strings.TrimSpace(domain)
config.SearchDomains = append(config.SearchDomains, domain) fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
}
config.SearchDomains = append(config.SearchDomains, fqdn)
continue continue
} }
} }

View File

@ -12,6 +12,7 @@
"tailscale.com/net/dns/resolver" "tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/wgengine/monitor" "tailscale.com/wgengine/monitor"
) )
@ -60,7 +61,7 @@ func forceSplitDNSForTesting(cfg *Config) {
} }
if cfg.Routes == nil { if cfg.Routes == nil {
cfg.Routes = map[string][]netaddr.IPPort{} cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
} }
for _, search := range cfg.SearchDomains { for _, search := range cfg.SearchDomains {
cfg.Routes[search] = cfg.DefaultResolvers cfg.Routes[search] = cfg.DefaultResolvers
@ -112,14 +113,14 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
// Default resolvers plus other stuff always ends up proxying // Default resolvers plus other stuff always ends up proxying
// through quad-100. // through quad-100.
rcfg := resolver.Config{ rcfg := resolver.Config{
Routes: map[string][]netaddr.IPPort{ Routes: map[dnsname.FQDN][]netaddr.IPPort{
".": cfg.DefaultResolvers, ".": cfg.DefaultResolvers,
}, },
Hosts: cfg.Hosts, Hosts: cfg.Hosts,
LocalDomains: addFQDNDots(cfg.AuthoritativeSuffixes), LocalDomains: cfg.AuthoritativeSuffixes,
} }
for suffix, resolvers := range cfg.Routes { for suffix, resolvers := range cfg.Routes {
rcfg.Routes[suffix+"."] = resolvers rcfg.Routes[suffix] = resolvers
} }
ocfg := OSConfig{ ocfg := OSConfig{
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()}, Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
@ -149,12 +150,12 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot // or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
// split-DNS. Install a split config pointing at quad-100. // split-DNS. Install a split config pointing at quad-100.
rcfg = resolver.Config{ rcfg = resolver.Config{
Routes: map[string][]netaddr.IPPort{},
Hosts: cfg.Hosts, Hosts: cfg.Hosts,
LocalDomains: addFQDNDots(cfg.AuthoritativeSuffixes), LocalDomains: cfg.AuthoritativeSuffixes,
Routes: map[dnsname.FQDN][]netaddr.IPPort{},
} }
for suffix, resolvers := range cfg.Routes { for suffix, resolvers := range cfg.Routes {
rcfg.Routes[suffix+"."] = resolvers rcfg.Routes[suffix] = resolvers
} }
ocfg = OSConfig{ ocfg = OSConfig{
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()}, Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
@ -179,7 +180,7 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
// quad-9 if we accidentally go down this codepath. // quad-9 if we accidentally go down this codepath.
canUseHack := false canUseHack := false
for _, dom := range cfg.SearchDomains { for _, dom := range cfg.SearchDomains {
if strings.HasSuffix(dom, ".tailscale.com") { if strings.HasSuffix(dom.WithoutTrailingDot(), ".tailscale.com") {
canUseHack = true canUseHack = true
break break
} }
@ -198,17 +199,6 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
return rcfg, ocfg, nil return rcfg, ocfg, nil
} }
func addFQDNDots(domains []string) []string {
ret := make([]string, 0, len(domains))
for _, dom := range domains {
if !strings.HasSuffix(dom, ".") {
dom = dom + "."
}
ret = append(ret, dom)
}
return ret
}
// toIPsOnly returns only the IP portion of ipps. // toIPsOnly returns only the IP portion of ipps.
// TODO: this discards port information on the assumption that we're // TODO: this discards port information on the assumption that we're
// always pointing at port 53. // always pointing at port 53.

View File

@ -11,6 +11,7 @@
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/net/dns/resolver" "tailscale.com/net/dns/resolver"
"tailscale.com/util/dnsname"
) )
type fakeOSConfigurator struct { type fakeOSConfigurator struct {
@ -64,78 +65,78 @@ func TestManager(t *testing.T) {
{ {
name: "search-only", name: "search-only",
in: Config{ in: Config{
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
os: OSConfig{ os: OSConfig{
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
}, },
{ {
name: "corp", name: "corp",
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("1.1.1.1", "9.9.9.9"), Nameservers: mustIPs("1.1.1.1", "9.9.9.9"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
}, },
{ {
name: "corp-split", name: "corp-split",
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("1.1.1.1", "9.9.9.9"), Nameservers: mustIPs("1.1.1.1", "9.9.9.9"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
}, },
{ {
name: "corp-magic", name: "corp-magic",
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"), Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
{ {
name: "corp-magic-split", name: "corp-magic-split",
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"), Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
{ {
@ -143,11 +144,11 @@ func TestManager(t *testing.T) {
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"), Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -160,12 +161,12 @@ func TestManager(t *testing.T) {
in: Config{ in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"), DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"), Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -177,15 +178,15 @@ func TestManager(t *testing.T) {
name: "routes", name: "routes",
in: Config{ in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"), Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
bs: OSConfig{ bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"), Nameservers: mustIPs("8.8.8.8"),
SearchDomains: strs("coffee.shop"), SearchDomains: fqdns("coffee.shop"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf", "coffee.shop"), SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -197,13 +198,13 @@ func TestManager(t *testing.T) {
name: "routes-split", name: "routes-split",
in: Config{ in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"), Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("2.2.2.2"), Nameservers: mustIPs("2.2.2.2"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: strs("corp.com"), MatchDomains: fqdns("corp.com"),
}, },
}, },
{ {
@ -212,15 +213,15 @@ func TestManager(t *testing.T) {
Routes: upstreams( Routes: upstreams(
"corp.com", "2.2.2.2:53", "corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"), "bigco.net", "3.3.3.3:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
bs: OSConfig{ bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"), Nameservers: mustIPs("8.8.8.8"),
SearchDomains: strs("coffee.shop"), SearchDomains: fqdns("coffee.shop"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf", "coffee.shop"), SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -235,13 +236,13 @@ func TestManager(t *testing.T) {
Routes: upstreams( Routes: upstreams(
"corp.com", "2.2.2.2:53", "corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"), "bigco.net", "3.3.3.3:53"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: strs("bigco.net", "corp.com"), MatchDomains: fqdns("bigco.net", "corp.com"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -255,23 +256,23 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
bs: OSConfig{ bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"), Nameservers: mustIPs("8.8.8.8"),
SearchDomains: strs("coffee.shop"), SearchDomains: fqdns("coffee.shop"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf", "coffee.shop"), SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams(".", "8.8.8.8:53"), Routes: upstreams(".", "8.8.8.8:53"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
{ {
@ -280,20 +281,20 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: strs("ts.com"), MatchDomains: fqdns("ts.com"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
{ {
@ -303,16 +304,16 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
bs: OSConfig{ bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"), Nameservers: mustIPs("8.8.8.8"),
SearchDomains: strs("coffee.shop"), SearchDomains: fqdns("coffee.shop"),
}, },
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf", "coffee.shop"), SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams( Routes: upstreams(
@ -321,7 +322,7 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
{ {
@ -331,21 +332,21 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: strs("ts.com"), AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
}, },
split: true, split: true,
os: OSConfig{ os: OSConfig{
Nameservers: mustIPs("100.100.100.100"), Nameservers: mustIPs("100.100.100.100"),
SearchDomains: strs("tailscale.com", "universe.tf"), SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: strs("corp.com", "ts.com"), MatchDomains: fqdns("corp.com", "ts.com"),
}, },
rs: resolver.Config{ rs: resolver.Config{
Routes: upstreams("corp.com.", "2.2.2.2:53"), Routes: upstreams("corp.com.", "2.2.2.2:53"),
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: strs("ts.com."), LocalDomains: fqdns("ts.com."),
}, },
}, },
} }
@ -387,11 +388,20 @@ func mustIPPs(strs ...string) (ret []netaddr.IPPort) {
return ret return ret
} }
func strs(strs ...string) []string { return strs } func fqdns(strs ...string) (ret []dnsname.FQDN) {
for _, s := range strs {
fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
ret = append(ret, fqdn)
}
return ret
}
func hosts(strs ...string) (ret map[string][]netaddr.IP) { func hosts(strs ...string) (ret map[dnsname.FQDN][]netaddr.IP) {
var key string var key dnsname.FQDN
ret = map[string][]netaddr.IP{} ret = map[dnsname.FQDN][]netaddr.IP{}
for _, s := range strs { for _, s := range strs {
if ip, err := netaddr.ParseIP(s); err == nil { if ip, err := netaddr.ParseIP(s); err == nil {
if key == "" { if key == "" {
@ -399,15 +409,19 @@ func hosts(strs ...string) (ret map[string][]netaddr.IP) {
} }
ret[key] = append(ret[key], ip) ret[key] = append(ret[key], ip)
} else { } else {
key = s fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
key = fqdn
} }
} }
return ret return ret
} }
func upstreams(strs ...string) (ret map[string][]netaddr.IPPort) { func upstreams(strs ...string) (ret map[dnsname.FQDN][]netaddr.IPPort) {
var key string var key dnsname.FQDN
ret = map[string][]netaddr.IPPort{} ret = map[dnsname.FQDN][]netaddr.IPPort{}
for _, s := range strs { for _, s := range strs {
if ipp, err := netaddr.ParseIPPort(s); err == nil { if ipp, err := netaddr.ParseIPPort(s); err == nil {
if key == "" { if key == "" {
@ -415,7 +429,11 @@ func upstreams(strs ...string) (ret map[string][]netaddr.IPPort) {
} }
ret[key] = append(ret[key], ipp) ret[key] = append(ret[key], ipp)
} else { } else {
key = s fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
key = fqdn
} }
} }
return ret return ret

View File

@ -17,6 +17,7 @@
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/dnsname"
) )
const ( const (
@ -95,7 +96,7 @@ func delValue(key registry.Key, name string) error {
// system's "primary" resolver. // system's "primary" resolver.
// //
// If no resolvers are provided, the Tailscale NRPT rule is deleted. // If no resolvers are provided, the Tailscale NRPT rule is deleted.
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []string) error { func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
if len(resolvers) == 0 { if len(resolvers) == 0 {
return m.delKey(nrptBase) return m.delKey(nrptBase)
} }
@ -108,7 +109,7 @@ func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []string) er
for _, domain := range domains { for _, domain := range domains {
// NRPT rules must have a leading dot, which is not usual for // NRPT rules must have a leading dot, which is not usual for
// DNS search paths. // DNS search paths.
doms = append(doms, "."+domain) doms = append(doms, "."+domain.WithoutTrailingDot())
} }
// CreateKey is actually open-or-create, which suits us fine. // CreateKey is actually open-or-create, which suits us fine.
@ -139,7 +140,7 @@ func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []string) er
// "primary" resolvers. // "primary" resolvers.
// domains can be set without resolvers, which just contributes new // domains can be set without resolvers, which just contributes new
// paths to the global DNS search list. // paths to the global DNS search list.
func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []string) error { func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
var ipsv4 []string var ipsv4 []string
var ipsv6 []string var ipsv6 []string
@ -151,6 +152,11 @@ func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []string)
} }
} }
domStrs := make([]string, 0, len(domains))
for _, dom := range domains {
domStrs = append(domStrs, dom.WithoutTrailingDot())
}
key4, err := m.openKey(m.ifPath(ipv4RegBase)) key4, err := m.openKey(m.ifPath(ipv4RegBase))
if err != nil { if err != nil {
return err return err
@ -169,7 +175,7 @@ func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []string)
if err := delValue(key4, "SearchList"); err != nil { if err := delValue(key4, "SearchList"); err != nil {
return err return err
} }
} else if err := key4.SetStringValue("SearchList", strings.Join(domains, ",")); err != nil { } else if err := key4.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
return err return err
} }
@ -191,7 +197,7 @@ func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []string)
if err := delValue(key6, "SearchList"); err != nil { if err := delValue(key6, "SearchList"); err != nil {
return err return err
} }
} else if err := key6.SetStringValue("SearchList", strings.Join(domains, ",")); err != nil { } else if err := key6.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
return err return err
} }

View File

@ -8,6 +8,7 @@
"errors" "errors"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/util/dnsname"
) )
// An OSConfigurator applies DNS settings to the operating system. // An OSConfigurator applies DNS settings to the operating system.
@ -42,13 +43,13 @@ type OSConfig struct {
// SearchDomains are the domain suffixes to use when expanding // SearchDomains are the domain suffixes to use when expanding
// single-label name queries. SearchDomains is additive to // single-label name queries. SearchDomains is additive to
// whatever non-Tailscale search domains the OS has. // whatever non-Tailscale search domains the OS has.
SearchDomains []string SearchDomains []dnsname.FQDN
// MatchDomains are the DNS suffixes for which Nameservers should // MatchDomains are the DNS suffixes for which Nameservers should
// be used. If empty, Nameservers is installed as the "primary" resolver. // be used. If empty, Nameservers is installed as the "primary" resolver.
// A non-empty MatchDomains requests a "split DNS" configuration // A non-empty MatchDomains requests a "split DNS" configuration
// from the OS, which will only work with OSConfigurators that // from the OS, which will only work with OSConfigurators that
// report SupportsSplitDNS()=true. // report SupportsSplitDNS()=true.
MatchDomains []string MatchDomains []dnsname.FQDN
} }
// ErrGetBaseConfigNotSupported is the error // ErrGetBaseConfigNotSupported is the error

View File

@ -137,7 +137,7 @@ func (m resolvedManager) SetDNS(config OSConfig) error {
var linkDomains = make([]resolvedLinkDomain, len(config.SearchDomains)) var linkDomains = make([]resolvedLinkDomain, len(config.SearchDomains))
for i, domain := range config.SearchDomains { for i, domain := range config.SearchDomains {
linkDomains[i] = resolvedLinkDomain{ linkDomains[i] = resolvedLinkDomain{
Domain: domain, Domain: domain.WithoutTrailingDot(),
RoutingOnly: false, RoutingOnly: false,
} }
} }

View File

@ -102,7 +102,7 @@ func getTxID(packet []byte) txid {
} }
type route struct { type route struct {
suffix string suffix dnsname.FQDN
resolvers []netaddr.IPPort resolvers []netaddr.IPPort
} }
@ -272,7 +272,7 @@ func (f *forwarder) forward(query packet) error {
var resolvers []netaddr.IPPort var resolvers []netaddr.IPPort
for _, route := range routes { for _, route := range routes {
if route.suffix != "." && !dnsname.HasSuffix(domain, route.suffix) { if route.suffix != "." && !route.suffix.Contains(domain) {
continue continue
} }
resolvers = route.resolvers resolvers = route.resolvers
@ -489,7 +489,7 @@ func (c *fwdConn) close() {
} }
// nameFromQuery extracts the normalized query name from bs. // nameFromQuery extracts the normalized query name from bs.
func nameFromQuery(bs []byte) (string, error) { func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
var parser dns.Parser var parser dns.Parser
hdr, err := parser.Start(bs) hdr, err := parser.Start(bs)
@ -506,5 +506,5 @@ func nameFromQuery(bs []byte) (string, error) {
} }
n := q.Name.Data[:q.Name.Length] n := q.Name.Data[:q.Name.Length]
return rawNameToLower(n), nil return dnsname.ToFQDN(rawNameToLower(n))
} }

View File

@ -9,7 +9,6 @@
import ( import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -59,12 +58,12 @@ type Config struct {
// queries within that suffix. // queries within that suffix.
// Queries only match the most specific suffix. // Queries only match the most specific suffix.
// To register a "default route", add an entry for ".". // To register a "default route", add an entry for ".".
Routes map[string][]netaddr.IPPort Routes map[dnsname.FQDN][]netaddr.IPPort
// LocalHosts is a map of FQDNs to corresponding IPs. // LocalHosts is a map of FQDNs to corresponding IPs.
Hosts map[string][]netaddr.IP Hosts map[dnsname.FQDN][]netaddr.IP
// LocalDomains is a list of DNS name suffixes that should not be // LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers. // routed to upstream resolvers.
LocalDomains []string LocalDomains []dnsname.FQDN
} }
// Resolver is a DNS resolver for nodes on the Tailscale network, // Resolver is a DNS resolver for nodes on the Tailscale network,
@ -92,9 +91,9 @@ type Resolver struct {
// mu guards the following fields from being updated while used. // mu guards the following fields from being updated while used.
mu sync.Mutex mu sync.Mutex
localDomains []string localDomains []dnsname.FQDN
hostToIP map[string][]netaddr.IP hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]string ipToHost map[netaddr.IP]dnsname.FQDN
} }
// New returns a new resolver. // New returns a new resolver.
@ -107,8 +106,8 @@ func New(logf logger.Logf, linkMon *monitor.Mon) *Resolver {
responses: make(chan packet), responses: make(chan packet),
errors: make(chan error), errors: make(chan error),
closed: make(chan struct{}), closed: make(chan struct{}),
hostToIP: map[string][]netaddr.IP{}, hostToIP: map[dnsname.FQDN][]netaddr.IP{},
ipToHost: map[netaddr.IP]string{}, ipToHost: map[netaddr.IP]dnsname.FQDN{},
} }
r.forwarder = newForwarder(r.logf, r.responses) r.forwarder = newForwarder(r.logf, r.responses)
if r.linkMon != nil { if r.linkMon != nil {
@ -121,10 +120,6 @@ func New(logf logger.Logf, linkMon *monitor.Mon) *Resolver {
return r return r
} }
func isFQDN(s string) bool {
return strings.HasSuffix(s, ".")
}
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook } func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
func (r *Resolver) SetConfig(cfg Config) error { func (r *Resolver) SetConfig(cfg Config) error {
@ -133,26 +128,15 @@ func (r *Resolver) SetConfig(cfg Config) error {
} }
routes := make([]route, 0, len(cfg.Routes)) routes := make([]route, 0, len(cfg.Routes))
reverse := make(map[netaddr.IP]string, len(cfg.Hosts)) reverse := make(map[netaddr.IP]dnsname.FQDN, len(cfg.Hosts))
for host, ips := range cfg.Hosts { for host, ips := range cfg.Hosts {
if !isFQDN(host) {
return fmt.Errorf("host entry %q is not a FQDN", host)
}
for _, ip := range ips { for _, ip := range ips {
reverse[ip] = host reverse[ip] = host
} }
} }
for _, domain := range cfg.LocalDomains {
if !isFQDN(domain) {
return fmt.Errorf("local domain %q is not a FQDN", domain)
}
}
for suffix, ips := range cfg.Routes { for suffix, ips := range cfg.Routes {
if !strings.HasSuffix(suffix, ".") {
return fmt.Errorf("route suffix %q is not a FQDN", suffix)
}
routes = append(routes, route{ routes = append(routes, route{
suffix: suffix, suffix: suffix,
resolvers: ips, resolvers: ips,
@ -160,7 +144,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
} }
// Sort from longest prefix to shortest. // Sort from longest prefix to shortest.
sort.Slice(routes, func(i, j int) bool { sort.Slice(routes, func(i, j int) bool {
return dnsname.NumLabels(routes[i].suffix) > dnsname.NumLabels(routes[j].suffix) return routes[i].suffix.NumLabels() > routes[j].suffix.NumLabels()
}) })
r.forwarder.setRoutes(routes) r.forwarder.setRoutes(routes)
@ -229,12 +213,11 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
// resolveLocal returns an IP for the given domain, if domain is in // resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested // the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL). // typ (A, AAAA, ALL).
// The domain name must be in canonical form (with a trailing period).
// Returns dns.RCodeRefused to indicate that the local map is not // Returns dns.RCodeRefused to indicate that the local map is not
// authoritative for domain. // authoritative for domain.
func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RCode) { func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, dns.RCode) {
// Reject .onion domains per RFC 7686. // Reject .onion domains per RFC 7686.
if dnsname.HasSuffix(domain, ".onion") { if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
return netaddr.IP{}, dns.RCodeNameError return netaddr.IP{}, dns.RCodeNameError
} }
@ -246,7 +229,7 @@ func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RC
addrs, found := hosts[domain] addrs, found := hosts[domain]
if !found { if !found {
for _, suffix := range localDomains { for _, suffix := range localDomains {
if dnsname.HasSuffix(domain, suffix) { if suffix.Contains(domain) {
// We are authoritative for the queried domain. // We are authoritative for the queried domain.
return netaddr.IP{}, dns.RCodeNameError return netaddr.IP{}, dns.RCodeNameError
} }
@ -304,8 +287,7 @@ func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RC
} }
// resolveReverse returns the unique domain name that maps to the given address. // resolveReverse returns the unique domain name that maps to the given address.
// The returned domain name is in canonical form (with a trailing period). func (r *Resolver) resolveLocalReverse(ip netaddr.IP) (dnsname.FQDN, dns.RCode) {
func (r *Resolver) resolveLocalReverse(ip netaddr.IP) (string, dns.RCode) {
r.mu.Lock() r.mu.Lock()
ips := r.ipToHost ips := r.ipToHost
r.mu.Unlock() r.mu.Unlock()
@ -362,7 +344,7 @@ type response struct {
Header dns.Header Header dns.Header
Question dns.Question Question dns.Question
// Name is the response to a PTR query. // Name is the response to a PTR query.
Name string Name dnsname.FQDN
// IP is the response to an A, AAAA, or ALL query. // IP is the response to an A, AAAA, or ALL query.
IP netaddr.IP IP netaddr.IP
} }
@ -425,7 +407,7 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
// marshalPTRRecord serializes a PTR record into an active builder. // marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call. // The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) error { func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
var answer dns.PTRResource var answer dns.PTRResource
var err error var err error
@ -435,7 +417,7 @@ func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) err
Class: dns.ClassINET, Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second), TTL: uint32(defaultTTL / time.Second),
} }
answer.PTR, err = dns.NewName(name) answer.PTR, err = dns.NewName(name.WithTrailingDot())
if err != nil { if err != nil {
return err return err
} }
@ -508,12 +490,13 @@ func marshalResponse(resp *response) ([]byte, error) {
// r._dns-sd._udp.<domain>. // r._dns-sd._udp.<domain>.
// dr._dns-sd._udp.<domain>. // dr._dns-sd._udp.<domain>.
// lb._dns-sd._udp.<domain>. // lb._dns-sd._udp.<domain>.
func hasRDNSBonjourPrefix(s string) bool { func hasRDNSBonjourPrefix(name dnsname.FQDN) bool {
// Even the shortest name containing a Bonjour prefix is long, // Even the shortest name containing a Bonjour prefix is long,
// so check length (cheap) and bail early if possible. // so check length (cheap) and bail early if possible.
if len(s) < len("*._dns-sd._udp.0.0.0.0.in-addr.arpa.") { if len(name) < len("*._dns-sd._udp.0.0.0.0.in-addr.arpa.") {
return false return false
} }
s := name.WithTrailingDot()
dot := strings.IndexByte(s, '.') dot := strings.IndexByte(s, '.')
if dot == -1 { if dot == -1 {
return false // shouldn't happen return false // shouldn't happen
@ -548,9 +531,9 @@ func rawNameToLower(name []byte) string {
// 4.3.2.1.in-addr.arpa // 4.3.2.1.in-addr.arpa
// is transformed to // is transformed to
// 1.2.3.4 // 1.2.3.4
func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) { func rdnsNameToIPv4(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
name = strings.TrimSuffix(name, rdnsv4Suffix) s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv4Suffix)
ip, err := netaddr.ParseIP(string(name)) ip, err := netaddr.ParseIP(s)
if err != nil { if err != nil {
return netaddr.IP{}, false return netaddr.IP{}, false
} }
@ -567,21 +550,21 @@ func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) {
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. // b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
// is transformed to // is transformed to
// 2001:db8::567:89ab // 2001:db8::567:89ab
func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) { func rdnsNameToIPv6(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
var b [32]byte var b [32]byte
var ipb [16]byte var ipb [16]byte
name = strings.TrimSuffix(name, rdnsv6Suffix) s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv6Suffix)
// 32 nibbles and 31 dots between them. // 32 nibbles and 31 dots between them.
if len(name) != 63 { if len(s) != 63 {
return netaddr.IP{}, false return netaddr.IP{}, false
} }
// Dots and hex digits alternate. // Dots and hex digits alternate.
prevDot := true prevDot := true
// i ranges over name backward; j ranges over b forward. // i ranges over name backward; j ranges over b forward.
for i, j := len(name)-1, 0; i >= 0; i-- { for i, j := len(s)-1, 0; i >= 0; i-- {
thisDot := (name[i] == '.') thisDot := (s[i] == '.')
if prevDot == thisDot { if prevDot == thisDot {
return netaddr.IP{}, false return netaddr.IP{}, false
} }
@ -590,7 +573,7 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
if !thisDot { if !thisDot {
// This is safe assuming alternation. // This is safe assuming alternation.
// We do not check that non-dots are hex digits: hex.Decode below will do that. // We do not check that non-dots are hex digits: hex.Decode below will do that.
b[j] = name[i] b[j] = s[i]
j++ j++
} }
} }
@ -605,7 +588,7 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
// respondReverse returns a DNS response to a PTR query. // respondReverse returns a DNS response to a PTR query.
// It is assumed that resp.Question is populated by respond before this is called. // It is assumed that resp.Question is populated by respond before this is called.
func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]byte, error) { func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) { if hasRDNSBonjourPrefix(name) {
return nil, errNotOurName return nil, errNotOurName
} }
@ -613,9 +596,9 @@ func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]
var ip netaddr.IP var ip netaddr.IP
var ok bool var ok bool
switch { switch {
case strings.HasSuffix(name, rdnsv4Suffix): case strings.HasSuffix(name.WithTrailingDot(), rdnsv4Suffix):
ip, ok = rdnsNameToIPv4(name) ip, ok = rdnsNameToIPv4(name)
case strings.HasSuffix(name, rdnsv6Suffix): case strings.HasSuffix(name.WithTrailingDot(), rdnsv6Suffix):
ip, ok = rdnsNameToIPv6(name) ip, ok = rdnsNameToIPv6(name)
default: default:
return nil, errNotOurName return nil, errNotOurName
@ -656,7 +639,12 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
return marshalResponse(resp) return marshalResponse(resp)
} }
rawName := resp.Question.Name.Data[:resp.Question.Name.Length] rawName := resp.Question.Name.Data[:resp.Question.Name.Length]
name := rawNameToLower(rawName) name, err := dnsname.ToFQDN(rawNameToLower(rawName))
if err != nil {
// DNS packet unexpectedly contains an invalid FQDN.
resp.Header.RCode = dns.RCodeFormatError
return marshalResponse(resp)
}
// Always try to handle reverse lookups; delegate inside when not found. // Always try to handle reverse lookups; delegate inside when not found.
// This way, queries for existent nodes do not leak, // This way, queries for existent nodes do not leak,

View File

@ -13,23 +13,24 @@
dns "golang.org/x/net/dns/dnsmessage" dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/util/dnsname"
) )
var testipv4 = netaddr.MustParseIP("1.2.3.4") var testipv4 = netaddr.MustParseIP("1.2.3.4")
var testipv6 = netaddr.MustParseIP("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f") var testipv6 = netaddr.MustParseIP("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f")
var dnsCfg = Config{ var dnsCfg = Config{
Hosts: map[string][]netaddr.IP{ Hosts: map[dnsname.FQDN][]netaddr.IP{
"test1.ipn.dev.": []netaddr.IP{testipv4}, "test1.ipn.dev.": []netaddr.IP{testipv4},
"test2.ipn.dev.": []netaddr.IP{testipv6}, "test2.ipn.dev.": []netaddr.IP{testipv6},
}, },
LocalDomains: []string{"ipn.dev."}, LocalDomains: []dnsname.FQDN{"ipn.dev."},
} }
func dnspacket(domain string, tp dns.Type) []byte { func dnspacket(domain dnsname.FQDN, tp dns.Type) []byte {
var dnsHeader dns.Header var dnsHeader dns.Header
question := dns.Question{ question := dns.Question{
Name: dns.MustNewName(domain), Name: dns.MustNewName(domain.WithTrailingDot()),
Type: tp, Type: tp,
Class: dns.ClassINET, Class: dns.ClassINET,
} }
@ -44,7 +45,7 @@ func dnspacket(domain string, tp dns.Type) []byte {
type dnsResponse struct { type dnsResponse struct {
ip netaddr.IP ip netaddr.IP
name string name dnsname.FQDN
rcode dns.RCode rcode dns.RCode
} }
@ -94,7 +95,10 @@ func unpackResponse(payload []byte) (dnsResponse, error) {
if err != nil { if err != nil {
return response, err return response, err
} }
response.name = res.NS.String() response.name, err = dnsname.ToFQDN(res.NS.String())
if err != nil {
return response, err
}
default: default:
return response, errors.New("type not in {A, AAAA, NS}") return response, errors.New("type not in {A, AAAA, NS}")
} }
@ -119,7 +123,7 @@ func mustIP(str string) netaddr.IP {
func TestRDNSNameToIPv4(t *testing.T) { func TestRDNSNameToIPv4(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input dnsname.FQDN
wantIP netaddr.IP wantIP netaddr.IP
wantOK bool wantOK bool
}{ }{
@ -144,7 +148,7 @@ func TestRDNSNameToIPv4(t *testing.T) {
func TestRDNSNameToIPv6(t *testing.T) { func TestRDNSNameToIPv6(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input dnsname.FQDN
wantIP netaddr.IP wantIP netaddr.IP
wantOK bool wantOK bool
}{ }{
@ -194,7 +198,7 @@ func TestResolveLocal(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
qname string qname dnsname.FQDN
qtype dns.Type qtype dns.Type
ip netaddr.IP ip netaddr.IP
code dns.RCode code dns.RCode
@ -235,7 +239,7 @@ func TestResolveLocalReverse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
ip netaddr.IP ip netaddr.IP
want string want dnsname.FQDN
code dns.RCode code dns.RCode
}{ }{
{"ipv4", testipv4, "test1.ipn.dev.", dns.RCodeSuccess}, {"ipv4", testipv4, "test1.ipn.dev.", dns.RCodeSuccess},
@ -285,7 +289,7 @@ func TestDelegate(t *testing.T) {
defer r.Close() defer r.Close()
cfg := dnsCfg cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{ cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": { ".": {
netaddr.MustParseIPPort(v4server.PacketConn.LocalAddr().String()), netaddr.MustParseIPPort(v4server.PacketConn.LocalAddr().String()),
netaddr.MustParseIPPort(v6server.PacketConn.LocalAddr().String()), netaddr.MustParseIPPort(v6server.PacketConn.LocalAddr().String()),
@ -360,7 +364,7 @@ func TestDelegateSplitRoute(t *testing.T) {
defer r.Close() defer r.Close()
cfg := dnsCfg cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{ cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": {netaddr.MustParseIPPort(server1.PacketConn.LocalAddr().String())}, ".": {netaddr.MustParseIPPort(server1.PacketConn.LocalAddr().String())},
"other.": {netaddr.MustParseIPPort(server2.PacketConn.LocalAddr().String())}, "other.": {netaddr.MustParseIPPort(server2.PacketConn.LocalAddr().String())},
} }
@ -417,7 +421,7 @@ func TestDelegateCollision(t *testing.T) {
defer r.Close() defer r.Close()
cfg := dnsCfg cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{ cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": { ".": {
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()), netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
}, },
@ -425,7 +429,7 @@ func TestDelegateCollision(t *testing.T) {
r.SetConfig(cfg) r.SetConfig(cfg)
packets := []struct { packets := []struct {
qname string qname dnsname.FQDN
qtype dns.Type qtype dns.Type
addr netaddr.IPPort addr netaddr.IPPort
}{ }{
@ -692,7 +696,7 @@ func TestAllocs(t *testing.T) {
func TestTrimRDNSBonjourPrefix(t *testing.T) { func TestTrimRDNSBonjourPrefix(t *testing.T) {
tests := []struct { tests := []struct {
in string in dnsname.FQDN
want bool want bool
}{ }{
{"b._dns-sd._udp.0.10.20.172.in-addr.arpa.", true}, {"b._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
@ -702,7 +706,6 @@ func TestTrimRDNSBonjourPrefix(t *testing.T) {
{"lb._dns-sd._udp.0.10.20.172.in-addr.arpa.", true}, {"lb._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
{"qq._dns-sd._udp.0.10.20.172.in-addr.arpa.", false}, {"qq._dns-sd._udp.0.10.20.172.in-addr.arpa.", false},
{"0.10.20.172.in-addr.arpa.", false}, {"0.10.20.172.in-addr.arpa.", false},
{"i-have-no-dot", false},
} }
for _, test := range tests { for _, test := range tests {
@ -722,7 +725,7 @@ func BenchmarkFull(b *testing.B) {
defer r.Close() defer r.Close()
cfg := dnsCfg cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{ cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": { ".": {
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()), netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
}, },

View File

@ -5,45 +5,134 @@
// Package dnsname contains string functions for working with DNS names. // Package dnsname contains string functions for working with DNS names.
package dnsname package dnsname
import "strings" import (
"fmt"
"strings"
)
var separators = map[byte]bool{ const (
' ': true, // maxLabelLength is the maximum length of a label permitted by RFC 1035.
'.': true, maxLabelLength = 63
'@': true, // maxNameLength is the maximum length of a DNS name.
'_': true, maxNameLength = 253
} )
func islower(c byte) bool { // A FQDN is a fully-qualified DNS name or name suffix.
return 'a' <= c && c <= 'z' type FQDN string
}
func isupper(c byte) bool { func ToFQDN(s string) (FQDN, error) {
return 'A' <= c && c <= 'Z' if isValidFQDN(s) {
} return FQDN(s), nil
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
} }
if len(s) == 0 {
return FQDN("."), nil
}
if s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
if len(s) > maxNameLength {
return "", fmt.Errorf("%q is too long to be a DNS name", s)
}
fs := strings.Split(s, ".")
for _, f := range fs {
if !validLabel(f) {
return "", fmt.Errorf("%q is not a valid DNS label", f)
}
}
return FQDN(s + "."), nil
} }
// maxLabelLength is the maximal length of a label permitted by RFC 1035. func validLabel(s string) bool {
const maxLabelLength = 63 if len(s) == 0 || len(s) > maxLabelLength {
return false
}
if !isalphanum(s[0]) || !isalphanum(s[len(s)-1]) {
return false
}
for i := 1; i < len(s)-1; i++ {
if !isalphanum(s[i]) && s[i] != '-' {
return false
}
}
return true
}
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
}
// WithoutTrailingDot returns f as a string, with the trailing dot
// removed.
func (f FQDN) WithoutTrailingDot() string {
return string(f[:len(f)-1])
}
func (f FQDN) NumLabels() int {
if f == "." {
return 0
}
return strings.Count(f.WithTrailingDot(), ".")
}
func (f FQDN) Contains(other FQDN) bool {
if f == other {
return true
}
cmp := f.WithTrailingDot()
if cmp != "." {
cmp = "." + cmp
}
return strings.HasSuffix(other.WithTrailingDot(), cmp)
}
// isValidFQDN reports whether s is already a valid FQDN, without
// allocating.
func isValidFQDN(s string) bool {
if len(s) == 0 {
return false
}
if len(s) > maxNameLength {
return false
}
// DNS root name.
if s == "." {
return true
}
// Missing trailing dot.
if s[len(s)-1] != '.' {
return false
}
// Leading dots not allowed.
if s[0] == '.' {
return false
}
st := 0
for i := 0; i < len(s); i++ {
if s[i] != '.' {
continue
}
label := s[st:i]
if len(label) == 0 || len(label) > maxLabelLength {
return false
}
if !isalphanum(label[0]) || !isalphanum(label[len(label)-1]) {
return false
}
for j := 1; j < len(label)-1; j++ {
if !isalphanum(label[j]) && label[j] != '-' {
return false
}
}
st = i + 1
}
return true
}
// SanitizeLabel takes a string intended to be a DNS name label // SanitizeLabel takes a string intended to be a DNS name label
// and turns it into a valid name label according to RFC 1035. // and turns it into a valid name label according to RFC 1035.
@ -133,3 +222,38 @@ func NumLabels(hostname string) int {
} }
return strings.Count(hostname, ".") return strings.Count(hostname, ".")
} }
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
}
}

View File

@ -9,6 +9,87 @@
"testing" "testing"
) )
func TestFQDN(t *testing.T) {
tests := []struct {
in string
want FQDN
wantErr bool
wantLabels int
}{
{"", ".", false, 0},
{".", ".", false, 0},
{"foo.com", "foo.com.", false, 2},
{"foo.com.", "foo.com.", false, 2},
{"com", "com.", false, 1},
{"www.tailscale.com", "www.tailscale.com.", false, 3},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0},
{strings.Repeat("aaaaa.", 60) + "com", "", true, 0},
{".com", "", true, 0},
{"foo..com", "", true, 0},
}
for _, test := range tests {
t.Run(test.in, func(t *testing.T) {
got, err := ToFQDN(test.in)
if got != test.want {
t.Errorf("ToFQDN(%q) got %q, want %q", test.in, got, test.want)
}
if (err != nil) != test.wantErr {
t.Errorf("ToFQDN(%q) err %v, wantErr=%v", test.in, err, test.wantErr)
}
if err != nil {
return
}
gotDot := got.WithTrailingDot()
if gotDot != string(test.want) {
t.Errorf("ToFQDN(%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("ToFQDN(%q).WithoutTrailingDot() got %q, want %q", test.in, gotNoDot, wantNoDot)
}
if gotLabels := got.NumLabels(); gotLabels != test.wantLabels {
t.Errorf("ToFQDN(%q).NumLabels() got %v, want %v", test.in, gotLabels, test.wantLabels)
}
})
}
}
func TestFQDNContains(t *testing.T) {
tests := []struct {
a, b string
want bool
}{
{"", "", true},
{"", "foo.com", true},
{"foo.com", "", false},
{"tailscale.com", "www.tailscale.com", true},
{"www.tailscale.com", "tailscale.com", false},
{"scale.com", "tailscale.com", false},
{"foo.com", "foo.com", true},
}
for _, test := range tests {
t.Run(test.a+"_"+test.b, func(t *testing.T) {
a, err := ToFQDN(test.a)
if err != nil {
t.Fatalf("ToFQDN(%q): %v", test.a, err)
}
b, err := ToFQDN(test.b)
if err != nil {
t.Fatalf("ToFQDN(%q): %v", test.b, err)
}
if got := a.Contains(b); got != test.want {
t.Errorf("ToFQDN(%q).Contains(%q) got %v, want %v", a, b, got, test.want)
}
})
}
}
func TestSanitizeLabel(t *testing.T) { func TestSanitizeLabel(t *testing.T) {
tests := []struct { tests := []struct {
name string name string