mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
NextDNS serves alternative DoH endpoints beside dns.nextdns.io: the ultralow and anycast variants of dns, dns1 and dns2. They are the same service and take the same client info headers, but isNextDNS matched the one host, so an upstream pointed at any of them was treated as a third party and sent no client info. Recognition now follows the parent domain through dns.IsSubDomain, as IsControlD already does for the ControlD domains. The label by label comparison is case insensitive and keeps lookalikes such as notnextdns.io and nextdns.io.example.com out. Based on the change proposed by Mike (Github username @mike406). See: https://github.com/Control-D-Inc/ctrld/pull/335.
621 lines
15 KiB
Go
621 lines
15 KiB
Go
package ctrld
|
|
|
|
import (
|
|
"net/url"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
uc *UpstreamConfig
|
|
}{
|
|
{
|
|
name: "doh/doh3",
|
|
uc: &UpstreamConfig{
|
|
Name: "doh",
|
|
Type: ResolverTypeDOH,
|
|
Endpoint: "https://freedns.controld.com/p2",
|
|
Timeout: 5000,
|
|
},
|
|
},
|
|
{
|
|
name: "doq/dot",
|
|
uc: &UpstreamConfig{
|
|
Name: "dot",
|
|
Type: ResolverTypeDOT,
|
|
Endpoint: "p2.freedns.controld.com",
|
|
Timeout: 5000,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Enable parallel tests once https://github.com/microsoft/wmi/issues/165 fixed.
|
|
// t.Parallel()
|
|
tc.uc.Init()
|
|
tc.uc.SetupBootstrapIP()
|
|
if len(tc.uc.bootstrapIPs) == 0 {
|
|
t.Log(defaultNameservers())
|
|
t.Fatalf("could not bootstrap ip: %s", tc.uc.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestUpstreamConfig_Init(t *testing.T) {
|
|
u1, _ := url.Parse("https://example.com")
|
|
u2, _ := url.Parse("https://example.com?k=v")
|
|
u3, _ := url.Parse("https://freedns.controld.com/p1")
|
|
tests := []struct {
|
|
name string
|
|
uc *UpstreamConfig
|
|
expected *UpstreamConfig
|
|
}{
|
|
{
|
|
"doh+doh3",
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "example.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
u: u1,
|
|
},
|
|
},
|
|
{
|
|
"doh+doh3 with query param",
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com?k=v",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com?k=v",
|
|
BootstrapIP: "",
|
|
Domain: "example.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
u: u2,
|
|
},
|
|
},
|
|
{
|
|
"dot+doq",
|
|
&UpstreamConfig{
|
|
Name: "dot",
|
|
Type: "dot",
|
|
Endpoint: "freedns.controld.com:8853",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "dot",
|
|
Type: "dot",
|
|
Endpoint: "freedns.controld.com:8853",
|
|
BootstrapIP: "",
|
|
Domain: "freedns.controld.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackSplit,
|
|
},
|
|
},
|
|
{
|
|
"dot+doq without port",
|
|
&UpstreamConfig{
|
|
Name: "dot",
|
|
Type: "dot",
|
|
Endpoint: "freedns.controld.com",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackSplit,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "dot",
|
|
Type: "dot",
|
|
Endpoint: "freedns.controld.com:853",
|
|
BootstrapIP: "",
|
|
Domain: "freedns.controld.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackSplit,
|
|
},
|
|
},
|
|
{
|
|
"legacy",
|
|
&UpstreamConfig{
|
|
Name: "legacy",
|
|
Type: "legacy",
|
|
Endpoint: "1.2.3.4:53",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "legacy",
|
|
Type: "legacy",
|
|
Endpoint: "1.2.3.4:53",
|
|
BootstrapIP: "1.2.3.4",
|
|
Domain: "1.2.3.4",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
{
|
|
"legacy without port",
|
|
&UpstreamConfig{
|
|
Name: "legacy",
|
|
Type: "legacy",
|
|
Endpoint: "1.2.3.4",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "legacy",
|
|
Type: "legacy",
|
|
Endpoint: "1.2.3.4:53",
|
|
BootstrapIP: "1.2.3.4",
|
|
Domain: "1.2.3.4",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
{
|
|
"doh+doh3 with send client info set",
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com?k=v",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
SendClientInfo: ptrBool(false),
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "doh",
|
|
Type: "doh",
|
|
Endpoint: "https://example.com?k=v",
|
|
BootstrapIP: "",
|
|
Domain: "example.com",
|
|
Timeout: 0,
|
|
SendClientInfo: ptrBool(false),
|
|
IPStack: IpStackBoth,
|
|
u: u2,
|
|
},
|
|
},
|
|
{
|
|
"h3",
|
|
&UpstreamConfig{
|
|
Name: "doh3",
|
|
Type: "doh3",
|
|
Endpoint: "h3://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "doh3",
|
|
Type: "doh3",
|
|
Endpoint: "https://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "example.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
u: u1,
|
|
},
|
|
},
|
|
{
|
|
"h3 without type",
|
|
&UpstreamConfig{
|
|
Name: "doh3",
|
|
Endpoint: "h3://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "doh3",
|
|
Type: "doh3",
|
|
Endpoint: "https://example.com",
|
|
BootstrapIP: "",
|
|
Domain: "example.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
u: u1,
|
|
},
|
|
},
|
|
{
|
|
"sdns -> doh",
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "sdns",
|
|
Endpoint: "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "doh",
|
|
Endpoint: "https://freedns.controld.com/p1",
|
|
BootstrapIP: "76.76.2.11",
|
|
Domain: "freedns.controld.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
u: u3,
|
|
},
|
|
},
|
|
{
|
|
"sdns -> dot",
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "sdns",
|
|
Endpoint: "sdns://AwcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "dot",
|
|
Endpoint: "freedns.controld.com:843",
|
|
BootstrapIP: "76.76.2.11",
|
|
Domain: "freedns.controld.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
{
|
|
"sdns -> doq",
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "sdns",
|
|
Endpoint: "sdns://BAcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "doq",
|
|
Endpoint: "freedns.controld.com:784",
|
|
BootstrapIP: "76.76.2.11",
|
|
Domain: "freedns.controld.com",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
{
|
|
"sdns -> legacy",
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "sdns",
|
|
Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "legacy",
|
|
Endpoint: "76.76.2.11:53",
|
|
BootstrapIP: "76.76.2.11",
|
|
Domain: "76.76.2.11",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
{
|
|
"sdns without type",
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE",
|
|
BootstrapIP: "",
|
|
Domain: "",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
&UpstreamConfig{
|
|
Name: "sdns",
|
|
Type: "legacy",
|
|
Endpoint: "76.76.2.11:53",
|
|
BootstrapIP: "76.76.2.11",
|
|
Domain: "76.76.2.11",
|
|
Timeout: 0,
|
|
IPStack: IpStackBoth,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tc.uc.Init()
|
|
tc.uc.uid = "" // we don't care about the uid.
|
|
assert.Equal(t, tc.expected, tc.uc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpstreamConfig_VerifyDomain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
uc *UpstreamConfig
|
|
verifyDomain string
|
|
}{
|
|
{
|
|
controlDComDomain,
|
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2"},
|
|
controldVerifiedDomain[controlDComDomain],
|
|
},
|
|
{
|
|
controlDDevDomain,
|
|
&UpstreamConfig{Endpoint: "https://freedns.controld.dev/p2"},
|
|
controldVerifiedDomain[controlDDevDomain],
|
|
},
|
|
{
|
|
"non-ControlD upstream",
|
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query"},
|
|
"",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := tc.uc.VerifyDomain(); got != tc.verifyDomain {
|
|
t.Errorf("unexpected verify domain, want: %q, got: %q", tc.verifyDomain, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
uc *UpstreamConfig
|
|
sendClientInfo bool
|
|
}{
|
|
{
|
|
"default with controld upstream DoH",
|
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH},
|
|
true,
|
|
},
|
|
{
|
|
"default with controld upstream DoH3",
|
|
&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},
|
|
false,
|
|
},
|
|
{
|
|
"set false with controld upstream",
|
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH, SendClientInfo: ptrBool(false)},
|
|
false,
|
|
},
|
|
{
|
|
"set true with controld upstream",
|
|
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", SendClientInfo: ptrBool(true)},
|
|
true,
|
|
},
|
|
{
|
|
"set false with non-ControlD upstream",
|
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query", SendClientInfo: ptrBool(false)},
|
|
false,
|
|
},
|
|
{
|
|
"set true with non-ControlD upstream",
|
|
&UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH, SendClientInfo: ptrBool(true)},
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := tc.uc.UpstreamSendClientInfo(); got != tc.sendClientInfo {
|
|
t.Errorf("unexpected result, want: %v, got: %v", tc.sendClientInfo, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpstreamConfig_IsDiscoverable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
uc *UpstreamConfig
|
|
discoverable bool
|
|
}{
|
|
{
|
|
"loopback",
|
|
&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy},
|
|
true,
|
|
},
|
|
{
|
|
"rfc1918",
|
|
&UpstreamConfig{Endpoint: "192.168.1.1", Type: ResolverTypeLegacy},
|
|
true,
|
|
},
|
|
{
|
|
"CGNAT",
|
|
&UpstreamConfig{Endpoint: "100.66.67.68", Type: ResolverTypeLegacy},
|
|
true,
|
|
},
|
|
{
|
|
"Public IP",
|
|
&UpstreamConfig{Endpoint: "8.8.8.8", Type: ResolverTypeLegacy},
|
|
false,
|
|
},
|
|
{
|
|
"override discoverable",
|
|
&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(false)},
|
|
false,
|
|
},
|
|
{
|
|
"override non-public",
|
|
&UpstreamConfig{Endpoint: "1.1.1.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(true)},
|
|
true,
|
|
},
|
|
{
|
|
"non-legacy upstream",
|
|
&UpstreamConfig{Endpoint: "https://192.168.1.1/custom-doh", Type: ResolverTypeDOH},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tc.uc.Init()
|
|
if got := tc.uc.IsDiscoverable(); got != tc.discoverable {
|
|
t.Errorf("unexpected result, want: %v, got: %v", tc.discoverable, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRebootstrapRace(t *testing.T) {
|
|
uc := &UpstreamConfig{
|
|
Name: "test-doh",
|
|
Type: ResolverTypeDOH,
|
|
Endpoint: "https://example.com/dns-query",
|
|
Domain: "example.com",
|
|
bootstrapIPs: []string{"1.1.1.1", "1.0.0.1"},
|
|
}
|
|
|
|
uc.SetupTransport()
|
|
|
|
if uc.transport == nil {
|
|
t.Fatal("initial transport should be set")
|
|
}
|
|
|
|
const goroutines = 100
|
|
|
|
uc.ReBootstrap()
|
|
|
|
started := make(chan struct{})
|
|
go func() {
|
|
close(started)
|
|
for {
|
|
switch uc.rebootstrap.Load() {
|
|
case rebootstrapStarted, rebootstrapInProgress:
|
|
uc.ReBootstrap()
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
<-started
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
for range goroutines {
|
|
go func() {
|
|
defer wg.Done()
|
|
uc.ensureSetupTransport()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func ptrBool(b bool) *bool {
|
|
return &b
|
|
}
|