mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-13 10:26:06 +00:00
Replace boolean rebootstrap flag with a three-state atomic integer to prevent concurrent SetupTransport calls during rebootstrap. The atomic state machine ensures only one goroutine can proceed from "started" to "in progress", eliminating the need for a mutex while maintaining thread safety. States: NotStarted -> Started -> InProgress -> NotStarted Note that the race condition is still acceptable because any additional transports created during the race are functional. Once the connection is established, the unused transports are safely handled by the garbage collector.
559 lines
12 KiB
Go
559 lines
12 KiB
Go
package ctrld
|
|
|
|
import (
|
|
"context"
|
|
"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(context.Background())
|
|
tc.uc.SetupBootstrapIP(context.Background())
|
|
if len(tc.uc.bootstrapIPs) == 0 {
|
|
t.Log(defaultNameservers(context.Background()))
|
|
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(context.Background())
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 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(context.Background())
|
|
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"},
|
|
}
|
|
|
|
ctx := LoggerCtx(context.Background(), NopLogger)
|
|
|
|
uc.SetupTransport(ctx)
|
|
|
|
if uc.transport == nil {
|
|
t.Fatal("initial transport should be set")
|
|
}
|
|
|
|
const goroutines = 100
|
|
|
|
uc.ReBootstrap(ctx)
|
|
|
|
started := make(chan struct{})
|
|
go func() {
|
|
close(started)
|
|
for {
|
|
switch uc.rebootstrap.Load() {
|
|
case rebootstrapStarted, rebootstrapInProgress:
|
|
uc.ReBootstrap(ctx)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
<-started
|
|
|
|
var wg sync.WaitGroup
|
|
for range goroutines {
|
|
wg.Go(func() {
|
|
uc.ensureSetupTransport(ctx)
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func ptrBool(b bool) *bool {
|
|
return &b
|
|
}
|