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

+33
View File
@@ -126,6 +126,29 @@ func PrivateResolverCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, privateResolverCtxKey{}, true)
}
// LanOnlyQueryCtxKey is the context.Context key that limits an OS resolver
// query to its LAN nameservers.
type LanOnlyQueryCtxKey struct{}
// LanOnlyQueryCtx returns a context.Context with LanOnlyQueryCtxKey set.
//
// An OS resolver query made with it goes only to LAN-classified nameservers:
// private, loopback, link-local and CGNAT addresses. Every public nameserver
// is left out, the ones DHCP or a VPN supplied as well as ctrld's own public
// fallback, and with no LAN nameserver the query fails without being sent. It
// is for names that must not reach public DNS, such as an organization's
// Internal Domains. LanQueryCtx, by contrast, only drops ctrld's own public
// fallback.
func LanOnlyQueryCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, LanOnlyQueryCtxKey{}, true)
}
// isLanOnlyQuery reports whether ctx was made by LanOnlyQueryCtx.
func isLanOnlyQuery(ctx context.Context) bool {
lanOnly, _ := ctx.Value(LanOnlyQueryCtxKey{}).(bool)
return lanOnly
}
// defaultNameservers is like nameservers with each element formed "ip:53".
func defaultNameservers(ctx context.Context) []string {
ns := nameservers(ctx)
@@ -580,6 +603,12 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error
// the key so subnet-specific answers are neither coalesced nor hot-cached
// across different subnets (RFC 7871 §7.3).
key := fmt.Sprintf("%s:%d:%s", domain, qtype, dnscache.CanonicalECS(msg))
// A LAN-only query must not join, or be answered from the hot cache of, an
// ordinary query for the same name: that answer may have come from a public
// nameserver.
if isLanOnlyQuery(ctx) {
key += ":lan-only"
}
logger := LoggerFromCtx(ctx)
Log(ctx, logger.Debug(), "OS resolver query started: %s - %s", domain, dns.TypeToString[qtype])
@@ -641,6 +670,10 @@ func (o *osResolver) resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error
if p := o.lanServers.Load(); p != nil {
nss = append(nss, (*p)...)
}
// A LAN-only query is never sent to a public nameserver.
if isLanOnlyQuery(ctx) {
publicServers = nil
}
numServers := len(nss) + len(publicServers)
// If this is a LAN query, skip public DNS.