mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
cli: route organization Internal Domains from managed config
Port the final merged change from !1009 to master, retaining master's context-aware logger/resolver APIs, proxy decomposition and firewall refresh handling. Adapt source regression tests and cover master's refresh handler and transport-level private-address diagnostics.
This commit is contained in:
12 files changed
+2248
-18
No files matched your search
@@ -55,7 +55,8 @@ type ResolverConfig struct {
|
||||
CustomLastUpdate int64 `json:"custom_last_update"`
|
||||
VersionTarget string `json:"version_target"`
|
||||
} `json:"ctrld"`
|
||||
Exclude []string `json:"exclude"`
|
||||
Exclude []string `json:"exclude"`
|
||||
SplitDNS []SplitDNS `json:"split_dns"`
|
||||
// DestinationIPs is the organization's effective Allowed Destination IP list:
|
||||
// the entries configured for this endpoint's organization plus any inherited
|
||||
// from a parent organization. Each entry is an IPv4/IPv6 address or a CIDR
|
||||
@@ -67,6 +68,24 @@ type ResolverConfig struct {
|
||||
DeactivationPin *int64 `json:"deactivation_pin,omitempty"`
|
||||
}
|
||||
|
||||
// Internal Domain resolution modes. Mode is what the administrator selected;
|
||||
// Resolvers is only meaningful under SplitDNSModeResolvers.
|
||||
const (
|
||||
// SplitDNSModeOS follows the endpoint's own OS/default resolver.
|
||||
SplitDNSModeOS = "os"
|
||||
// SplitDNSModeResolvers uses the configured addresses and nothing else.
|
||||
SplitDNSModeResolvers = "resolvers"
|
||||
)
|
||||
|
||||
// SplitDNS is one organization Internal Domain. Mode decides the routing;
|
||||
// Resolvers is read only under SplitDNSModeResolvers. An empty Mode predates
|
||||
// the field, so the caller infers the mode from Resolvers in that case.
|
||||
type SplitDNS struct {
|
||||
Domain string `json:"domain"`
|
||||
Mode string `json:"mode"`
|
||||
Resolvers []string `json:"resolvers"`
|
||||
}
|
||||
|
||||
type utilityResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Body struct {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
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) != 2 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user