diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index 3185cbe2b..ae370aa61 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -39,6 +39,7 @@ import ( "tailscale.com/util/clientmetric" "tailscale.com/util/cloudenv" "tailscale.com/util/dnsname" + "tailscale.com/util/rands" ) const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon." @@ -610,17 +611,17 @@ func stubResolverForOS() (ip netip.Addr, err error) { return ip, nil } -// resolveLocal returns an IP for the given domain, if domain is in -// the local hosts map and has an IP corresponding to the requested +// resolveLocal returns all IPs for the given domain, if domain is in +// the local hosts map and has IPs corresponding to the requested // typ (A, AAAA, ALL). // Returns dns.RCodeRefused to indicate that the local map is not // authoritative for domain. -func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, dns.RCode) { +func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) ([]netip.Addr, dns.RCode) { metricDNSResolveLocal.Add(1) // Reject .onion domains per RFC 7686. if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") { metricDNSResolveLocalErrorOnion.Add(1) - return netip.Addr{}, dns.RCodeNameError + return nil, dns.RCodeNameError } // We return a symbolic domain if someone does a reverse lookup on the @@ -629,14 +630,16 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, if domain == dnsSymbolicFQDN { switch typ { case dns.TypeA: - return tsaddr.TailscaleServiceIP(), dns.RCodeSuccess + return []netip.Addr{tsaddr.TailscaleServiceIP()}, dns.RCodeSuccess case dns.TypeAAAA: - return tsaddr.TailscaleServiceIPv6(), dns.RCodeSuccess + return []netip.Addr{tsaddr.TailscaleServiceIPv6()}, dns.RCodeSuccess + case dns.TypeALL: + return []netip.Addr{tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6()}, dns.RCodeSuccess } } // Special-case: 4via6 DNS names. - if ip, ok := r.resolveViaDomain(domain, typ); ok { - return ip, dns.RCodeSuccess + if ips, ok := r.resolveViaDomain(domain, typ); ok { + return ips, dns.RCodeSuccess } r.mu.Lock() @@ -650,12 +653,12 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, if suffix.Contains(domain) { // We are authoritative for the queried domain. metricDNSResolveLocalErrorMissing.Add(1) - return netip.Addr{}, dns.RCodeNameError + return nil, dns.RCodeNameError } } // Not authoritative, signal that forwarding is advisable. metricDNSResolveLocalErrorRefused.Add(1) - return netip.Addr{}, dns.RCodeRefused + return nil, dns.RCodeRefused } // Refactoring note: this must happen after we check suffixes, @@ -664,42 +667,49 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, // DNS semantics subtlety: when a DNS name exists, but no records // are available for the requested record type, we must return // RCodeSuccess with no data, not NXDOMAIN. + var result []netip.Addr switch typ { case dns.TypeA: for _, ip := range addrs { if ip.Is4() { - metricDNSResolveLocalOKA.Add(1) - return ip, dns.RCodeSuccess + result = append(result, ip) } } - metricDNSResolveLocalNoA.Add(1) - return netip.Addr{}, dns.RCodeSuccess + if len(result) > 0 { + metricDNSResolveLocalOKA.Add(1) + } else { + metricDNSResolveLocalNoA.Add(1) + } + return result, dns.RCodeSuccess case dns.TypeAAAA: for _, ip := range addrs { if ip.Is6() { - metricDNSResolveLocalOKAAAA.Add(1) - return ip, dns.RCodeSuccess + result = append(result, ip) } } - metricDNSResolveLocalNoAAAA.Add(1) - return netip.Addr{}, dns.RCodeSuccess + if len(result) > 0 { + metricDNSResolveLocalOKAAAA.Add(1) + } else { + metricDNSResolveLocalNoAAAA.Add(1) + } + return result, dns.RCodeSuccess case dns.TypeALL: - // Answer with whatever we've got. - // It could be IPv4, IPv6, or a zero addr. - // TODO: Return all available resolutions (A and AAAA, if we have them). - if len(addrs) == 0 { + // Answer with all available resolutions (A and AAAA). + result = make([]netip.Addr, len(addrs)) + copy(result, addrs) + if len(result) > 0 { + metricDNSResolveLocalOKAll.Add(1) + } else { metricDNSResolveLocalNoAll.Add(1) - return netip.Addr{}, dns.RCodeSuccess } - metricDNSResolveLocalOKAll.Add(1) - return addrs[0], dns.RCodeSuccess + return result, dns.RCodeSuccess // Leave some record types explicitly unimplemented. // These types relate to recursive resolution or special // DNS semantics and might be implemented in the future. case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO: metricDNSResolveNotImplType.Add(1) - return netip.Addr{}, dns.RCodeNotImplemented + return nil, dns.RCodeNotImplemented // For everything except for the few types above that are explicitly not implemented, return no records. // This is what other DNS systems do: always return NOERROR @@ -710,7 +720,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, default: metricDNSResolveNoRecordType.Add(1) // The name exists, but no records exist of the requested type. - return netip.Addr{}, dns.RCodeSuccess + return nil, dns.RCodeSuccess } } @@ -722,7 +732,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, // // This exists as a convenient mapping into Tailscales 'Via Range'. // -// It returns a zero netip.Addr and true to indicate a successful response with +// It returns no IPs and true to indicate a successful response with // an empty answers section if the specified domain is a valid Tailscale 4via6 // domain, but the request type is neither quad-A nor ALL. // @@ -730,7 +740,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, // (2022-06-02) to work around an issue in Chrome where it would treat // "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out // the old format in early 2023. -func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr, bool) { +func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) ([]netip.Addr, bool) { fqdn := string(domain.WithoutTrailingDot()) switch typ { case dns.TypeA, dns.TypeAAAA, dns.TypeALL: @@ -743,10 +753,10 @@ func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Ad // a zero (invalid) netip.Addr and true to indicate a successful empty response, // or a zero netip.Addr and false to indicate that it is not a Tailscale 4via6 domain. default: - return netip.Addr{}, false + return nil, false } if len(fqdn) < len("via-X.0.0.0.0") { - return netip.Addr{}, false // too short to be valid + return nil, false // too short to be valid } var siteID string @@ -757,29 +767,29 @@ func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Ad // Third time's a charm. The earlier two formats follow after this block. firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7" if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) { - return netip.Addr{}, false + return nil, false } v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-") if !ok { - return netip.Addr{}, false + return nil, false } siteID = suffix ip4Str = strings.ReplaceAll(v4hyphens, "-", ".") case strings.HasPrefix(fqdn, "via-"): firstDot := strings.Index(fqdn, ".") if firstDot < 0 { - return netip.Addr{}, false // missing dot delimiters + return nil, false // missing dot delimiters } siteID = fqdn[len("via-"):firstDot] ip4Str = fqdn[firstDot+1:] default: lastDot := strings.LastIndex(fqdn, ".") if lastDot < 0 { - return netip.Addr{}, false // missing dot delimiters + return nil, false // missing dot delimiters } suffix := fqdn[lastDot+1:] if !strings.HasPrefix(suffix, "via-") { - return netip.Addr{}, false + return nil, false } siteID = suffix[len("via-"):] ip4Str = fqdn[:lastDot] @@ -787,21 +797,21 @@ func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Ad ip4, err := netip.ParseAddr(ip4Str) if err != nil { - return netip.Addr{}, false // badly formed, don't respond + return nil, false // badly formed, don't respond } prefix, err := strconv.ParseUint(siteID, 0, 32) if err != nil { - return netip.Addr{}, false // badly formed, don't respond + return nil, false // badly formed, don't respond } if typ == dns.TypeA { - return netip.Addr{}, true // the name exists, but cannot be resolved to an IPv4 address + return nil, true // the name exists, but cannot be resolved to an IPv4 address } // MapVia will never error when given an IPv4 netip.Prefix. out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen())) - return out.Addr(), true + return []netip.Addr{out.Addr()}, true } // resolveReverse returns the unique domain name that maps to the given address. @@ -1307,14 +1317,17 @@ func (r *Resolver) respond(query []byte) ([]byte, error) { return r.respondReverse(query, name, parser.response()) } - ip, rcode := r.resolveLocal(name, parser.Question.Type) + ips, rcode := r.resolveLocal(name, parser.Question.Type) if rcode == dns.RCodeRefused { return nil, errNotOurName // sentinel error return value: it requests forwarding } + if len(ips) > 1 { + rands.Shuffle(uint64(time.Now().UnixNano()), ips) + } resp := parser.response() resp.Header.RCode = rcode - resp.IP = ip + resp.IPs = ips metricDNSMagicDNSSuccessName.Add(1) return marshalResponse(resp) } diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go index f0dbb48b3..111679182 100644 --- a/net/dns/resolver/tsdns_test.go +++ b/net/dns/resolver/tsdns_test.go @@ -22,6 +22,7 @@ import ( "time" miekdns "github.com/miekg/dns" + "github.com/stretchr/testify/assert" dns "golang.org/x/net/dns/dnsmessage" "tailscale.com/health" "tailscale.com/net/netaddr" @@ -35,8 +36,12 @@ import ( ) var ( - testipv4 = netip.MustParseAddr("1.2.3.4") - testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f") + testipv4 = netip.MustParseAddr("1.2.3.4") + testipv4Second = netip.MustParseAddr("5.6.7.8") + testipv4Third = netip.MustParseAddr("9.10.11.12") + testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f") + testipv6Second = netip.MustParseAddr("1011:1213:1415:1617:1819:1a1b:1c1d:1e1f") + testipv6Third = netip.MustParseAddr("2021:2223:2425:2627:2829:2a2b:2c2d:2e2f") testipv4Arpa = dnsname.FQDN("4.3.2.1.in-addr.arpa.") testipv6Arpa = dnsname.FQDN("f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa.") @@ -46,6 +51,8 @@ var dnsCfg = Config{ Hosts: map[dnsname.FQDN][]netip.Addr{ "test1.ipn.dev.": {testipv4}, "test2.ipn.dev.": {testipv6}, + "test3.ipn.dev.": {testipv4Second, testipv6Second}, + "test4.ipn.dev.": {testipv4Second, testipv4Third, testipv6Second, testipv6Third}, }, LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."}, } @@ -245,6 +252,24 @@ func mustIP(str string) netip.Addr { return ip } +func parseResponseIPs(t *testing.T, response []byte) (rcode int, a []netip.Addr, aaaa []netip.Addr) { + t.Helper() + var msg miekdns.Msg + if err := msg.Unpack(response); err != nil { + t.Fatalf("failed to unpack response: %v", err) + } + rcode = msg.Rcode + for _, ans := range msg.Answer { + switch rr := ans.(type) { + case *miekdns.A: + a = append(a, netip.AddrFrom4([4]byte(rr.A.To4()))) + case *miekdns.AAAA: + aaaa = append(aaaa, netip.AddrFrom16([16]byte(rr.AAAA))) + } + } + return rcode, a, aaaa +} + func TestRoutesRequireNoCustomResolvers(t *testing.T) { tests := []struct { name string @@ -374,57 +399,63 @@ func TestResolveLocal(t *testing.T) { name string qname dnsname.FQDN qtype dns.Type - ip netip.Addr + ips []netip.Addr code dns.RCode }{ - {"ipv4", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess}, - {"ipv6", "test2.ipn.dev.", dns.TypeAAAA, testipv6, dns.RCodeSuccess}, - {"no-ipv6", "test1.ipn.dev.", dns.TypeAAAA, netip.Addr{}, dns.RCodeSuccess}, - {"nxdomain", "test3.ipn.dev.", dns.TypeA, netip.Addr{}, dns.RCodeNameError}, - {"foreign domain", "google.com.", dns.TypeA, netip.Addr{}, dns.RCodeRefused}, - {"all", "test1.ipn.dev.", dns.TypeA, testipv4, dns.RCodeSuccess}, - {"mx-ipv4", "test1.ipn.dev.", dns.TypeMX, netip.Addr{}, dns.RCodeSuccess}, - {"mx-ipv6", "test2.ipn.dev.", dns.TypeMX, netip.Addr{}, dns.RCodeSuccess}, - {"mx-nxdomain", "test3.ipn.dev.", dns.TypeMX, netip.Addr{}, dns.RCodeNameError}, - {"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netip.Addr{}, dns.RCodeNameError}, - {"onion-domain", "footest.onion.", dns.TypeA, netip.Addr{}, dns.RCodeNameError}, - {"magicdns", dnsSymbolicFQDN, dns.TypeA, netip.MustParseAddr("100.100.100.100"), dns.RCodeSuccess}, - {"via_hex", dnsname.FQDN("via-0xff.1.2.3.4."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4"), dns.RCodeSuccess}, - {"via_dec", dnsname.FQDN("via-1.10.0.0.1."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:10.0.0.1"), dns.RCodeSuccess}, - {"x_via_hex", dnsname.FQDN("4.3.2.1.via-0xff."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:4.3.2.1"), dns.RCodeSuccess}, - {"x_via_dec", dnsname.FQDN("1.0.0.10.via-1."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.0.0.10"), dns.RCodeSuccess}, - {"via_invalid", dnsname.FQDN("via-."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, - {"via_invalid_2", dnsname.FQDN("2.3.4.5.via-."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, + {"a-ipv4", "test1.ipn.dev.", dns.TypeA, []netip.Addr{testipv4}, dns.RCodeSuccess}, + {"aaaa-ipv4", "test1.ipn.dev.", dns.TypeAAAA, nil, dns.RCodeSuccess}, + {"all-ipv4", "test1.ipn.dev.", dns.TypeALL, []netip.Addr{testipv4}, dns.RCodeSuccess}, + {"a-ipv6", "test2.ipn.dev.", dns.TypeA, nil, dns.RCodeSuccess}, + {"aaaa-ipv6", "test2.ipn.dev.", dns.TypeAAAA, []netip.Addr{testipv6}, dns.RCodeSuccess}, + {"all-ipv6", "test2.ipn.dev.", dns.TypeAAAA, []netip.Addr{testipv6}, dns.RCodeSuccess}, + {"a-both", "test3.ipn.dev.", dns.TypeA, []netip.Addr{testipv4Second}, dns.RCodeSuccess}, + {"aaaa-both", "test3.ipn.dev.", dns.TypeAAAA, []netip.Addr{testipv6Second}, dns.RCodeSuccess}, + {"all-both", "test3.ipn.dev.", dns.TypeALL, []netip.Addr{testipv4Second, testipv6Second}, dns.RCodeSuccess}, + {"a-multi", "test4.ipn.dev.", dns.TypeA, []netip.Addr{testipv4Second, testipv4Third}, dns.RCodeSuccess}, + {"aaaa-multi", "test4.ipn.dev.", dns.TypeAAAA, []netip.Addr{testipv6Second, testipv6Third}, dns.RCodeSuccess}, + {"all-multi", "test4.ipn.dev.", dns.TypeALL, []netip.Addr{testipv4Second, testipv4Third, testipv6Second, testipv6Third}, dns.RCodeSuccess}, + {"nxdomain", "footest.ipn.dev.", dns.TypeA, nil, dns.RCodeNameError}, + {"foreign domain", "google.com.", dns.TypeA, nil, dns.RCodeRefused}, + {"mx-ipv4", "test1.ipn.dev.", dns.TypeMX, nil, dns.RCodeSuccess}, + {"mx-ipv6", "test2.ipn.dev.", dns.TypeMX, nil, dns.RCodeSuccess}, + {"mx-nxdomain", "footest.ipn.dev.", dns.TypeMX, nil, dns.RCodeNameError}, + {"ns-nxdomain", "footest.ipn.dev.", dns.TypeNS, nil, dns.RCodeNameError}, + {"onion-domain", "footest.onion.", dns.TypeA, nil, dns.RCodeNameError}, + {"magicdns", dnsSymbolicFQDN, dns.TypeA, []netip.Addr{netip.MustParseAddr("100.100.100.100")}, dns.RCodeSuccess}, + {"via_hex", dnsname.FQDN("via-0xff.1.2.3.4."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4")}, dns.RCodeSuccess}, + {"via_dec", dnsname.FQDN("via-1.10.0.0.1."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:10.0.0.1")}, dns.RCodeSuccess}, + {"x_via_hex", dnsname.FQDN("4.3.2.1.via-0xff."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:4.3.2.1")}, dns.RCodeSuccess}, + {"x_via_dec", dnsname.FQDN("1.0.0.10.via-1."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.0.0.10")}, dns.RCodeSuccess}, + {"via_invalid", dnsname.FQDN("via-."), dns.TypeAAAA, nil, dns.RCodeRefused}, + {"via_invalid_2", dnsname.FQDN("2.3.4.5.via-."), dns.TypeAAAA, nil, dns.RCodeRefused}, // Hyphenated 4via6 format. // Without any suffix domain: - {"via_form3_hex_bare", dnsname.FQDN("1-2-3-4-via-0xff."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4"), dns.RCodeSuccess}, - {"via_form3_dec_bare", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, + {"via_form3_hex_bare", dnsname.FQDN("1-2-3-4-via-0xff."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4")}, dns.RCodeSuccess}, + {"via_form3_dec_bare", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4")}, dns.RCodeSuccess}, // With a Tailscale domain: - {"via_form3_dec_ts.net", dnsname.FQDN("1-2-3-4-via-1.foo.ts.net."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, - {"via_form3_dec_tailscale.net", dnsname.FQDN("1-2-3-4-via-1.foo.tailscale.net."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, + {"via_form3_dec_ts.net", dnsname.FQDN("1-2-3-4-via-1.foo.ts.net."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4")}, dns.RCodeSuccess}, + {"via_form3_dec_tailscale.net", dnsname.FQDN("1-2-3-4-via-1.foo.tailscale.net."), dns.TypeAAAA, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4")}, dns.RCodeSuccess}, // Non-Tailscale domain suffixes aren't allowed for now: (the allowed // suffixes are currently hard-coded and not plumbed via the netmap) - {"via_form3_dec_example.com", dnsname.FQDN("1-2-3-4-via-1.example.com."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, - {"via_form3_dec_examplets.net", dnsname.FQDN("1-2-3-4-via-1.examplets.net."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, + {"via_form3_dec_example.com", dnsname.FQDN("1-2-3-4-via-1.example.com."), dns.TypeAAAA, nil, dns.RCodeRefused}, + {"via_form3_dec_examplets.net", dnsname.FQDN("1-2-3-4-via-1.examplets.net."), dns.TypeAAAA, nil, dns.RCodeRefused}, // Resolve A and ALL types of resource records. - {"via_type_a", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeA, netip.Addr{}, dns.RCodeSuccess}, - {"via_invalid_type_a", dnsname.FQDN("1-2-3-4-via-."), dns.TypeA, netip.Addr{}, dns.RCodeRefused}, - {"via_type_all", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeALL, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, - {"via_invalid_type_all", dnsname.FQDN("1-2-3-4-via-."), dns.TypeALL, netip.Addr{}, dns.RCodeRefused}, + {"via_type_a", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeA, nil, dns.RCodeSuccess}, + {"via_invalid_type_a", dnsname.FQDN("1-2-3-4-via-."), dns.TypeA, nil, dns.RCodeRefused}, + {"via_type_all", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeALL, []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4")}, dns.RCodeSuccess}, + {"via_invalid_type_all", dnsname.FQDN("1-2-3-4-via-."), dns.TypeALL, nil, dns.RCodeRefused}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ip, code := r.resolveLocal(tt.qname, tt.qtype) + ips, code := r.resolveLocal(tt.qname, tt.qtype) if code != tt.code { t.Errorf("code = %v; want %v", code, tt.code) } - // Only check ip for non-err - if ip != tt.ip { - t.Errorf("ip = %v; want %v", ip, tt.ip) - } + // Only check ips for non-err + assert.ElementsMatch(t, tt.ips, ips) }) } } @@ -902,7 +933,7 @@ var nxdomainResponse = []byte{ 0x00, 0x00, // no answers 0x00, 0x00, 0x00, 0x00, // no authority or additional RRs // Question: - 0x05, 0x74, 0x65, 0x73, 0x74, 0x33, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name + 0x07, 0x66, 0x6f, 0x6f, 0x74, 0x65, 0x73, 0x74, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00, // name: footest.ipn.dev. 0x00, 0x01, 0x00, 0x01, // type A, class IN } @@ -937,7 +968,7 @@ func TestFull(t *testing.T) { {"ptr4", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR, noEdns), ptrResponse}, {"ptr6", dnspacket("f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa.", dns.TypePTR, noEdns), ptrResponse6}, - {"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA, noEdns), nxdomainResponse}, + {"nxdomain", dnspacket("footest.ipn.dev.", dns.TypeA, noEdns), nxdomainResponse}, } for _, tt := range tests { @@ -953,6 +984,43 @@ func TestFull(t *testing.T) { } } +// Test addresses parsed from the response, to tolerate different record ordering +func TestFullParsed(t *testing.T) { + r := newResolver(t) + defer r.Close() + + r.SetConfig(dnsCfg) + + tests := []struct { + name string + qname dnsname.FQDN + qtype dns.Type + wantA []netip.Addr + wantAAAA []netip.Addr + }{ + {"single-a", "test1.ipn.dev.", dns.TypeA, []netip.Addr{testipv4}, nil}, + {"single-aaaa", "test2.ipn.dev.", dns.TypeAAAA, nil, []netip.Addr{testipv6}}, + {"multi-a", "test4.ipn.dev.", dns.TypeA, []netip.Addr{testipv4Second, testipv4Third}, nil}, + {"multi-aaaa", "test4.ipn.dev.", dns.TypeAAAA, nil, []netip.Addr{testipv6Second, testipv6Third}}, + {"multi-all", "test4.ipn.dev.", dns.TypeALL, []netip.Addr{testipv4Second, testipv4Third}, []netip.Addr{testipv6Second, testipv6Third}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := syncRespond(r, dnspacket(tt.qname, tt.qtype, noEdns)) + if err != nil { + t.Fatalf("err = %v; want nil", err) + } + rcode, a, aaaa := parseResponseIPs(t, response) + if rcode != int(dns.RCodeSuccess) { + t.Errorf("rcode = %v; want %v", rcode, dns.RCodeSuccess) + } + assert.ElementsMatch(t, tt.wantA, a, "A records mismatch") + assert.ElementsMatch(t, tt.wantAAAA, aaaa, "AAAA records mismatch") + }) + } +} + func TestAllocs(t *testing.T) { r := newResolver(t) defer r.Close()