mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
cli: add network fallback and explicit-only Internal Domain modes
Internal Domains now support three modes. "os" (network default) is unchanged. "resolvers" becomes explicit resolver with network fallback, the default explicit selection: the configured resolvers are tried first, and a timeout, unreachable resolver, SERVFAIL, NXDOMAIN, REFUSED or NOTIMP hands the query to the network resolvers - matching VPN DNS servers, then domain-less VPN DNS servers, then the OS resolver's LAN nameservers. A valid empty answer stays final. "resolvers_only" keeps the previous strict behavior. The OS step uses a new LanOnlyQueryCtx: the OS resolver then asks only its LAN-classified nameservers (private, loopback, link-local, CGNAT) and sends nothing when it has none, so the private name never reaches a public nameserver, whether DHCP supplied it or it is ctrld's own public fallback. LanQueryCtx only dropped the latter. LAN-only queries get their own singleflight and hot-cache key, so they are never answered with an ordinary query's public answer. In fallback mode a configured resolver that the upstream monitor has marked down is skipped instead of waited on, so an endpoint off the organization network reaches the fallback without paying the resolver's 2s timeout on every query. Nothing else marks a generated resolver up again, so skipping one re-checks it in the background, at most once every 30s; an answer marks it up. Strict mode never skips. An absent mode with resolvers gets the fallback mode. An unrecognized mode with resolvers fails closed to explicit resolver only, so a wire value this build does not know never sends the domain to the Control D upstream; without resolvers it is dropped. Switching between the explicit modes is detected on refresh. Generated upstreams are now identified by an UpstreamConfig.InternalDomain marker that records their mode and that no configuration file can set, instead of by the internal_ key prefix. An upstream a local ctrld.toml or custom config defines as internal_foo, or as a copy of a generated one, is an ordinary upstream again: it keeps the OS-resolver catch-all, recovery and query-health grading. The "resolvers_only" wire value is provisional until the API agrees on it; the dashboard does not offer strict mode yet.
This commit is contained in:
16 files changed
+1292
-142
No files matched your search
+33
@@ -139,6 +139,29 @@ func LanQueryCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, LanQueryCtxKey{}, 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() []string {
|
||||
ns := nameservers()
|
||||
@@ -590,6 +613,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"
|
||||
}
|
||||
|
||||
// Checking the cache first.
|
||||
if val, ok := o.cache.Load(key); ok {
|
||||
@@ -642,6 +671,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.
|
||||
|
||||
Reference in new issue
Block a user