mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d629ecda33 | |||
| 87ddf03b90 | |||
| d49a4c67c9 | |||
| 2c38ff74c3 | |||
| 75e8447c75 | |||
| 4395efcb22 |
@@ -9,7 +9,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||||
go: ["1.24.x"]
|
go: ["1.25.x"]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -21,6 +21,6 @@ jobs:
|
|||||||
- run: "go test -race ./..."
|
- run: "go test -race ./..."
|
||||||
- uses: dominikh/staticcheck-action@v1.4.0
|
- uses: dominikh/staticcheck-action@v1.4.0
|
||||||
with:
|
with:
|
||||||
version: "2025.1.1"
|
version: "2026.1"
|
||||||
install-go: false
|
install-go: false
|
||||||
cache-key: ${{ matrix.go }}
|
cache-key: ${{ matrix.go }}
|
||||||
|
|||||||
+37
-1
@@ -548,6 +548,7 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
if vpnServers := p.vpnDNS.UpstreamForDomain(domain); len(vpnServers) > 0 {
|
if vpnServers := p.vpnDNS.UpstreamForDomain(domain); len(vpnServers) > 0 {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "VPN DNS route matched for domain %s, using servers: %v", domain, vpnServers)
|
ctrld.Log(ctx, mainLog.Load().Debug(), "VPN DNS route matched for domain %s, using servers: %v", domain, vpnServers)
|
||||||
|
|
||||||
|
var gotTransportFailure bool
|
||||||
for _, server := range vpnServers {
|
for _, server := range vpnServers {
|
||||||
upstreamConfig := p.vpnDNS.upstreamConfigFor(server)
|
upstreamConfig := p.vpnDNS.upstreamConfigFor(server)
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "Querying VPN DNS server: %s", server)
|
ctrld.Log(ctx, mainLog.Load().Debug(), "Querying VPN DNS server: %s", server)
|
||||||
@@ -561,6 +562,7 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
answer, err := dnsResolver.Resolve(resolveCtx, req.msg)
|
answer, err := dnsResolver.Resolve(resolveCtx, req.msg)
|
||||||
cancel()
|
cancel()
|
||||||
if answer != nil {
|
if answer != nil {
|
||||||
|
p.vpnDNS.VPNDNSReachable()
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "VPN DNS query successful")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "VPN DNS query successful")
|
||||||
if p.cache != nil {
|
if p.cache != nil {
|
||||||
ttl := 60 * time.Second
|
ttl := 60 * time.Second
|
||||||
@@ -573,9 +575,22 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
}
|
}
|
||||||
return &proxyResponse{answer: answer}
|
return &proxyResponse{answer: answer}
|
||||||
}
|
}
|
||||||
|
gotTransportFailure = true
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug().Err(err), "VPN DNS server %s failed", server)
|
ctrld.Log(ctx, mainLog.Load().Debug().Err(err), "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, mainLog.Load().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}
|
||||||
|
}
|
||||||
|
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "All VPN DNS servers failed, falling back to normal upstreams")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "All VPN DNS servers failed, falling back to normal upstreams")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,13 +604,15 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
// polluting captive portal / DHCP flows.
|
// polluting captive portal / DHCP flows.
|
||||||
if dnsIntercept && p.vpnDNS != nil && req.ufr.matched &&
|
if dnsIntercept && p.vpnDNS != nil && req.ufr.matched &&
|
||||||
len(upstreams) > 0 && upstreams[0] == upstreamOS &&
|
len(upstreams) > 0 && upstreams[0] == upstreamOS &&
|
||||||
len(req.msg.Question) > 0 && !p.isAdDomainQuery(req.msg) {
|
len(req.msg.Question) > 0 {
|
||||||
if dlServers := p.vpnDNS.DomainlessServers(); len(dlServers) > 0 {
|
if dlServers := p.vpnDNS.DomainlessServers(); len(dlServers) > 0 {
|
||||||
domain := req.msg.Question[0].Name
|
domain := req.msg.Question[0].Name
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(),
|
ctrld.Log(ctx, mainLog.Load().Debug(),
|
||||||
"Split-rule query %s going to upstream.os, trying %d domain-less VPN DNS servers first: %v",
|
"Split-rule query %s going to upstream.os, trying %d domain-less VPN DNS servers first: %v",
|
||||||
domain, len(dlServers), dlServers)
|
domain, len(dlServers), dlServers)
|
||||||
|
|
||||||
|
var gotDNSAnswer bool
|
||||||
|
var gotTransportFailure bool
|
||||||
for _, server := range dlServers {
|
for _, server := range dlServers {
|
||||||
upstreamCfg := p.vpnDNS.upstreamConfigFor(server)
|
upstreamCfg := p.vpnDNS.upstreamConfigFor(server)
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "Querying domain-less VPN DNS server: %s", server)
|
ctrld.Log(ctx, mainLog.Load().Debug(), "Querying domain-less VPN DNS server: %s", server)
|
||||||
@@ -608,6 +625,10 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
resolveCtx, cancel := upstreamCfg.Context(ctx)
|
resolveCtx, cancel := upstreamCfg.Context(ctx)
|
||||||
answer, err := dnsResolver.Resolve(resolveCtx, req.msg)
|
answer, err := dnsResolver.Resolve(resolveCtx, req.msg)
|
||||||
cancel()
|
cancel()
|
||||||
|
if answer != nil {
|
||||||
|
gotDNSAnswer = true
|
||||||
|
p.vpnDNS.VPNDNSReachable()
|
||||||
|
}
|
||||||
if answer != nil && answer.Rcode == dns.RcodeSuccess {
|
if answer != nil && answer.Rcode == dns.RcodeSuccess {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(),
|
ctrld.Log(ctx, mainLog.Load().Debug(),
|
||||||
"Domain-less VPN DNS server %s answered %s successfully", server, domain)
|
"Domain-less VPN DNS server %s answered %s successfully", server, domain)
|
||||||
@@ -618,10 +639,25 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
|||||||
"Domain-less VPN DNS server %s returned %s for %s, trying next",
|
"Domain-less VPN DNS server %s returned %s for %s, trying next",
|
||||||
server, dns.RcodeToString[answer.Rcode], domain)
|
server, dns.RcodeToString[answer.Rcode], domain)
|
||||||
} else {
|
} else {
|
||||||
|
gotTransportFailure = true
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug().Err(err),
|
ctrld.Log(ctx, mainLog.Load().Debug().Err(err),
|
||||||
"Domain-less VPN DNS server %s failed for %s", server, domain)
|
"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, mainLog.Load().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}
|
||||||
|
}
|
||||||
|
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(),
|
ctrld.Log(ctx, mainLog.Load().Debug(),
|
||||||
"All domain-less VPN DNS servers failed for %s, falling back to OS resolver", domain)
|
"All domain-less VPN DNS servers failed for %s, falling back to OS resolver", domain)
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-19
@@ -3,6 +3,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var vpnDNSSettlingEnabled = runtime.GOOS == "windows"
|
||||||
|
|
||||||
// vpnDNSExemption represents a VPN DNS server that needs pf/WFP exemption,
|
// vpnDNSExemption represents a VPN DNS server that needs pf/WFP exemption,
|
||||||
// including the interface it was discovered on. The interface is used on macOS
|
// including the interface it was discovered on. The interface is used on macOS
|
||||||
// to create interface-scoped pf exemptions that allow the VPN's local DNS
|
// to create interface-scoped pf exemptions that allow the VPN's local DNS
|
||||||
@@ -38,6 +41,14 @@ type vpnDNSManager struct {
|
|||||||
// as additional nameservers for queries that match split-DNS rules
|
// as additional nameservers for queries that match split-DNS rules
|
||||||
// (from ctrld config, AD domain, or VPN suffix config).
|
// (from ctrld config, AD domain, or VPN suffix config).
|
||||||
domainlessServers []string
|
domainlessServers []string
|
||||||
|
// retainedAfterEmptyDiscovery means Windows reported an empty VPN DNS
|
||||||
|
// snapshot once while previous VPN DNS state existed. We keep that last-known
|
||||||
|
// state for one guarded refresh cycle because Windows can briefly report an
|
||||||
|
// intermediate empty adapter/DNS state after sleep/wake or reconnect.
|
||||||
|
retainedAfterEmptyDiscovery bool
|
||||||
|
// discoverVPNDNS is injected for tests so Refresh does not depend on the
|
||||||
|
// runner host's real VPN/virtual adapter state.
|
||||||
|
discoverVPNDNS func(context.Context) []ctrld.VPNDNSConfig
|
||||||
// Called when VPN DNS server list changes, to update intercept exemptions.
|
// Called when VPN DNS server list changes, to update intercept exemptions.
|
||||||
onServersChanged vpnDNSExemptFunc
|
onServersChanged vpnDNSExemptFunc
|
||||||
}
|
}
|
||||||
@@ -48,6 +59,7 @@ type vpnDNSManager struct {
|
|||||||
func newVPNDNSManager(exemptFunc vpnDNSExemptFunc) *vpnDNSManager {
|
func newVPNDNSManager(exemptFunc vpnDNSExemptFunc) *vpnDNSManager {
|
||||||
return &vpnDNSManager{
|
return &vpnDNSManager{
|
||||||
routes: make(map[string][]string),
|
routes: make(map[string][]string),
|
||||||
|
discoverVPNDNS: ctrld.DiscoverVPNDNS,
|
||||||
onServersChanged: exemptFunc,
|
onServersChanged: exemptFunc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +70,11 @@ func (m *vpnDNSManager) Refresh(guardAgainstNoNameservers bool) {
|
|||||||
logger := mainLog.Load()
|
logger := mainLog.Load()
|
||||||
|
|
||||||
logger.Debug().Msg("Refreshing VPN DNS configurations")
|
logger.Debug().Msg("Refreshing VPN DNS configurations")
|
||||||
configs := ctrld.DiscoverVPNDNS(context.Background())
|
discoverVPNDNS := m.discoverVPNDNS
|
||||||
|
if discoverVPNDNS == nil {
|
||||||
|
discoverVPNDNS = ctrld.DiscoverVPNDNS
|
||||||
|
}
|
||||||
|
configs := discoverVPNDNS(context.Background())
|
||||||
|
|
||||||
// Detect exit mode: if the default route goes through a VPN DNS interface,
|
// Detect exit mode: if the default route goes through a VPN DNS interface,
|
||||||
// the VPN is routing ALL traffic (exit node / full tunnel). This is more
|
// the VPN is routing ALL traffic (exit node / full tunnel). This is more
|
||||||
@@ -78,6 +94,29 @@ func (m *vpnDNSManager) Refresh(guardAgainstNoNameservers bool) {
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if vpnDNSSettlingEnabled && len(configs) == 0 && guardAgainstNoNameservers && m.hasVPNDNSStateLocked() {
|
||||||
|
if !m.retainedAfterEmptyDiscovery {
|
||||||
|
exemptions := m.currentExemptionsLocked()
|
||||||
|
m.retainedAfterEmptyDiscovery = true
|
||||||
|
logger.Debug().Msgf(
|
||||||
|
"VPN DNS discovery empty; retaining last-known VPN DNS state for one guarded refresh (%d domainless servers, %d exemptions)",
|
||||||
|
len(m.domainlessServers), len(exemptions))
|
||||||
|
if m.onServersChanged != nil {
|
||||||
|
if err := m.onServersChanged(exemptions); err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to re-apply retained VPN DNS exemptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug().Msgf(
|
||||||
|
"VPN DNS discovery still empty on next guarded refresh; clearing retained VPN DNS state (%d domainless servers)",
|
||||||
|
len(m.domainlessServers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any discovery path that does not return with retained state clears the
|
||||||
|
// settling marker: non-empty discovery replaces old servers immediately, and
|
||||||
|
// an unguarded/second empty discovery clears stale state below.
|
||||||
|
m.retainedAfterEmptyDiscovery = false
|
||||||
m.configs = configs
|
m.configs = configs
|
||||||
m.routes = make(map[string][]string)
|
m.routes = make(map[string][]string)
|
||||||
|
|
||||||
@@ -151,6 +190,63 @@ func (m *vpnDNSManager) Refresh(guardAgainstNoNameservers bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *vpnDNSManager) hasVPNDNSStateLocked() bool {
|
||||||
|
return len(m.configs) > 0 || len(m.routes) > 0 || len(m.domainlessServers) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *vpnDNSManager) currentExemptionsLocked() []vpnDNSExemption {
|
||||||
|
type key struct{ server, iface string }
|
||||||
|
seen := make(map[key]bool)
|
||||||
|
var exemptions []vpnDNSExemption
|
||||||
|
for _, config := range m.configs {
|
||||||
|
for _, server := range config.Servers {
|
||||||
|
k := key{server, config.InterfaceName}
|
||||||
|
if seen[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[k] = true
|
||||||
|
exemptions = append(exemptions, vpnDNSExemption{
|
||||||
|
Server: server,
|
||||||
|
Interface: config.InterfaceName,
|
||||||
|
IsExitMode: config.IsExitMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exemptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldFailClosedAfterVPNDNSTransportFailure reports whether split-rule
|
||||||
|
// queries should fail closed instead of falling back to OS/public DNS after
|
||||||
|
// every candidate VPN DNS server failed before returning a DNS packet. This is
|
||||||
|
// Windows-only and only active while serving retained VPN DNS state from a
|
||||||
|
// guarded empty discovery, which is the short window where Windows can report
|
||||||
|
// VPN DNS before routes to those servers are usable after wake/reconnect.
|
||||||
|
func (m *vpnDNSManager) ShouldFailClosedAfterVPNDNSTransportFailure(domain string, servers []string) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !vpnDNSSettlingEnabled || len(servers) == 0 || !m.retainedAfterEmptyDiscovery || !m.hasVPNDNSStateLocked() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLog.Load().Debug().Msgf(
|
||||||
|
"VPN DNS transport failed for %s while retained VPN DNS state is active; suppressing OS fallback for this query (servers=%v)",
|
||||||
|
domain, servers)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPNDNSReachable records that a VPN DNS server returned a DNS response. The
|
||||||
|
// response may be negative (NXDOMAIN/SERVFAIL); the important signal is that
|
||||||
|
// the VPN DNS transport is reachable again.
|
||||||
|
func (m *vpnDNSManager) VPNDNSReachable() {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if m.retainedAfterEmptyDiscovery {
|
||||||
|
mainLog.Load().Debug().Msg("VPN DNS transport recovered; clearing retained-empty-discovery state")
|
||||||
|
}
|
||||||
|
m.retainedAfterEmptyDiscovery = false
|
||||||
|
}
|
||||||
|
|
||||||
// UpstreamForDomain checks if the domain matches any VPN search domain.
|
// UpstreamForDomain checks if the domain matches any VPN search domain.
|
||||||
// Returns VPN DNS servers if matched, nil otherwise.
|
// Returns VPN DNS servers if matched, nil otherwise.
|
||||||
func (m *vpnDNSManager) UpstreamForDomain(domain string) []string {
|
func (m *vpnDNSManager) UpstreamForDomain(domain string) []string {
|
||||||
@@ -208,24 +304,7 @@ func (m *vpnDNSManager) CurrentServers() []string {
|
|||||||
func (m *vpnDNSManager) CurrentExemptions() []vpnDNSExemption {
|
func (m *vpnDNSManager) CurrentExemptions() []vpnDNSExemption {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
return m.currentExemptionsLocked()
|
||||||
type key struct{ server, iface string }
|
|
||||||
seen := make(map[key]bool)
|
|
||||||
var exemptions []vpnDNSExemption
|
|
||||||
for _, config := range m.configs {
|
|
||||||
for _, server := range config.Servers {
|
|
||||||
k := key{server, config.InterfaceName}
|
|
||||||
if !seen[k] {
|
|
||||||
seen[k] = true
|
|
||||||
exemptions = append(exemptions, vpnDNSExemption{
|
|
||||||
Server: server,
|
|
||||||
Interface: config.InterfaceName,
|
|
||||||
IsExitMode: config.IsExitMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return exemptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes returns a copy of the current VPN DNS routes for debugging.
|
// Routes returns a copy of the current VPN DNS routes for debugging.
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
func withVPNDNSSettlingEnabled(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
old := vpnDNSSettlingEnabled
|
||||||
|
vpnDNSSettlingEnabled = true
|
||||||
|
t.Cleanup(func() { vpnDNSSettlingEnabled = old })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPNDNSRefreshRetainsStateForOneGuardedEmptyDiscovery(t *testing.T) {
|
||||||
|
withVPNDNSSettlingEnabled(t)
|
||||||
|
var gotExemptions []vpnDNSExemption
|
||||||
|
m := newVPNDNSManager(func(exemptions []vpnDNSExemption) error {
|
||||||
|
gotExemptions = exemptions
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
m.discoverVPNDNS = func(context.Context) []ctrld.VPNDNSConfig { return nil }
|
||||||
|
m.configs = []ctrld.VPNDNSConfig{{
|
||||||
|
InterfaceName: "Ethernet 6",
|
||||||
|
Servers: []string{"10.25.37.21", "10.25.37.22"},
|
||||||
|
}}
|
||||||
|
m.domainlessServers = []string{"10.25.37.21", "10.25.37.22"}
|
||||||
|
|
||||||
|
m.Refresh(true)
|
||||||
|
|
||||||
|
if got := m.DomainlessServers(); len(got) != 2 {
|
||||||
|
t.Fatalf("expected retained domainless servers, got %v", got)
|
||||||
|
}
|
||||||
|
if len(gotExemptions) != 2 {
|
||||||
|
t.Fatalf("expected retained exemptions to be re-applied, got %v", gotExemptions)
|
||||||
|
}
|
||||||
|
if !m.retainedAfterEmptyDiscovery {
|
||||||
|
t.Fatal("expected empty discovery retention to be marked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPNDNSRefreshClearsOnSecondGuardedEmptyDiscovery(t *testing.T) {
|
||||||
|
withVPNDNSSettlingEnabled(t)
|
||||||
|
var gotExemptions []vpnDNSExemption
|
||||||
|
m := newVPNDNSManager(func(exemptions []vpnDNSExemption) error {
|
||||||
|
gotExemptions = exemptions
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
m.discoverVPNDNS = func(context.Context) []ctrld.VPNDNSConfig { return nil }
|
||||||
|
m.configs = []ctrld.VPNDNSConfig{{
|
||||||
|
InterfaceName: "Ethernet 6",
|
||||||
|
Servers: []string{"10.25.37.21"},
|
||||||
|
}}
|
||||||
|
m.domainlessServers = []string{"10.25.37.21"}
|
||||||
|
m.retainedAfterEmptyDiscovery = true
|
||||||
|
|
||||||
|
m.Refresh(true)
|
||||||
|
|
||||||
|
if got := m.DomainlessServers(); len(got) != 0 {
|
||||||
|
t.Fatalf("expected domainless servers to be cleared on second empty discovery, got %v", got)
|
||||||
|
}
|
||||||
|
if len(gotExemptions) != 0 {
|
||||||
|
t.Fatalf("expected empty exemptions after clearing stale state, got %v", gotExemptions)
|
||||||
|
}
|
||||||
|
if m.retainedAfterEmptyDiscovery {
|
||||||
|
t.Fatal("expected retained empty-discovery marker to be cleared with stale state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPNDNSTransportFailureSuppressesFallbackOnlyWhileRetainingState(t *testing.T) {
|
||||||
|
withVPNDNSSettlingEnabled(t)
|
||||||
|
m := newVPNDNSManager(nil)
|
||||||
|
m.domainlessServers = []string{"10.25.37.21"}
|
||||||
|
|
||||||
|
if m.ShouldFailClosedAfterVPNDNSTransportFailure("splunk.aws.arena.net.", []string{"10.25.37.21"}) {
|
||||||
|
t.Fatal("did not expect transport failure to suppress OS fallback outside retained empty-discovery state")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.retainedAfterEmptyDiscovery = true
|
||||||
|
if !m.ShouldFailClosedAfterVPNDNSTransportFailure("splunk.aws.arena.net.", []string{"10.25.37.21"}) {
|
||||||
|
t.Fatal("expected transport failure to suppress OS fallback while retained state is active")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.VPNDNSReachable()
|
||||||
|
if m.retainedAfterEmptyDiscovery {
|
||||||
|
t.Fatal("expected reachable DNS response to clear retained empty-discovery state")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/Control-D-Inc/ctrld
|
module github.com/Control-D-Inc/ctrld
|
||||||
|
|
||||||
go 1.24
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.2.1
|
github.com/Masterminds/semver/v3 v3.2.1
|
||||||
@@ -36,9 +36,9 @@ require (
|
|||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.55.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/sys v0.35.0
|
golang.org/x/sys v0.45.0
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
tailscale.com v1.74.0
|
tailscale.com v1.74.0
|
||||||
)
|
)
|
||||||
@@ -92,11 +92,11 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.35.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
|
|||||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -361,8 +361,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -386,8 +386,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -420,8 +420,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -441,8 +441,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -492,8 +492,8 @@ golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepC
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -504,8 +504,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -558,8 +558,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
+52
-9
@@ -56,6 +56,8 @@ const (
|
|||||||
var (
|
var (
|
||||||
dcRetryMu sync.Mutex
|
dcRetryMu sync.Mutex
|
||||||
dcRetryCancel context.CancelFunc
|
dcRetryCancel context.CancelFunc
|
||||||
|
dcRetryDomain string
|
||||||
|
dcRetryID uint64
|
||||||
|
|
||||||
// Lazy-loaded netapi32 for DsGetDcNameW calls.
|
// Lazy-loaded netapi32 for DsGetDcNameW calls.
|
||||||
netapi32DLL = windows.NewLazySystemDLL("netapi32.dll")
|
netapi32DLL = windows.NewLazySystemDLL("netapi32.dll")
|
||||||
@@ -133,9 +135,6 @@ func dnsFromAdapter() []string {
|
|||||||
func getDNSServers(ctx context.Context) ([]string, error) {
|
func getDNSServers(ctx context.Context) ([]string, error) {
|
||||||
logger := *ProxyLogger.Load()
|
logger := *ProxyLogger.Load()
|
||||||
|
|
||||||
// Cancel any in-flight DC retry from a previous network state.
|
|
||||||
cancelDCRetry()
|
|
||||||
|
|
||||||
// Check context before making the call
|
// Check context before making the call
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
@@ -157,6 +156,9 @@ func getDNSServers(ctx context.Context) ([]string, error) {
|
|||||||
var dcServers []string
|
var dcServers []string
|
||||||
var adDomain string
|
var adDomain string
|
||||||
isDomain := checkDomainJoined()
|
isDomain := checkDomainJoined()
|
||||||
|
if !isDomain {
|
||||||
|
cancelDCRetry()
|
||||||
|
}
|
||||||
if isDomain {
|
if isDomain {
|
||||||
domainName, err := system.GetActiveDirectoryDomain()
|
domainName, err := system.GetActiveDirectoryDomain()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,6 +166,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
|
|||||||
"Failed to get local AD domain: %v", err)
|
"Failed to get local AD domain: %v", err)
|
||||||
} else {
|
} else {
|
||||||
adDomain = domainName
|
adDomain = domainName
|
||||||
|
cancelDCRetryForOtherDomain(domainName)
|
||||||
// Load netapi32.dll
|
// Load netapi32.dll
|
||||||
var info *DomainControllerInfo
|
var info *DomainControllerInfo
|
||||||
flags := uint32(DS_RETURN_DNS_NAME | DS_IP_REQUIRED | DS_IS_DNS_NAME)
|
flags := uint32(DS_RETURN_DNS_NAME | DS_IP_REQUIRED | DS_IS_DNS_NAME)
|
||||||
@@ -205,7 +208,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
|
|||||||
// Start background retry for transient DC errors.
|
// Start background retry for transient DC errors.
|
||||||
if isTransientDCError(ret) {
|
if isTransientDCError(ret) {
|
||||||
Log(ctx, logger.Info(),
|
Log(ctx, logger.Info(),
|
||||||
"AD DC detection failed with transient error %d for %s, starting background retry", ret, domainName)
|
"AD DC detection failed with retryable error %d for %s, ensuring background retry", ret, domainName)
|
||||||
startDCRetry(domainName)
|
startDCRetry(domainName)
|
||||||
}
|
}
|
||||||
} else if info != nil {
|
} else if info != nil {
|
||||||
@@ -219,6 +222,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
|
|||||||
|
|
||||||
if ip := net.ParseIP(dcAddr); ip != nil {
|
if ip := net.ParseIP(dcAddr); ip != nil {
|
||||||
dcServers = append(dcServers, ip.String())
|
dcServers = append(dcServers, ip.String())
|
||||||
|
cancelDCRetry()
|
||||||
Log(ctx, logger.Debug(),
|
Log(ctx, logger.Debug(),
|
||||||
"Added domain controller DNS servers: %v", dcServers)
|
"Added domain controller DNS servers: %v", dcServers)
|
||||||
}
|
}
|
||||||
@@ -387,10 +391,13 @@ func checkDomainJoined() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isTransientDCError returns true if the DsGetDcName error code indicates
|
// isTransientDCError returns true if the DsGetDcName error code indicates
|
||||||
// a transient failure that may succeed on retry.
|
// a transient failure that may succeed on retry. ERROR_NO_SUCH_DOMAIN is
|
||||||
|
// retryable here because we only call this path after Windows already reported
|
||||||
|
// the machine is domain joined and returned a local AD domain name; during VPN
|
||||||
|
// DNS churn, DC locator can temporarily fail to resolve that known domain.
|
||||||
func isTransientDCError(code uintptr) bool {
|
func isTransientDCError(code uintptr) bool {
|
||||||
switch code {
|
switch code {
|
||||||
case errConnReset, errRPCUnavailable, errNoLogonServers, errDCNotFound, errNetUnreachable:
|
case errNoSuchDomain, errConnReset, errRPCUnavailable, errNoLogonServers, errDCNotFound, errNetUnreachable:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -404,23 +411,49 @@ func cancelDCRetry() {
|
|||||||
if dcRetryCancel != nil {
|
if dcRetryCancel != nil {
|
||||||
dcRetryCancel()
|
dcRetryCancel()
|
||||||
dcRetryCancel = nil
|
dcRetryCancel = nil
|
||||||
|
dcRetryDomain = ""
|
||||||
|
dcRetryID = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cancelDCRetryForOtherDomain keeps an existing retry alive during noisy
|
||||||
|
// network-change refreshes, but stops it if Windows reports a different AD
|
||||||
|
// domain. This avoids the start/cancel storm seen when DsGetDcName briefly
|
||||||
|
// returns ERROR_NO_SUCH_DOMAIN while VPN DNS is still settling.
|
||||||
|
func cancelDCRetryForOtherDomain(domainName string) {
|
||||||
|
dcRetryMu.Lock()
|
||||||
|
defer dcRetryMu.Unlock()
|
||||||
|
if dcRetryCancel == nil || dcRetryDomain == "" || strings.EqualFold(dcRetryDomain, domainName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dcRetryCancel()
|
||||||
|
dcRetryCancel = nil
|
||||||
|
dcRetryDomain = ""
|
||||||
|
dcRetryID = 0
|
||||||
|
}
|
||||||
|
|
||||||
// startDCRetry spawns a background goroutine that retries DsGetDcName with
|
// startDCRetry spawns a background goroutine that retries DsGetDcName with
|
||||||
// exponential backoff. On success it appends the DC IP to the OS resolver.
|
// exponential backoff. On success it appends the DC IP to the OS resolver.
|
||||||
func startDCRetry(domainName string) {
|
func startDCRetry(domainName string) {
|
||||||
dcRetryMu.Lock()
|
dcRetryMu.Lock()
|
||||||
// Cancel any previous retry.
|
if dcRetryCancel != nil && strings.EqualFold(dcRetryDomain, domainName) {
|
||||||
|
ProxyLogger.Load().Debug().Msgf("AD DC retry already running for domain %s", domainName)
|
||||||
|
dcRetryMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
if dcRetryCancel != nil {
|
if dcRetryCancel != nil {
|
||||||
dcRetryCancel()
|
dcRetryCancel()
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
dcRetryID++
|
||||||
|
retryID := dcRetryID
|
||||||
dcRetryCancel = cancel
|
dcRetryCancel = cancel
|
||||||
|
dcRetryDomain = domainName
|
||||||
dcRetryMu.Unlock()
|
dcRetryMu.Unlock()
|
||||||
|
|
||||||
go func() {
|
go func(retryID uint64) {
|
||||||
logger := *ProxyLogger.Load()
|
logger := *ProxyLogger.Load()
|
||||||
|
defer clearDCRetryIfCurrent(domainName, retryID)
|
||||||
delay := dcRetryInitialDelay
|
delay := dcRetryInitialDelay
|
||||||
|
|
||||||
for attempt := 1; attempt <= dcRetryMaxAttempts; attempt++ {
|
for attempt := 1; attempt <= dcRetryMaxAttempts; attempt++ {
|
||||||
@@ -472,7 +505,17 @@ func startDCRetry(domainName string) {
|
|||||||
|
|
||||||
Log(ctx, logger.Warn(),
|
Log(ctx, logger.Warn(),
|
||||||
"AD DC retry exhausted %d attempts for domain %s", dcRetryMaxAttempts, domainName)
|
"AD DC retry exhausted %d attempts for domain %s", dcRetryMaxAttempts, domainName)
|
||||||
}()
|
}(retryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearDCRetryIfCurrent(domainName string, retryID uint64) {
|
||||||
|
dcRetryMu.Lock()
|
||||||
|
defer dcRetryMu.Unlock()
|
||||||
|
if dcRetryCancel != nil && dcRetryID == retryID && strings.EqualFold(dcRetryDomain, domainName) {
|
||||||
|
dcRetryCancel = nil
|
||||||
|
dcRetryDomain = ""
|
||||||
|
dcRetryID = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryGetDCAddress attempts a single DsGetDcName call and returns the DC IP on success,
|
// tryGetDCAddress attempts a single DsGetDcName call and returns the DC IP on success,
|
||||||
|
|||||||
Reference in New Issue
Block a user