mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
fix: port Windows VPN DNS settling fix to master
This commit is contained in:
committed by
Cuong Manh Le
parent
0d183feddb
commit
3c740b9693
+79
-6
@@ -639,13 +639,14 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
if vpnServers := p.vpnDNS.UpstreamForDomain(domain); len(vpnServers) > 0 {
|
||||
ctrld.Log(ctx, p.Debug(), "VPN DNS route matched for domain %s, using servers: %v", domain, vpnServers)
|
||||
|
||||
// Try each VPN DNS server
|
||||
var gotTransportFailure bool
|
||||
for _, server := range vpnServers {
|
||||
upstreamConfig := p.vpnDNS.upstreamConfigFor(server)
|
||||
ctrld.Log(ctx, p.Debug(), "Querying VPN DNS server: %s", server)
|
||||
|
||||
answer := p.queryUpstream(ctx, req, "vpn-dns", upstreamConfig)
|
||||
if answer != nil {
|
||||
p.vpnDNS.VPNDNSReachable()
|
||||
ctrld.Log(ctx, p.Debug(), "VPN DNS query successful")
|
||||
|
||||
// Update cache if enabled
|
||||
@@ -654,15 +655,87 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||
}
|
||||
|
||||
return &proxyResponse{answer: answer, cached: false}
|
||||
} else {
|
||||
ctrld.Log(ctx, p.Debug(), "VPN DNS server %s failed", server)
|
||||
}
|
||||
gotTransportFailure = true
|
||||
ctrld.Log(ctx, p.Debug(), "VPN DNS server %s failed", server)
|
||||
}
|
||||
|
||||
// Explicit VPN DNS routes are authoritative for their suffix. If all
|
||||
// routed servers fail at the transport layer while Windows is serving
|
||||
// retained VPN DNS state, fail closed instead of leaking VPN/internal
|
||||
// names to normal upstreams.
|
||||
if gotTransportFailure && p.vpnDNS.ShouldFailClosedAfterVPNDNSTransportFailure(domain, vpnServers) {
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"All VPN DNS servers had transport failures for %s; returning SERVFAIL while retained VPN DNS state is active", domain)
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(req.msg, dns.RcodeServerFailure)
|
||||
return &proxyResponse{answer: answer, cached: false}
|
||||
}
|
||||
|
||||
ctrld.Log(ctx, p.Debug(), "All VPN DNS servers failed, falling back to normal upstreams")
|
||||
}
|
||||
}
|
||||
|
||||
// Domain-less VPN DNS fallback: when a query is going to upstream.os via a
|
||||
// split-rule (matched policy) and we have VPN DNS servers with no associated
|
||||
// domains, try those servers for this query. This handles cases like F5 VPN
|
||||
// where the VPN doesn't advertise DNS search domains but its DNS servers
|
||||
// know the internal zones referenced by split-rules (e.g., *.provisur.local).
|
||||
// These servers are NOT used for general OS resolver queries to avoid
|
||||
// polluting captive portal / DHCP flows.
|
||||
if dnsIntercept && p.vpnDNS != nil && req.ufr.matched &&
|
||||
len(upstreams) > 0 && upstreams[0] == upstreamOS &&
|
||||
len(req.msg.Question) > 0 {
|
||||
if dlServers := p.vpnDNS.DomainlessServers(); len(dlServers) > 0 {
|
||||
domain := req.msg.Question[0].Name
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"Split-rule query %s going to upstream.os, trying %d domain-less VPN DNS servers first: %v",
|
||||
domain, len(dlServers), dlServers)
|
||||
|
||||
var gotDNSAnswer bool
|
||||
var gotTransportFailure bool
|
||||
for _, server := range dlServers {
|
||||
upstreamConfig := p.vpnDNS.upstreamConfigFor(server)
|
||||
ctrld.Log(ctx, p.Debug(), "Querying domain-less VPN DNS server: %s", server)
|
||||
|
||||
answer := p.queryUpstream(ctx, req, "vpn-dns", upstreamConfig)
|
||||
if answer != nil {
|
||||
gotDNSAnswer = true
|
||||
p.vpnDNS.VPNDNSReachable()
|
||||
}
|
||||
if answer != nil && answer.Rcode == dns.RcodeSuccess {
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"Domain-less VPN DNS server %s answered %s successfully", server, domain)
|
||||
return &proxyResponse{answer: answer, cached: false}
|
||||
}
|
||||
if answer != nil {
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"Domain-less VPN DNS server %s returned %s for %s, trying next",
|
||||
server, dns.RcodeToString[answer.Rcode], domain)
|
||||
} else {
|
||||
gotTransportFailure = true
|
||||
ctrld.Log(ctx, p.Debug(), "Domain-less VPN DNS server %s failed for %s", server, domain)
|
||||
}
|
||||
}
|
||||
|
||||
// If every domainless VPN DNS attempt failed before receiving a DNS
|
||||
// packet while Windows is serving retained VPN DNS state, fail closed
|
||||
// instead of asking LAN/public DNS about internal split-rule names and
|
||||
// caching false negatives. Reachable negative DNS responses still fall
|
||||
// through to the old OS fallback behavior below.
|
||||
if !gotDNSAnswer && gotTransportFailure && p.vpnDNS.ShouldFailClosedAfterVPNDNSTransportFailure(domain, dlServers) {
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"All domain-less VPN DNS servers had transport failures for %s; returning SERVFAIL while retained VPN DNS state is active", domain)
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(req.msg, dns.RcodeServerFailure)
|
||||
return &proxyResponse{answer: answer, cached: false}
|
||||
}
|
||||
|
||||
ctrld.Log(ctx, p.Debug(),
|
||||
"All domain-less VPN DNS servers failed for %s, falling back to OS resolver", domain)
|
||||
}
|
||||
}
|
||||
|
||||
ctrld.Log(ctx, p.Debug(), "No cache hit, trying upstreams")
|
||||
if res := p.tryUpstreams(ctx, req, upstreams, upstreamConfigs); res != nil {
|
||||
ctrld.Log(ctx, p.Debug(), "Upstream query successful")
|
||||
@@ -1662,7 +1735,7 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error {
|
||||
// even though the physical interface didn't change. Runs after tunnel checks
|
||||
// so the pf anchor rebuild includes current VPN DNS exemptions.
|
||||
if dnsIntercept && p.vpnDNS != nil {
|
||||
p.vpnDNS.Refresh(ctx)
|
||||
p.vpnDNS.Refresh(ctx, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1749,7 +1822,7 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error {
|
||||
// Refresh VPN DNS routes — runs after tunnel checks so the anchor
|
||||
// rebuild includes current VPN DNS exemptions.
|
||||
if p.vpnDNS != nil {
|
||||
p.vpnDNS.Refresh(ctrld.LoggerCtx(ctx, p.logger.Load()))
|
||||
p.vpnDNS.Refresh(ctrld.LoggerCtx(ctx, p.logger.Load()), true)
|
||||
}
|
||||
// Schedule delayed re-checks to catch async VPN teardown changes.
|
||||
p.scheduleDelayedRechecks()
|
||||
@@ -2077,7 +2150,7 @@ func (p *prog) completeRecovery(reason RecoveryReason, recovered string) error {
|
||||
// This also re-exempts VPN DNS servers (which may have changed) and
|
||||
// removes any DHCP exemptions that were added during recovery.
|
||||
if p.vpnDNS != nil {
|
||||
p.vpnDNS.Refresh(ctrld.LoggerCtx(context.Background(), p.logger.Load()))
|
||||
p.vpnDNS.Refresh(ctrld.LoggerCtx(context.Background(), p.logger.Load()), true)
|
||||
}
|
||||
|
||||
// Reinitialize OS resolver for the recovered state.
|
||||
|
||||
Reference in New Issue
Block a user