WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -72,6 +72,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "self_name_and_peers",
|
||||
os: "windows", // the full Hosts map is built only for the Windows hosts-file path
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "myname.net.",
|
||||
@@ -108,6 +109,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "subdomain_resolve_capability",
|
||||
os: "windows", // the full Hosts map is built only for the Windows hosts-file path
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "myname.net.",
|
||||
@@ -136,7 +138,6 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
"peer-with-cap.net.": ips("100.102.0.1"),
|
||||
"peer-without-cap.net.": ips("100.102.0.2"),
|
||||
},
|
||||
SubdomainHosts: set.Of[dnsname.FQDN]("myname.net.", "peer-with-cap.net."),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,6 +145,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
// should get IPv6 records for all its peers,
|
||||
// even if they have IPv4.
|
||||
name: "v6_only_self",
|
||||
os: "windows", // the full Hosts map is built only for the Windows hosts-file path
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "myname.net.",
|
||||
@@ -198,9 +200,8 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"myname.net.": ips("100.101.101.101"),
|
||||
"foo.com.": ips("1.2.3.4"),
|
||||
"bar.com.": ips("1::6"),
|
||||
"foo.com.": ips("1.2.3.4"),
|
||||
"bar.com.": ips("1::6"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -425,23 +426,21 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
want: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"a.": ips("100.101.101.101"),
|
||||
"p1.": ips("100.102.0.1"),
|
||||
},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
|
||||
dnsname.FQDN("example.com."): {
|
||||
{Addr: "tailscale-app:app1"},
|
||||
},
|
||||
},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
verOS := cmp.Or(tt.os, "linux")
|
||||
goos := cmp.Or(tt.os, "linux")
|
||||
var log tstest.MemLogger
|
||||
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), tt.expired, log.Logf, verOS)
|
||||
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), tt.expired, log.Logf, goos)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
gotj, _ := json.MarshalIndent(got, "", "\t")
|
||||
wantj, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
|
||||
@@ -228,6 +228,7 @@ type LocalBackend struct {
|
||||
keyLogf logger.Logf // for printing list of peers on change
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
sys *tsd.System
|
||||
goos string // runtime.GOOS usually, except in tests
|
||||
eventClient *eventbus.Client
|
||||
appcTask execqueue.ExecQueue // handles updates from appc
|
||||
|
||||
@@ -639,6 +640,11 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
e.SetNetLogSource(netLogNodeSource{b})
|
||||
e.SetWGPeerLookup(b.lookupPeerWireGuardString)
|
||||
b.dialer.SetResolveMagicDNS(b.resolveMagicDNS)
|
||||
if buildfeatures.HasDNS {
|
||||
if dm, ok := sys.DNSManager.GetOK(); ok {
|
||||
dm.Resolver().SetMagicDNSHosts(magicDNSHosts{b})
|
||||
}
|
||||
}
|
||||
|
||||
if sys.InitialConfig != nil {
|
||||
if err := b.initPrefsFromConfig(sys.InitialConfig); err != nil {
|
||||
@@ -6095,7 +6101,7 @@ func (b *LocalBackend) authReconfigLocked() {
|
||||
hasPAC := b.interfaceState.HasPAC()
|
||||
disableSubnetsIfPAC := cn.SelfHasCap(tailcfg.NodeAttrDisableSubnetsIfPAC)
|
||||
dohURL, dohURLOK := cn.exitNodeCanProxyDNS(prefs.ExitNodeID())
|
||||
dcfg := cn.dnsConfigForNetmap(prefs, b.keyExpired, version.OS())
|
||||
dcfg := cn.dnsConfigForNetmap(prefs, b.keyExpired, cmp.Or(b.goos, runtime.GOOS))
|
||||
// If the current node is an app connector, ensure the app connector machine is started
|
||||
b.reconfigAppConnectorLocked(nm.SelfNode, prefs)
|
||||
|
||||
|
||||
+151
-59
@@ -305,6 +305,12 @@ func (nb *nodeBackend) NodeByWireGuardString(s string) (_ tailcfg.NodeID, ok boo
|
||||
func (nb *nodeBackend) NodeByID(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
return nb.nodeByIDLocked(id)
|
||||
}
|
||||
|
||||
// nodeByIDLocked returns the node (peer or self) with the given node
|
||||
// ID. nb.mu must be held.
|
||||
func (nb *nodeBackend) nodeByIDLocked(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
|
||||
if nb.netMap != nil {
|
||||
if self := nb.netMap.SelfNode; self.Valid() && self.ID() == id {
|
||||
return self, true
|
||||
@@ -1126,10 +1132,122 @@ func (nb *nodeBackend) setFilter(f *filter.Filter) {
|
||||
nb.filterAtomic.Store(f)
|
||||
}
|
||||
|
||||
func (nb *nodeBackend) dnsConfigForNetmap(prefs ipn.PrefsView, selfExpired bool, versionOS string) *dns.Config {
|
||||
func (nb *nodeBackend) dnsConfigForNetmap(prefs ipn.PrefsView, selfExpired bool, goos string) *dns.Config {
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
return dnsConfigForNetmap(nb.netMap, nb.peers, prefs, selfExpired, nb.logf, versionOS)
|
||||
return dnsConfigForNetmap(nb.netMap, nb.peers, prefs, selfExpired, nb.logf, goos)
|
||||
}
|
||||
|
||||
// magicDNSHostAddrs returns the MagicDNS A/AAAA answer for fqdn from
|
||||
// the live node indexes, and whether the name is known. It backs
|
||||
// [resolver.MagicDNSHosts.LookupHost], replacing the full
|
||||
// dns.Config.Hosts map so that netmap deltas need no per-peer DNS
|
||||
// rebuild.
|
||||
func (nb *nodeBackend) magicDNSHostAddrs(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil, false
|
||||
}
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
n, ok := nb.nodeByFQDNLocked(fqdn)
|
||||
if !ok || n.Addresses().Len() == 0 {
|
||||
return nil, false
|
||||
}
|
||||
nm := nb.netMap
|
||||
selfV6Only := nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs6) &&
|
||||
!nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4)
|
||||
wantAAAA := nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA)
|
||||
return magicDNSAddrs(n.Addresses(), selfV6Only, wantAAAA), true
|
||||
}
|
||||
|
||||
// magicDNSPTR returns the MagicDNS name of the node (peer or self)
|
||||
// that owns ip, backing [resolver.MagicDNSHosts.LookupPTR].
|
||||
func (nb *nodeBackend) magicDNSPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return "", false
|
||||
}
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
nid, ok := nb.nodeByAddr[ip]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
n, ok := nb.nodeByIDLocked(nid)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(n.Name())
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return fqdn, true
|
||||
}
|
||||
|
||||
// magicDNSSubdomainHost reports whether fqdn names a node with the
|
||||
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, backing
|
||||
// [resolver.MagicDNSHosts.SubdomainHost].
|
||||
func (nb *nodeBackend) magicDNSSubdomainHost(fqdn dnsname.FQDN) bool {
|
||||
if !buildfeatures.HasDNS {
|
||||
return false
|
||||
}
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
n, ok := nb.nodeByFQDNLocked(fqdn)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if nm := nb.netMap; nm != nil && nm.SelfNode.Valid() && nm.SelfNode.ID() == n.ID() {
|
||||
return nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve)
|
||||
}
|
||||
return n.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve)
|
||||
}
|
||||
|
||||
// nodeByFQDNLocked returns the node (peer or self) with the given
|
||||
// MagicDNS FQDN. nb.mu must be held.
|
||||
func (nb *nodeBackend) nodeByFQDNLocked(fqdn dnsname.FQDN) (_ tailcfg.NodeView, ok bool) {
|
||||
// The resolver already lowercases query names, but lowercase
|
||||
// again (nearly free when already lowercase) so that no other
|
||||
// caller of the [resolver.MagicDNSHosts] hook can miss on case.
|
||||
nid, ok := nb.nodeByName[strings.ToLower(strings.TrimSuffix(string(fqdn), "."))]
|
||||
if !ok {
|
||||
return tailcfg.NodeView{}, false
|
||||
}
|
||||
return nb.nodeByIDLocked(nid)
|
||||
}
|
||||
|
||||
// magicDNSAddrs returns the MagicDNS answer addresses for a node
|
||||
// owning the given addresses: all of its IPv6 addresses if this node
|
||||
// (the querier) is IPv6-only, otherwise everything except the node's
|
||||
// IPv6 addresses when it also has IPv4, unless wantAAAA. The result
|
||||
// may be empty (name exists, no records of the desired family).
|
||||
func magicDNSAddrs(addrs views.Slice[netip.Prefix], selfV6Only, wantAAAA bool) (ips []netip.Addr) {
|
||||
var have4 bool
|
||||
for _, addr := range addrs.All() {
|
||||
if addr.Addr().Is4() {
|
||||
have4 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, addr := range addrs.All() {
|
||||
if selfV6Only {
|
||||
if addr.Addr().Is6() {
|
||||
ips = append(ips, addr.Addr())
|
||||
}
|
||||
continue
|
||||
}
|
||||
// If this node has an IPv4 address, then remove peers'
|
||||
// IPv6 addresses for now, as we don't guarantee that
|
||||
// the peer node actually can speak IPv6 correctly.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.Addr().Is6() && have4 && !wantAAAA {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.Addr())
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func (nb *nodeBackend) exitNodeCanProxyDNS(exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
|
||||
@@ -1234,9 +1352,9 @@ func useWithExitNodeRoutes(routes map[string][]*dnstype.Resolver) map[string][]*
|
||||
// dnsConfigForNetmap returns a *dns.Config for the given netmap,
|
||||
// prefs, client OS version, and cloud hosting environment.
|
||||
//
|
||||
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
|
||||
// a runtime.GOOS.
|
||||
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired bool, logf logger.Logf, versionOS string) *dns.Config {
|
||||
// The goos is runtime.GOOS usually, except in tests, where it may
|
||||
// vary to test any OS's behavior from any host.
|
||||
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, selfExpired bool, logf logger.Logf, goos string) *dns.Config {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -1269,63 +1387,29 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
|
||||
|
||||
wantAAAA := nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA)
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
// quad-100 can always respond to MagicDNS queries, even if the OS
|
||||
// isn't configured to make MagicDNS resolution truly
|
||||
// magic. Details in
|
||||
// https://github.com/tailscale/tailscale/issues/1886.
|
||||
set := func(name string, addrs views.Slice[netip.Prefix]) {
|
||||
if addrs.Len() == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
var have4 bool
|
||||
for _, addr := range addrs.All() {
|
||||
if addr.Addr().Is4() {
|
||||
have4 = true
|
||||
break
|
||||
// Populate MagicDNS records. The internal quad-100 resolver pulls
|
||||
// per-node records on demand from the live node indexes (see
|
||||
// [nodeBackend.magicDNSHostAddrs]), so quad-100 can always respond
|
||||
// to MagicDNS queries without dcfg.Hosts; details in
|
||||
// https://github.com/tailscale/tailscale/issues/1886. dcfg.Hosts
|
||||
// carries only what must be enumerable in full: control's
|
||||
// DNS.ExtraRecords below, plus every node's records on Windows,
|
||||
// whose hosts-file fallback path (see compileHostEntries in
|
||||
// net/dns) needs the complete set.
|
||||
if goos == "windows" {
|
||||
set := func(name string, addrs views.Slice[netip.Prefix]) {
|
||||
if addrs.Len() == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
var ips []netip.Addr
|
||||
for _, addr := range addrs.All() {
|
||||
if selfV6Only {
|
||||
if addr.Addr().Is6() {
|
||||
ips = append(ips, addr.Addr())
|
||||
}
|
||||
continue
|
||||
fqdn, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return // TODO: propagate error?
|
||||
}
|
||||
// If this node has an IPv4 address, then
|
||||
// remove peers' IPv6 addresses for now, as we
|
||||
// don't guarantee that the peer node actually
|
||||
// can speak IPv6 correctly.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.Addr().Is6() && have4 && !wantAAAA {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.Addr())
|
||||
dcfg.Hosts[fqdn] = magicDNSAddrs(addrs, selfV6Only, wantAAAA)
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
set(nm.SelfName(), nm.GetAddresses())
|
||||
if nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) {
|
||||
if fqdn, err := dnsname.ToFQDN(nm.SelfName()); err == nil {
|
||||
dcfg.SubdomainHosts.Make()
|
||||
dcfg.SubdomainHosts.Add(fqdn)
|
||||
}
|
||||
}
|
||||
for _, peer := range peers {
|
||||
set(peer.Name(), peer.Addresses())
|
||||
if peer.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve) {
|
||||
if fqdn, err := dnsname.ToFQDN(peer.Name()); err == nil {
|
||||
dcfg.SubdomainHosts.Make()
|
||||
dcfg.SubdomainHosts.Add(fqdn)
|
||||
}
|
||||
set(nm.SelfName(), nm.GetAddresses())
|
||||
for _, peer := range peers {
|
||||
set(peer.Name(), peer.Addresses())
|
||||
}
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
@@ -1363,6 +1447,14 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
} else {
|
||||
// Without MagicDNS domain routing, no Routes entry covers the
|
||||
// node records served on demand via [magicDNSHosts]. Tell
|
||||
// dns.Manager they exist, so it keeps quad-100 in the OS
|
||||
// resolver path and the names still resolve, as they did when
|
||||
// they were listed in dcfg.Hosts. Any netmap with a self node
|
||||
// has such records.
|
||||
dcfg.MagicDNSHostsUnrouted = nm.SelfNode.Valid()
|
||||
}
|
||||
|
||||
addDefault := func(resolvers []*dnstype.Resolver) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
@@ -559,3 +560,82 @@ func TestNodeBackendRouteManagerExtras(t *testing.T) {
|
||||
t.Errorf("Outbound still routes %v after extras cleared", transit.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the live MagicDNS lookup methods backing
|
||||
// [resolver.MagicDNSHosts]: forward, reverse, and subdomain-cap
|
||||
// lookups must serve from the node indexes and stay correct across
|
||||
// netmap deltas without any full Hosts map rebuild.
|
||||
func TestNodeBackendMagicDNSHosts(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context(), tstest.WhileTestRunningLogger(t), eventbus.New())
|
||||
|
||||
self := &tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "self.example.ts.net.",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
}
|
||||
p1 := &tailcfg.Node{
|
||||
ID: 2,
|
||||
Key: key.NewNode().Public(),
|
||||
Name: "p1.example.ts.net.",
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.2/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||
},
|
||||
CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil},
|
||||
}
|
||||
nb.SetNetMap(&netmap.NetworkMap{
|
||||
SelfNode: self.View(),
|
||||
Peers: []tailcfg.NodeView{p1.View()},
|
||||
})
|
||||
|
||||
wantHost := func(fqdn dnsname.FQDN, want ...netip.Addr) {
|
||||
t.Helper()
|
||||
ips, ok := nb.magicDNSHostAddrs(fqdn)
|
||||
if len(want) == 0 {
|
||||
if ok {
|
||||
t.Errorf("magicDNSHostAddrs(%q) = %v; want no match", fqdn, ips)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ok || !slices.Equal(ips, want) {
|
||||
t.Errorf("magicDNSHostAddrs(%q) = %v, %v; want %v", fqdn, ips, ok, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The self node has IPv4, so the peer's IPv6 address is
|
||||
// filtered out (issue 1152).
|
||||
wantHost("p1.example.ts.net.", netip.MustParseAddr("100.64.0.2"))
|
||||
wantHost("self.example.ts.net.", netip.MustParseAddr("100.64.0.1"))
|
||||
wantHost("unknown.example.ts.net.")
|
||||
|
||||
if fqdn, ok := nb.magicDNSPTR(netip.MustParseAddr("100.64.0.2")); !ok || fqdn != "p1.example.ts.net." {
|
||||
t.Errorf("magicDNSPTR(100.64.0.2) = %q, %v; want p1's name", fqdn, ok)
|
||||
}
|
||||
if got, want := nb.magicDNSSubdomainHost("p1.example.ts.net."), true; got != want {
|
||||
t.Errorf("magicDNSSubdomainHost(p1) = %v; want %v", got, want)
|
||||
}
|
||||
if got, want := nb.magicDNSSubdomainHost("self.example.ts.net."), false; got != want {
|
||||
t.Errorf("magicDNSSubdomainHost(self) = %v; want %v", got, want)
|
||||
}
|
||||
|
||||
// Removing the peer via a delta drops its records.
|
||||
if _, handled := nb.UpdateNetmapDelta([]netmap.NodeMutation{netmap.MakeNodeMutationRemove(2)}); !handled {
|
||||
t.Fatal("UpdateNetmapDelta not handled")
|
||||
}
|
||||
wantHost("p1.example.ts.net.")
|
||||
if fqdn, ok := nb.magicDNSPTR(netip.MustParseAddr("100.64.0.2")); ok {
|
||||
t.Errorf("magicDNSPTR(100.64.0.2) after removal = %q; want no match", fqdn)
|
||||
}
|
||||
|
||||
// Adding a peer via a delta serves it immediately.
|
||||
p3 := &tailcfg.Node{
|
||||
ID: 3,
|
||||
Key: key.NewNode().Public(),
|
||||
Name: "p3.example.ts.net.",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.3/32")},
|
||||
}
|
||||
if _, handled := nb.UpdateNetmapDelta([]netmap.NodeMutation{netmap.NodeMutationUpsert{Node: p3.View()}}); !handled {
|
||||
t.Fatal("UpdateNetmapDelta not handled")
|
||||
}
|
||||
wantHost("p3.example.ts.net.", netip.MustParseAddr("100.64.0.3"))
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -72,6 +75,37 @@ func (b *LocalBackend) resolveMagicDNS(hostname, network string) (_ netip.Addr,
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// magicDNSHosts implements [resolver.MagicDNSHosts] on top of the
|
||||
// current nodeBackend's live node indexes, so the quad-100 resolver
|
||||
// pulls each MagicDNS answer on demand rather than LocalBackend
|
||||
// pushing a Hosts map of every node into it on every netmap change.
|
||||
// It is installed once at LocalBackend construction; going through
|
||||
// currentNode makes profile switches take effect immediately.
|
||||
type magicDNSHosts struct{ b *LocalBackend }
|
||||
|
||||
func (m magicDNSHosts) LookupHost(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil, false
|
||||
}
|
||||
return m.b.currentNode().magicDNSHostAddrs(fqdn)
|
||||
}
|
||||
|
||||
func (m magicDNSHosts) LookupPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return "", false
|
||||
}
|
||||
return m.b.currentNode().magicDNSPTR(ip)
|
||||
}
|
||||
|
||||
func (m magicDNSHosts) SubdomainHost(fqdn dnsname.FQDN) bool {
|
||||
if !buildfeatures.HasDNS {
|
||||
return false
|
||||
}
|
||||
return m.b.currentNode().magicDNSSubdomainHost(fqdn)
|
||||
}
|
||||
|
||||
var _ resolver.MagicDNSHosts = magicDNSHosts{}
|
||||
|
||||
// nodeAddrForNetwork returns the best address from n for the given
|
||||
// network ("tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"). For
|
||||
// unqualified networks ("tcp", "udp"), it prefers IPv4.
|
||||
|
||||
+25
-37
@@ -42,8 +42,6 @@ import (
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
@@ -1251,23 +1249,6 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
routesWithQuad100 := func(extra ...netip.Prefix) []netip.Prefix {
|
||||
return append(extra, netip.MustParsePrefix("100.100.100.100/32"))
|
||||
}
|
||||
hostsFor := func(nm *netmap.NetworkMap) map[dnsname.FQDN][]netip.Addr {
|
||||
var hosts map[dnsname.FQDN][]netip.Addr
|
||||
appendNode := func(n tailcfg.NodeView) {
|
||||
addrs := make([]netip.Addr, 0, n.Addresses().Len())
|
||||
for _, addr := range n.Addresses().All() {
|
||||
addrs = append(addrs, addr.Addr())
|
||||
}
|
||||
mak.Set(&hosts, must.Get(dnsname.ToFQDN(n.Name())), addrs)
|
||||
}
|
||||
if nm != nil && nm.SelfNode.Valid() {
|
||||
appendNode(nm.SelfNode)
|
||||
}
|
||||
for _, n := range nm.Peers {
|
||||
appendNode(n)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1328,9 +1309,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node1),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1385,9 +1367,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node2),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1434,9 +1417,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node1),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1467,9 +1451,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node3),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1513,9 +1498,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node1),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1544,9 +1530,10 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
Routes: routesWithQuad100(),
|
||||
},
|
||||
wantDNSCfg: &dns.Config{
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: hostsFor(node1),
|
||||
AcceptDNS: true,
|
||||
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{},
|
||||
MagicDNSHostsUnrouted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1572,6 +1559,7 @@ func TestEngineReconfigOnStateChange(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lb, engine, cc := newLocalBackendWithMockEngineAndControl(t, enableLogging)
|
||||
lb.goos = "linux" // so the expectations below are the same on every host OS
|
||||
|
||||
if tt.steps != nil {
|
||||
tt.steps(t, lb, cc)
|
||||
|
||||
+15
-2
@@ -61,6 +61,14 @@ type Config struct {
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
OnlyIPv6 bool
|
||||
// MagicDNSHostsUnrouted is whether MagicDNS host records are
|
||||
// served on demand via [resolver.MagicDNSHosts] (so are not
|
||||
// listed in Hosts) without being covered by any Routes entry;
|
||||
// that is, MagicDNS domain routing is off. It preserves the
|
||||
// effect the node records had when they were listed in Hosts:
|
||||
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
|
||||
// in the OS resolver path so the names still resolve.
|
||||
MagicDNSHostsUnrouted bool
|
||||
}
|
||||
|
||||
var magicDNSDualStack = envknob.RegisterBool("TS_DEBUG_MAGIC_DNS_DUAL_STACK")
|
||||
@@ -127,8 +135,13 @@ func (c Config) hasDefaultIPResolversOnly() bool {
|
||||
// hasHostsWithoutSplitDNSRoutes reports whether c contains any Host entries
|
||||
// that aren't covered by a SplitDNS route suffix.
|
||||
func (c Config) hasHostsWithoutSplitDNSRoutes() bool {
|
||||
// TODO(bradfitz): this could be more efficient, but we imagine
|
||||
// the number of SplitDNS routes and/or hosts will be small.
|
||||
if c.MagicDNSHostsUnrouted {
|
||||
return true
|
||||
}
|
||||
// Hosts is small here on most platforms: per-node records are
|
||||
// served via [resolver.MagicDNSHosts] (accounted for above), so
|
||||
// only control's DNS.ExtraRecords remain. On Windows it still
|
||||
// carries every node's records for the hosts-file path.
|
||||
for host := range c.Hosts {
|
||||
if !c.hasSplitDNSRouteForHost(host) {
|
||||
return true
|
||||
|
||||
@@ -61,13 +61,14 @@ func (src *Config) Clone() *Config {
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ConfigCloneNeedsRegeneration = Config(struct {
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
MagicDNSHostsUnrouted bool
|
||||
}{})
|
||||
|
||||
// Clone duplicates src into dst and reports whether it succeeded.
|
||||
|
||||
+19
-9
@@ -139,16 +139,26 @@ func (v ConfigView) SubdomainHosts() views.Map[dnsname.FQDN, struct{}] {
|
||||
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
func (v ConfigView) OnlyIPv6() bool { return v.ж.OnlyIPv6 }
|
||||
func (v ConfigView) Equal(v2 ConfigView) bool { return v.ж.Equal(v2.ж) }
|
||||
func (v ConfigView) OnlyIPv6() bool { return v.ж.OnlyIPv6 }
|
||||
|
||||
// MagicDNSHostsUnrouted is whether MagicDNS host records are
|
||||
// served on demand via [resolver.MagicDNSHosts] (so are not
|
||||
// listed in Hosts) without being covered by any Routes entry;
|
||||
// that is, MagicDNS domain routing is off. It preserves the
|
||||
// effect the node records had when they were listed in Hosts:
|
||||
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
|
||||
// in the OS resolver path so the names still resolve.
|
||||
func (v ConfigView) MagicDNSHostsUnrouted() bool { return v.ж.MagicDNSHostsUnrouted }
|
||||
func (v ConfigView) Equal(v2 ConfigView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ConfigViewNeedsRegeneration = Config(struct {
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
AcceptDNS bool
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
SearchDomains []dnsname.FQDN
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
OnlyIPv6 bool
|
||||
MagicDNSHostsUnrouted bool
|
||||
}{})
|
||||
|
||||
@@ -229,6 +229,53 @@ type Resolver struct {
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
subdomainHosts set.Set[dnsname.FQDN]
|
||||
magicHosts MagicDNSHosts // or nil if none installed
|
||||
}
|
||||
|
||||
// MagicDNSHosts is a live source of MagicDNS host records, installed
|
||||
// via [Resolver.SetMagicDNSHosts].
|
||||
//
|
||||
// It replaces the per-node entries of [Config.Hosts]: instead of the
|
||||
// caller pushing a full snapshot of every node's name and addresses
|
||||
// into the resolver on every (possibly incremental) netmap change,
|
||||
// the resolver pulls the answer for one name on demand from the
|
||||
// caller's live indexes. [Config.Hosts] remains for control's
|
||||
// DNS.ExtraRecords entries, which are few, and is consulted first.
|
||||
//
|
||||
// Implementations must be safe for concurrent use and cheap: the
|
||||
// methods are called on the DNS query serving path. Name lookups are
|
||||
// case-insensitive: the resolver passes lowercase names, but
|
||||
// implementations must not rely on that.
|
||||
type MagicDNSHosts interface {
|
||||
// LookupHost returns the IPs to answer for the node with the
|
||||
// given MagicDNS FQDN, and whether the name is known. It returns
|
||||
// all answerable IPs regardless of record type; the resolver
|
||||
// filters them by the query's type, and a known name with no IPs
|
||||
// of the query's family is "name exists, no records", not
|
||||
// NXDOMAIN.
|
||||
LookupHost(dnsname.FQDN) (ips []netip.Addr, ok bool)
|
||||
|
||||
// LookupPTR returns the MagicDNS FQDN of the node that owns
|
||||
// the given Tailscale IP, and whether the IP is known.
|
||||
LookupPTR(netip.Addr) (_ dnsname.FQDN, ok bool)
|
||||
|
||||
// SubdomainHost reports whether fqdn names a node with the
|
||||
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, whose
|
||||
// subdomains all resolve to the node's own addresses.
|
||||
SubdomainHost(dnsname.FQDN) bool
|
||||
}
|
||||
|
||||
// SetMagicDNSHosts installs the live MagicDNS host source consulted
|
||||
// by forward and reverse MagicDNS lookups that miss [Config.Hosts].
|
||||
// It is expected to be called once, before the resolver serves
|
||||
// queries.
|
||||
func (r *Resolver) SetMagicDNSHosts(h MagicDNSHosts) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.magicHosts = h
|
||||
}
|
||||
|
||||
type ForwardLinkSelector interface {
|
||||
@@ -682,15 +729,23 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
hosts := r.hostToIP
|
||||
localDomains := r.localDomains
|
||||
subdomainHosts := r.subdomainHosts
|
||||
magicHosts := r.magicHosts
|
||||
r.mu.Unlock()
|
||||
|
||||
addrs, found := hosts[domain]
|
||||
if !found && magicHosts != nil {
|
||||
addrs, found = magicHosts.LookupHost(domain)
|
||||
}
|
||||
if !found {
|
||||
for parent := domain.Parent(); parent != ""; parent = parent.Parent() {
|
||||
if subdomainHosts.Contains(parent) {
|
||||
addrs, found = hosts[parent]
|
||||
break
|
||||
}
|
||||
if magicHosts != nil && magicHosts.SubdomainHost(parent) {
|
||||
addrs, found = magicHosts.LookupHost(parent)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
@@ -865,6 +920,9 @@ func (r *Resolver) fqdnForIPLocked(ip netip.Addr, name dnsname.FQDN) (dnsname.FQ
|
||||
}
|
||||
|
||||
ret, ok := r.ipToHost[ip]
|
||||
if !ok && r.magicHosts != nil {
|
||||
ret, ok = r.magicHosts.LookupPTR(ip)
|
||||
}
|
||||
if !ok {
|
||||
for _, suffix := range r.localDomains {
|
||||
if suffix.Contains(name) {
|
||||
|
||||
@@ -476,6 +476,111 @@ func TestResolveLocalSubdomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeMagicDNSHosts is a MagicDNSHosts for tests, serving from fixed maps.
|
||||
type fakeMagicDNSHosts struct {
|
||||
hosts map[dnsname.FQDN][]netip.Addr
|
||||
subdomain set.Set[dnsname.FQDN]
|
||||
ptr map[netip.Addr]dnsname.FQDN
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) LookupHost(fqdn dnsname.FQDN) (ips []netip.Addr, ok bool) {
|
||||
ips, ok = f.hosts[fqdn]
|
||||
return ips, ok
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) LookupPTR(ip netip.Addr) (_ dnsname.FQDN, ok bool) {
|
||||
name, ok := f.ptr[ip]
|
||||
return name, ok
|
||||
}
|
||||
|
||||
func (f fakeMagicDNSHosts) SubdomainHost(fqdn dnsname.FQDN) bool {
|
||||
return f.subdomain.Contains(fqdn)
|
||||
}
|
||||
|
||||
// Tests forward, subdomain, and reverse resolution served on demand
|
||||
// via the MagicDNSHosts hook, and that entries pushed via Config.Hosts
|
||||
// take precedence over the hook.
|
||||
func TestResolveLocalMagicDNSHosts(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
r.SetConfig(Config{
|
||||
Hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"extra.ipn.dev.": {netip.MustParseAddr("100.100.1.1")},
|
||||
"both.ipn.dev.": {netip.MustParseAddr("100.100.2.2")},
|
||||
},
|
||||
LocalDomains: []dnsname.FQDN{"ipn.dev.", "64.100.in-addr.arpa."},
|
||||
})
|
||||
node4 := netip.MustParseAddr("100.64.0.7")
|
||||
node6 := netip.MustParseAddr("fd7a:115c:a1e0::7")
|
||||
r.SetMagicDNSHosts(fakeMagicDNSHosts{
|
||||
hosts: map[dnsname.FQDN][]netip.Addr{
|
||||
"node.ipn.dev.": {node4, node6},
|
||||
"v4only.ipn.dev.": {node4},
|
||||
"subber.ipn.dev.": {node4},
|
||||
"both.ipn.dev.": {netip.MustParseAddr("100.100.9.9")}, // masked by Config.Hosts
|
||||
},
|
||||
subdomain: set.Of[dnsname.FQDN]("subber.ipn.dev."),
|
||||
ptr: map[netip.Addr]dnsname.FQDN{node4: "node.ipn.dev."},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
qname dnsname.FQDN
|
||||
qtype dns.Type
|
||||
ip netip.Addr
|
||||
code dns.RCode
|
||||
}{
|
||||
{"hook-ipv4", "node.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-ipv6", "node.ipn.dev.", dns.TypeAAAA, node6, dns.RCodeSuccess},
|
||||
// A known name with no records of the queried family is
|
||||
// "name exists, no records", not NXDOMAIN.
|
||||
{"hook-no-ipv6", "v4only.ipn.dev.", dns.TypeAAAA, netip.Addr{}, dns.RCodeSuccess},
|
||||
{"hook-nxdomain", "gone.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError},
|
||||
{"hook-foreign", "google.com.", dns.TypeA, netip.Addr{}, dns.RCodeRefused},
|
||||
{"hook-subdomain", "foo.subber.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-subdomain-deep", "bar.foo.subber.ipn.dev.", dns.TypeA, node4, dns.RCodeSuccess},
|
||||
{"hook-subdomain-no-cap", "foo.node.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError},
|
||||
// Config.Hosts entries (control's ExtraRecords) are
|
||||
// consulted before the hook.
|
||||
{"config-hosts", "extra.ipn.dev.", dns.TypeA, netip.MustParseAddr("100.100.1.1"), dns.RCodeSuccess},
|
||||
{"config-hosts-precedence", "both.ipn.dev.", dns.TypeA, netip.MustParseAddr("100.100.2.2"), dns.RCodeSuccess},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, code := r.resolveLocal(tt.qname, tt.qtype)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if ip != tt.ip {
|
||||
t.Errorf("ip = %v; want %v", ip, tt.ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
revTests := []struct {
|
||||
name string
|
||||
q dnsname.FQDN
|
||||
want dnsname.FQDN
|
||||
code dns.RCode
|
||||
}{
|
||||
{"hook-ptr", "7.0.64.100.in-addr.arpa.", "node.ipn.dev.", dns.RCodeSuccess},
|
||||
{"hook-ptr-nxdomain", "8.0.64.100.in-addr.arpa.", "", dns.RCodeNameError},
|
||||
{"hook-ptr-foreign", "5.4.3.2.in-addr.arpa.", "", dns.RCodeRefused},
|
||||
}
|
||||
for _, tt := range revTests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, code := r.resolveLocalReverse(tt.q)
|
||||
if code != tt.code {
|
||||
t.Errorf("code = %v; want %v", code, tt.code)
|
||||
}
|
||||
if name != tt.want {
|
||||
t.Errorf("name = %v; want %v", name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLocalReverse(t *testing.T) {
|
||||
r := newResolver(t)
|
||||
defer r.Close()
|
||||
|
||||
Reference in New Issue
Block a user