diff --git a/config.go b/config.go index 81b1f04..e2024c4 100644 --- a/config.go +++ b/config.go @@ -78,6 +78,12 @@ const ( controlDNetDomain = "controld.net" controlDDevDomain = "controld.dev" + // nextDNSDomain is the parent domain of the NextDNS DoH endpoints. Beside + // dns.nextdns.io, NextDNS serves alternative endpoints under it, such as + // ultralow.dns.nextdns.io and anycast.dns2.nextdns.io, which are the same + // service and take the same client info headers. + nextDNSDomain = "nextdns.io" + endpointPrefixHTTPS = "https://" endpointPrefixQUIC = "quic://" endpointPrefixH3 = "h3://" @@ -761,6 +767,7 @@ func (uc *UpstreamConfig) IsControlD() bool { return false } +// isNextDNS reports whether this is a NextDNS upstream. func (uc *UpstreamConfig) isNextDNS() bool { domain := uc.Domain if domain == "" { @@ -768,7 +775,7 @@ func (uc *UpstreamConfig) isNextDNS() bool { domain = u.Hostname() } } - return domain == "dns.nextdns.io" + return dns.IsSubDomain(nextDNSDomain, domain) } func (uc *UpstreamConfig) dohTransport(ctx context.Context, dnsType uint16) http.RoundTripper { diff --git a/config_internal_test.go b/config_internal_test.go index 24f85b6..b1f4042 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -398,6 +398,59 @@ func TestUpstreamConfig_VerifyDomain(t *testing.T) { } } +// TestUpstreamConfig_isNextDNS is a regression test for issue-610: NextDNS +// serves its alternative DoH endpoints, the "ultralow" and "anycast" variants, +// under nextdns.io, so recognition follows the parent domain instead of the +// single dns.nextdns.io host. Each case runs through the Domain field and +// through the endpoint-hostname fallback, the two ways an upstream names its +// host. +func TestUpstreamConfig_isNextDNS(t *testing.T) { + tests := []struct { + name string + domain string + want bool + }{ + {"the default endpoint", "dns.nextdns.io", true}, + {"the numbered endpoints", "dns1.nextdns.io", true}, + {"the second numbered endpoint", "dns2.nextdns.io", true}, + {"an ultralow endpoint", "ultralow.dns.nextdns.io", true}, + {"a numbered ultralow endpoint", "ultralow.dns1.nextdns.io", true}, + {"the second numbered ultralow endpoint", "ultralow.dns2.nextdns.io", true}, + {"an anycast endpoint", "anycast.dns.nextdns.io", true}, + {"a numbered anycast endpoint", "anycast.dns1.nextdns.io", true}, + {"the second numbered anycast endpoint", "anycast.dns2.nextdns.io", true}, + {"a mixed case endpoint", "UltraLow.DNS.NextDNS.IO", true}, + {"the parent domain itself", "nextdns.io", true}, + + {"another TLD", "dns.nextdns.com", false}, + {"a domain ending in the same labels", "notnextdns.io", false}, + {"the parent domain as a prefix", "xnextdns.io", false}, + {"the parent domain under another one", "nextdns.io.example.com", false}, + {"an unrelated resolver", "dns.google", false}, + {"a ControlD upstream", "freedns.controld.com", false}, + {"no host at all", "", false}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name+" as a domain", func(t *testing.T) { + t.Parallel() + uc := &UpstreamConfig{Domain: tc.domain} + if got := uc.isNextDNS(); got != tc.want { + t.Errorf("isNextDNS() = %v for domain %q, want %v", got, tc.domain, tc.want) + } + }) + t.Run(tc.name+" as an endpoint", func(t *testing.T) { + t.Parallel() + // Domain is empty, so the lookup falls back to the endpoint host. + uc := &UpstreamConfig{Endpoint: "https://" + tc.domain + "/abc123"} + if got := uc.isNextDNS(); got != tc.want { + t.Errorf("isNextDNS() = %v for endpoint %q, want %v", got, uc.Endpoint, tc.want) + } + }) + } +} + func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) { tests := []struct { name string @@ -414,6 +467,16 @@ func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) { &UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH3}, true, }, + { + "default with nextdns upstream DoH", + &UpstreamConfig{Endpoint: "https://dns.nextdns.io/abc123", Type: ResolverTypeDOH}, + true, + }, + { + "default with nextdns ultralow upstream DoH", + &UpstreamConfig{Endpoint: "https://ultralow.dns2.nextdns.io/abc123", Type: ResolverTypeDOH}, + true, + }, { "default with non-ControlD upstream", &UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH},