mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
util/dnsname: add FQDN type, use throughout codebase.
Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
7a1813fd24
commit
1a371b93be
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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()),
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user