mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
Internal Domains now support three modes. "os" is unchanged. "resolvers" becomes explicit resolver with network fallback: the configured resolvers are tried first, and a timeout, unreachable resolver, SERVFAIL or NXDOMAIN hands the query to matching VPN DNS servers, then domain-less VPN DNS servers, then the OS resolver's LAN nameservers through the new LanOnlyQueryCtx. "resolvers_only" keeps the previous strict behavior. Down fallback-mode resolvers are skipped and re-checked in the background at most every 30s. Generated upstreams are identified by the UpstreamConfig.InternalDomain marker instead of the internal_ key prefix. Master adaptations: the fallback state lives on proxyRequest and is applied in tryUpstreams, shouldContinueWithNextUpstream and handleAllUpstreamsFailure instead of v1.0's monolithic proxy loop. Resolver construction uses master's context-aware NewResolver, and the background re-check marks its context private so transport errors that name the resolver stay at debug. Tests use master's zap JSON capture and VPN manager logger.
163 lines
5.3 KiB
Go
163 lines
5.3 KiB
Go
package controld
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The API's utility response carries organization Internal Domains in
|
|
// resolver.split_dns. Parsing it must not change anything when the field is
|
|
// absent or empty, which is what every resolver without the feature returns.
|
|
func TestResolverConfigParsesSplitDNS(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
body string
|
|
want []SplitDNS
|
|
}{
|
|
{
|
|
name: "absent",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://dns.controld.com/abc","uid":"abc"}}}`,
|
|
},
|
|
{
|
|
name: "null",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a","split_dns":null}}}`,
|
|
},
|
|
{
|
|
name: "empty",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a","split_dns":[]}}}`,
|
|
want: []SplitDNS{},
|
|
},
|
|
{
|
|
name: "explicit resolvers",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a","split_dns":[
|
|
{"domain":"aws.example.com","resolvers":["10.0.0.10","10.0.0.11"]}]}}}`,
|
|
want: []SplitDNS{{Domain: "aws.example.com", Resolvers: []string{"10.0.0.10", "10.0.0.11"}}},
|
|
},
|
|
{
|
|
name: "os resolver selection",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a","split_dns":[
|
|
{"domain":"corp.example.com","resolvers":[]},
|
|
{"domain":"lab.example.com"}]}}}`,
|
|
want: []SplitDNS{
|
|
{Domain: "corp.example.com", Resolvers: []string{}},
|
|
{Domain: "lab.example.com"},
|
|
},
|
|
},
|
|
{
|
|
name: "coexists with exclude",
|
|
body: `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a",
|
|
"exclude":["corp.example.com"],
|
|
"split_dns":[{"domain":"aws.example.com","resolvers":["10.0.0.10"]}]}}}`,
|
|
want: []SplitDNS{{Domain: "aws.example.com", Resolvers: []string{"10.0.0.10"}}},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var resp utilityResponse
|
|
if err := json.NewDecoder(strings.NewReader(tc.body)).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := resp.Body.Resolver.SplitDNS
|
|
if len(got) != len(tc.want) {
|
|
t.Fatalf("split_dns = %+v, want %+v", got, tc.want)
|
|
}
|
|
for i := range got {
|
|
if got[i].Domain != tc.want[i].Domain {
|
|
t.Errorf("[%d].Domain = %q, want %q", i, got[i].Domain, tc.want[i].Domain)
|
|
}
|
|
if len(got[i].Resolvers) != len(tc.want[i].Resolvers) {
|
|
t.Fatalf("[%d].Resolvers = %v, want %v", i, got[i].Resolvers, tc.want[i].Resolvers)
|
|
}
|
|
for j := range got[i].Resolvers {
|
|
if got[i].Resolvers[j] != tc.want[i].Resolvers[j] {
|
|
t.Errorf("[%d].Resolvers[%d] = %q, want %q", i, j, got[i].Resolvers[j], tc.want[i].Resolvers[j])
|
|
}
|
|
}
|
|
}
|
|
// The rest of the model must be unaffected.
|
|
if resp.Body.Resolver.DOH == "" || resp.Body.Resolver.UID == "" {
|
|
t.Errorf("resolver = %+v", resp.Body.Resolver)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// An unknown field in an entry must not fail the whole response: the API can
|
|
// add one before ctrld knows about it.
|
|
func TestResolverConfigToleratesUnknownSplitDNSFields(t *testing.T) {
|
|
body := `{"success":true,"body":{"resolver":{"doh":"https://d","uid":"a","split_dns":[
|
|
{"domain":"aws.example.com","resolvers":["10.0.0.10"],"future_field":{"a":1}}]}}}`
|
|
var resp utilityResponse
|
|
if err := json.NewDecoder(strings.NewReader(body)).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp.Body.Resolver.SplitDNS) != 1 || resp.Body.Resolver.SplitDNS[0].Domain != "aws.example.com" {
|
|
t.Fatalf("split_dns = %+v", resp.Body.Resolver.SplitDNS)
|
|
}
|
|
}
|
|
|
|
// The shape the API actually returns, mode field included.
|
|
func TestResolverConfigParsesSplitDNSMode(t *testing.T) {
|
|
const body = `{
|
|
"success": true,
|
|
"body": {
|
|
"resolver": {
|
|
"split_dns": [
|
|
{
|
|
"domain": "corp.example.com",
|
|
"mode": "resolvers",
|
|
"resolvers": ["10.0.0.53", "10.0.0.54"]
|
|
},
|
|
{
|
|
"domain": "office.example.com",
|
|
"mode": "os",
|
|
"resolvers": []
|
|
},
|
|
{
|
|
"domain": "vault.example.com",
|
|
"mode": "resolvers_only",
|
|
"resolvers": ["10.0.0.55"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`
|
|
var resp utilityResponse
|
|
if err := json.NewDecoder(strings.NewReader(body)).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := resp.Body.Resolver.SplitDNS
|
|
if len(got) != 3 {
|
|
t.Fatalf("split_dns = %+v", got)
|
|
}
|
|
if got[0].Domain != "corp.example.com" || got[0].Mode != SplitDNSModeResolvers {
|
|
t.Errorf("[0] = %+v", got[0])
|
|
}
|
|
if len(got[0].Resolvers) != 2 || got[0].Resolvers[0] != "10.0.0.53" || got[0].Resolvers[1] != "10.0.0.54" {
|
|
t.Errorf("[0].Resolvers = %v", got[0].Resolvers)
|
|
}
|
|
if got[1].Domain != "office.example.com" || got[1].Mode != SplitDNSModeOS {
|
|
t.Errorf("[1] = %+v", got[1])
|
|
}
|
|
if len(got[1].Resolvers) != 0 {
|
|
t.Errorf("[1].Resolvers = %v, want empty", got[1].Resolvers)
|
|
}
|
|
if got[2].Mode != SplitDNSModeResolversOnly || len(got[2].Resolvers) != 1 {
|
|
t.Errorf("[2] = %+v", got[2])
|
|
}
|
|
}
|
|
|
|
// A response without the field must still decode, so a deployment that predates
|
|
// mode keeps working.
|
|
func TestResolverConfigToleratesAbsentSplitDNSMode(t *testing.T) {
|
|
const body = `{"success":true,"body":{"resolver":{"split_dns":[
|
|
{"domain":"corp.example.com","resolvers":["10.0.0.53"]}]}}}`
|
|
var resp utilityResponse
|
|
if err := json.NewDecoder(strings.NewReader(body)).Decode(&resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := resp.Body.Resolver.SplitDNS; len(got) != 1 || got[0].Mode != "" {
|
|
t.Fatalf("split_dns = %+v", got)
|
|
}
|
|
}
|