cli: add network fallback and explicit-only Internal Domain modes

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.
This commit is contained in:
Dev Scribe authored and Cuong Manh Le committed 2026-10-07 14:24:00 +07:00
1 parent bfc46ef265
commit b0408e4d1c
17 files changed
+1334 -138

No files matched your search

+18 -5
View File
@@ -109,17 +109,30 @@ type ResolverConfig struct {
}
// Internal Domain resolution modes. Mode is what the administrator selected;
// Resolvers is only meaningful under SplitDNSModeResolvers.
// Resolvers is only meaningful under SplitDNSModeResolvers and
// SplitDNSModeResolversOnly.
const (
// SplitDNSModeOS follows the endpoint's own OS/default resolver.
SplitDNSModeOS = "os"
// SplitDNSModeResolvers uses the configured addresses and nothing else.
// SplitDNSModeResolvers resolves the domain through the addresses in
// Resolvers first. When none of them answers, or they answer SERVFAIL or
// NXDOMAIN, the query falls back to the resolvers of the endpoint's active
// network and VPN. This is the default explicit selection.
SplitDNSModeResolvers = "resolvers"
// SplitDNSModeResolversOnly resolves the domain through the addresses in
// Resolvers and nothing else: their answer, or their failure, is final.
SplitDNSModeResolversOnly = "resolvers_only"
)
// 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.
// SplitDNS is one organization Internal Domain: a domain suffix, the resolution
// mode the organization administrator selected for it, and the resolver
// addresses that mode may need.
//
// Mode decides the routing. Resolvers is read only under the explicit modes,
// so addresses left behind by an earlier selection cannot resurrect themselves
// once the administrator switches back to the OS resolver. An empty Mode is a
// deployment that predates the field; the caller infers the mode from
// Resolvers in that case.
type SplitDNS struct {
Domain string `json:"domain"`
Mode string `json:"mode"`
+9 -1
View File
@@ -112,6 +112,11 @@ func TestResolverConfigParsesSplitDNSMode(t *testing.T) {
"domain": "office.example.com",
"mode": "os",
"resolvers": []
},
{
"domain": "vault.example.com",
"mode": "resolvers_only",
"resolvers": ["10.0.0.55"]
}
]
}
@@ -122,7 +127,7 @@ func TestResolverConfigParsesSplitDNSMode(t *testing.T) {
t.Fatal(err)
}
got := resp.Body.Resolver.SplitDNS
if len(got) != 2 {
if len(got) != 3 {
t.Fatalf("split_dns = %+v", got)
}
if got[0].Domain != "corp.example.com" || got[0].Mode != SplitDNSModeResolvers {
@@ -137,6 +142,9 @@ func TestResolverConfigParsesSplitDNSMode(t *testing.T) {
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