You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
tailscale/ipn/ipnlocal/dnsconfig_test.go

548 lines
15 KiB

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"cmp"
"encoding/json"
"net/netip"
"reflect"
"testing"
"tailscale.com/appc"
"tailscale.com/ipn"
"tailscale.com/net/dns"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
func ipps(ippStrs ...string) (ipps []netip.Prefix) {
for _, s := range ippStrs {
if ip, err := netip.ParseAddr(s); err == nil {
ipps = append(ipps, netip.PrefixFrom(ip, ip.BitLen()))
continue
}
ipps = append(ipps, netip.MustParsePrefix(s))
}
return
}
func ips(ss ...string) (ips []netip.Addr) {
for _, s := range ss {
ips = append(ips, netip.MustParseAddr(s))
}
return
}
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
nv := make([]tailcfg.NodeView, len(v))
for i, n := range v {
nv[i] = n.View()
}
return nv
}
func TestDNSConfigForNetmap(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
expired bool
peers []tailcfg.NodeView
os string // version.OS value; empty means linux
cloud cloudenv.Cloud
prefs *ipn.Prefs
want *dns.Config
wantLog string
}{
{
name: "empty",
nm: &netmap.NetworkMap{},
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{},
},
},
{
name: "self_name_and_peers",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("100.101.101.101"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{
"b.net.": ips("100.102.0.1", "100.102.0.2"),
"myname.net.": ips("100.101.101.101"),
"peera.net.": ips("100.102.0.1", "100.102.0.2"),
"v6-only.net.": ips("fe75::3"),
},
},
},
{
name: "subdomain_resolve_capability",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("100.101.101.101"),
}).View(),
AllCaps: set.SetOf([]tailcfg.NodeCapability{tailcfg.NodeAttrDNSSubdomainResolve}),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peer-with-cap.net.",
Addresses: ipps("100.102.0.1"),
CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil},
},
{
ID: 2,
Name: "peer-without-cap.net.",
Addresses: ipps("100.102.0.2"),
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{
"myname.net.": ips("100.101.101.101"),
"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."),
},
},
{
// An ephemeral node with only an IPv6 address
// should get IPv6 records for all its peers,
// even if they have IPv4.
name: "v6_only_self",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("fe75::1"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net.",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
OnlyIPv6: true,
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{
"b.net.": ips("fe75::2"),
"myname.net.": ips("fe75::1"),
"peera.net.": ips("fe75::1001"),
"v6-only.net.": ips("fe75::3"),
},
},
},
{
name: "extra_records",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("100.101.101.101"),
}).View(),
DNS: tailcfg.DNSConfig{
ExtraRecords: []tailcfg.DNSRecord{
{Name: "foo.com", Value: "1.2.3.4"},
{Name: "bar.com", Value: "1::6"},
{Name: "sdlfkjsdklfj", Type: "IGNORE"},
},
},
},
prefs: &ipn.Prefs{},
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"),
},
},
},
{
name: "corp_dns_misc",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "host.some.domain.net.",
}).View(),
DNS: tailcfg.DNSConfig{
Proxied: true,
Domains: []string{"foo.com", "bar.com"},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netip.Addr{},
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
"0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.": nil,
"100.100.in-addr.arpa.": nil,
"101.100.in-addr.arpa.": nil,
"102.100.in-addr.arpa.": nil,
"103.100.in-addr.arpa.": nil,
"104.100.in-addr.arpa.": nil,
"105.100.in-addr.arpa.": nil,
"106.100.in-addr.arpa.": nil,
"107.100.in-addr.arpa.": nil,
"108.100.in-addr.arpa.": nil,
"109.100.in-addr.arpa.": nil,
"110.100.in-addr.arpa.": nil,
"111.100.in-addr.arpa.": nil,
"112.100.in-addr.arpa.": nil,
"113.100.in-addr.arpa.": nil,
"114.100.in-addr.arpa.": nil,
"115.100.in-addr.arpa.": nil,
"116.100.in-addr.arpa.": nil,
"117.100.in-addr.arpa.": nil,
"118.100.in-addr.arpa.": nil,
"119.100.in-addr.arpa.": nil,
"120.100.in-addr.arpa.": nil,
"121.100.in-addr.arpa.": nil,
"122.100.in-addr.arpa.": nil,
"123.100.in-addr.arpa.": nil,
"124.100.in-addr.arpa.": nil,
"125.100.in-addr.arpa.": nil,
"126.100.in-addr.arpa.": nil,
"127.100.in-addr.arpa.": nil,
"64.100.in-addr.arpa.": nil,
"65.100.in-addr.arpa.": nil,
"66.100.in-addr.arpa.": nil,
"67.100.in-addr.arpa.": nil,
"68.100.in-addr.arpa.": nil,
"69.100.in-addr.arpa.": nil,
"70.100.in-addr.arpa.": nil,
"71.100.in-addr.arpa.": nil,
"72.100.in-addr.arpa.": nil,
"73.100.in-addr.arpa.": nil,
"74.100.in-addr.arpa.": nil,
"75.100.in-addr.arpa.": nil,
"76.100.in-addr.arpa.": nil,
"77.100.in-addr.arpa.": nil,
"78.100.in-addr.arpa.": nil,
"79.100.in-addr.arpa.": nil,
"80.100.in-addr.arpa.": nil,
"81.100.in-addr.arpa.": nil,
"82.100.in-addr.arpa.": nil,
"83.100.in-addr.arpa.": nil,
"84.100.in-addr.arpa.": nil,
"85.100.in-addr.arpa.": nil,
"86.100.in-addr.arpa.": nil,
"87.100.in-addr.arpa.": nil,
"88.100.in-addr.arpa.": nil,
"89.100.in-addr.arpa.": nil,
"90.100.in-addr.arpa.": nil,
"91.100.in-addr.arpa.": nil,
"92.100.in-addr.arpa.": nil,
"93.100.in-addr.arpa.": nil,
"94.100.in-addr.arpa.": nil,
"95.100.in-addr.arpa.": nil,
"96.100.in-addr.arpa.": nil,
"97.100.in-addr.arpa.": nil,
"98.100.in-addr.arpa.": nil,
"99.100.in-addr.arpa.": nil,
"some.domain.net.": nil,
},
SearchDomains: []dnsname.FQDN{
"foo.com.",
"bar.com.",
},
},
},
{
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
// Android had cases where it needed FallbackResolvers. This was the
// negative test for the case where Override-local-DNS was set, so the
// fallback resolvers did not need to be used. This test is still valid
// so we keep it, but the fallback test has been removed.
name: "android_does_NOT_need_fallbacks",
os: "android",
nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
Resolvers: []*dnstype.Resolver{
{Addr: "8.8.8.8"},
},
FallbackResolvers: []*dnstype.Resolver{
{Addr: "8.8.4.4"},
},
Routes: map[string][]*dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netip.Addr{},
DefaultResolvers: []*dnstype.Resolver{
{Addr: "8.8.8.8"},
},
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
{
name: "exit_nodes_need_fallbacks",
nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
FallbackResolvers: []*dnstype.Resolver{
{Addr: "8.8.4.4"},
},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
ExitNodeID: "some-id",
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netip.Addr{},
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
DefaultResolvers: []*dnstype.Resolver{
{Addr: "8.8.4.4"},
},
},
},
{
name: "not_exit_node_NOT_need_fallbacks",
nm: &netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
FallbackResolvers: []*dnstype.Resolver{
{Addr: "8.8.4.4"},
},
},
},
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netip.Addr{},
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
},
},
{
name: "self_expired",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "myname.net.",
Addresses: ipps("100.101.101.101"),
}).View(),
},
expired: true,
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{},
},
{
name: "conn25-split-dns",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "a",
Addresses: ipps("100.101.101.101"),
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{
tailcfg.RawMessage(`{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}`),
},
},
}).View(),
AllCaps: set.Of(tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName)),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "p1",
Addresses: ipps("100.102.0.1"),
Tags: []string{"tag:woo"},
Hostinfo: (&tailcfg.Hostinfo{
Services: []tailcfg.Service{
{
Proto: tailcfg.PeerAPI4,
Port: 1234,
},
},
AppConnector: opt.NewBool(true),
}).View(),
},
}),
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netip.Addr{
"a.": ips("100.101.101.101"),
"p1.": ips("100.102.0.1"),
},
Routes: map[dnsname.FQDN][]*dnstype.Resolver{
dnsname.FQDN("example.com."): {
{Addr: "http://100.102.0.1:1234/dns-query"},
},
},
},
},
{
name: "conn25-split-dns-no-matching-peers",
nm: &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "a",
Addresses: ipps("100.101.101.101"),
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{
tailcfg.RawMessage(`{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}`),
},
},
}).View(),
AllCaps: set.Of(tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName)),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "p1",
Addresses: ipps("100.102.0.1"),
Tags: []string{"tag:nomatch"},
Hostinfo: (&tailcfg.Hostinfo{
Services: []tailcfg.Service{
{
Proto: tailcfg.PeerAPI4,
Port: 1234,
},
},
AppConnector: opt.NewBool(true),
}).View(),
},
}),
prefs: &ipn.Prefs{
CorpDNS: true,
},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{
"a.": ips("100.101.101.101"),
"p1.": ips("100.102.0.1"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
verOS := cmp.Or(tt.os, "linux")
var log tstest.MemLogger
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), tt.expired, log.Logf, verOS)
if !reflect.DeepEqual(got, tt.want) {
gotj, _ := json.MarshalIndent(got, "", "\t")
wantj, _ := json.MarshalIndent(tt.want, "", "\t")
t.Errorf("wrong\n got: %s\n\nwant: %s\n", gotj, wantj)
}
if got := log.String(); got != tt.wantLog {
t.Errorf("log output wrong\n got: %q\nwant: %q\n", got, tt.wantLog)
}
})
}
}
func peersMap(s []tailcfg.NodeView) map[tailcfg.NodeID]tailcfg.NodeView {
m := make(map[tailcfg.NodeID]tailcfg.NodeView)
for _, n := range s {
if n.ID() == 0 {
panic("zero Node.ID")
}
m[n.ID()] = n
}
return m
}
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
b := newTestLocalBackend(t)
if b.allowExitNodeDNSProxyToServeName("google.com") {
t.Fatal("unexpected true on backend with nil NetMap")
}
b.currentNode().SetNetMap(&netmap.NetworkMap{
DNS: tailcfg.DNSConfig{
ExitNodeFilteredSet: []string{
".ts.net",
"some.exact.bad",
},
},
})
tests := []struct {
name string
want bool
}{
// Allow by default:
{"google.com", true},
{"GOOGLE.com", true},
// Rejected by suffix:
{"foo.TS.NET", false},
{"foo.ts.net", false},
// Suffix doesn't match
{"ts.net", true},
// Rejected by exact match:
{"some.exact.bad", false},
{"SOME.EXACT.BAD", false},
// But a prefix is okay.
{"prefix-okay.some.exact.bad", true},
}
for _, tt := range tests {
got := b.allowExitNodeDNSProxyToServeName(tt.name)
if got != tt.want {
t.Errorf("for %q = %v; want %v", tt.name, got, tt.want)
}
}
}