Merge pull request #254 from Control-D-Inc/release-branch-v1.4.6

Release branch v1.4.6
This commit is contained in:
Cuong Manh Le
2025-08-22 04:08:56 +07:00
committed by GitHub
9 changed files with 143 additions and 46 deletions
+2 -2
View File
@@ -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.23.x"] go: ["1.24.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.3.1 - uses: dominikh/staticcheck-action@v1.3.1
with: with:
version: "2024.1.1" version: "2025.1"
install-go: false install-go: false
cache-key: ${{ matrix.go }} cache-key: ${{ matrix.go }}
+3 -11
View File
@@ -84,13 +84,7 @@ type upstreamForResult struct {
srcAddr string srcAddr string
} }
func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error { func (p *prog) serveDNS(listenerNum string) error {
// Start network monitoring
if err := p.monitorNetworkChanges(mainCtx); err != nil {
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
// Don't return here as we still want DNS service to run
}
listenerConfig := p.cfg.Listener[listenerNum] listenerConfig := p.cfg.Listener[listenerNum]
// make sure ip is allocated // make sure ip is allocated
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil { if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
@@ -1187,7 +1181,7 @@ func FlushDNSCache() error {
} }
// monitorNetworkChanges starts monitoring for network interface changes // monitorNetworkChanges starts monitoring for network interface changes
func (p *prog) monitorNetworkChanges(ctx context.Context) error { func (p *prog) monitorNetworkChanges() error {
mon, err := netmon.New(func(format string, args ...any) { mon, err := netmon.New(func(format string, args ...any) {
// Always fetch the latest logger (and inject the prefix) // Always fetch the latest logger (and inject the prefix)
mainLog.Load().Printf("netmon: "+format, args...) mainLog.Load().Printf("netmon: "+format, args...)
@@ -1406,9 +1400,6 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
return err return err
} }
msg := new(dns.Msg)
msg.SetQuestion(".", dns.TypeNS)
timeout := 1000 * time.Millisecond timeout := 1000 * time.Millisecond
if uc.Timeout > 0 { if uc.Timeout > 0 {
timeout = time.Millisecond * time.Duration(uc.Timeout) timeout = time.Millisecond * time.Duration(uc.Timeout)
@@ -1422,6 +1413,7 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
mainLog.Load().Debug().Msgf("Rebootstrapping resolver for upstream: %s", upstream) mainLog.Load().Debug().Msgf("Rebootstrapping resolver for upstream: %s", upstream)
start := time.Now() start := time.Now()
msg := uc.VerifyMsg()
_, err = resolver.Resolve(ctx, msg) _, err = resolver.Resolve(ctx, msg)
duration := time.Since(start) duration := time.Since(start)
+10 -1
View File
@@ -530,6 +530,15 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
go p.watchLinkState(ctx) go p.watchLinkState(ctx)
} }
if !reload {
go func() {
// Start network monitoring
if err := p.monitorNetworkChanges(); err != nil {
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
}
}()
}
for listenerNum := range p.cfg.Listener { for listenerNum := range p.cfg.Listener {
p.cfg.Listener[listenerNum].Init() p.cfg.Listener[listenerNum].Init()
if !reload { if !reload {
@@ -541,7 +550,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
} }
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr) mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
if err := p.serveDNS(ctx, listenerNum); err != nil { if err := p.serveDNS(listenerNum); err != nil {
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum) mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
} }
mainLog.Load().Debug().Msgf("end of serveDNS listener.%s: %s", listenerNum, addr) mainLog.Load().Debug().Msgf("end of serveDNS listener.%s: %s", listenerNum, addr)
+9
View File
@@ -358,6 +358,15 @@ func (uc *UpstreamConfig) Init() {
} }
} }
// VerifyMsg creates and returns a new DNS message could be used for testing upstream health.
func (uc *UpstreamConfig) VerifyMsg() *dns.Msg {
msg := new(dns.Msg)
msg.RecursionDesired = true
msg.SetQuestion(".", dns.TypeNS)
msg.SetEdns0(4096, false) // ensure handling of large DNS response
return msg
}
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint. // VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
// It returns empty for non-ControlD upstream endpoint. // It returns empty for non-ControlD upstream endpoint.
func (uc *UpstreamConfig) VerifyDomain() string { func (uc *UpstreamConfig) VerifyDomain() string {
+1
View File
@@ -16,4 +16,5 @@ var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense "/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
"/var/db/dnsmasq.leases": ctrld.Dnsmasq, // OPNsense
} }
-1
View File
@@ -74,7 +74,6 @@ func (m *mdns) lookupIPByHostname(name string, v6 bool) string {
if value == name { if value == name {
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 { if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
ip = addr.String() ip = addr.String()
//lint:ignore S1008 This is used for readable.
if addr.IsLoopback() { // Continue searching if this is loopback address. if addr.IsLoopback() { // Continue searching if this is loopback address.
return true return true
} }
+1 -3
View File
@@ -104,7 +104,6 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
if value == name { if value == name {
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 { if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
ip = addr.String() ip = addr.String()
//lint:ignore S1008 This is used for readable.
if addr.IsLoopback() { // Continue searching if this is loopback address. if addr.IsLoopback() { // Continue searching if this is loopback address.
return true return true
} }
@@ -120,8 +119,7 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
// is reachable, set p.serverDown to false, so p.lookupHostname can continue working. // is reachable, set p.serverDown to false, so p.lookupHostname can continue working.
func (p *ptrDiscover) checkServer() { func (p *ptrDiscover) checkServer() {
bo := backoff.NewBackoff("ptrDiscover", func(format string, args ...any) {}, time.Minute*5) bo := backoff.NewBackoff("ptrDiscover", func(format string, args ...any) {}, time.Minute*5)
m := new(dns.Msg) m := (&ctrld.UpstreamConfig{}).VerifyMsg()
m.SetQuestion(".", dns.TypeNS)
ping := func() error { ping := func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
+26 -28
View File
@@ -23,20 +23,17 @@ import (
) )
const ( const (
maxDNSAdapterRetries = 5 maxDNSAdapterRetries = 5
retryDelayDNSAdapter = 1 * time.Second retryDelayDNSAdapter = 1 * time.Second
defaultDNSAdapterTimeout = 10 * time.Second defaultDNSAdapterTimeout = 10 * time.Second
minDNSServers = 1 // Minimum number of DNS servers we want to find minDNSServers = 1 // Minimum number of DNS servers we want to find
NetSetupUnknown uint32 = 0
NetSetupWorkgroup uint32 = 1 DS_FORCE_REDISCOVERY = 0x00000001
NetSetupDomain uint32 = 2 DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
NetSetupCloudDomain uint32 = 3 DS_BACKGROUND_ONLY = 0x00000100
DS_FORCE_REDISCOVERY = 0x00000001 DS_IP_REQUIRED = 0x00000200
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010 DS_IS_DNS_NAME = 0x00020000
DS_BACKGROUND_ONLY = 0x00000100 DS_RETURN_DNS_NAME = 0x40000000
DS_IP_REQUIRED = 0x00000200
DS_IS_DNS_NAME = 0x00020000
DS_RETURN_DNS_NAME = 0x40000000
) )
type DomainControllerInfo struct { type DomainControllerInfo struct {
@@ -158,7 +155,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
0, // DomainGuid - not needed 0, // DomainGuid - not needed
0, // SiteName - not needed 0, // SiteName - not needed
uintptr(flags), // Flags uintptr(flags), // Flags
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
if ret != 0 { if ret != 0 {
switch ret { switch ret {
@@ -343,27 +340,28 @@ func checkDomainJoined() bool {
var domain *uint16 var domain *uint16
var status uint32 var status uint32
err := windows.NetGetJoinInformation(nil, &domain, &status) if err := windows.NetGetJoinInformation(nil, &domain, &status); err != nil {
if err != nil { Log(context.Background(), logger.Debug(), "Failed to get domain join status: %v", err)
Log(context.Background(), logger.Debug(),
"Failed to get domain join status: %v", err)
return false return false
} }
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain))) defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
// NETSETUP_JOIN_STATUS constants from Microsoft Windows API
// See: https://learn.microsoft.com/en-us/windows/win32/api/lmjoin/ne-lmjoin-netsetup_join_status
//
// NetSetupUnknownStatus uint32 = 0 // The status is unknown
// NetSetupUnjoined uint32 = 1 // The computer is not joined to a domain or workgroup
// NetSetupWorkgroupName uint32 = 2 // The computer is joined to a workgroup
// NetSetupDomainName uint32 = 3 // The computer is joined to a domain
//
// We only care about NetSetupDomainName.
domainName := windows.UTF16PtrToString(domain) domainName := windows.UTF16PtrToString(domain)
Log(context.Background(), logger.Debug(), Log(context.Background(), logger.Debug(),
"Domain join status: domain=%s status=%d (Unknown=0, Workgroup=1, Domain=2, CloudDomain=3)", "Domain join status: domain=%s status=%d (UnknownStatus=0, Unjoined=1, WorkgroupName=2, DomainName=3)",
domainName, status) domainName, status)
// Consider domain or cloud domain as domain-joined isDomain := status == syscall.NetSetupDomainName
isDomain := status == NetSetupDomain || status == NetSetupCloudDomain Log(context.Background(), logger.Debug(), "Is domain joined? status=%d, result=%v", status, isDomain)
Log(context.Background(), logger.Debug(),
"Is domain joined? status=%d, traditional=%v, cloud=%v, result=%v",
status,
status == NetSetupDomain,
status == NetSetupCloudDomain,
isDomain)
return isDomain return isDomain
} }
+91
View File
@@ -282,6 +282,35 @@ func Test_Edns0_CacheReply(t *testing.T) {
} }
} }
// https://github.com/Control-D-Inc/ctrld/issues/255
func Test_legacyResolverWithBigExtraSection(t *testing.T) {
lanPC, err := net.ListenPacket("udp", "127.0.0.1:0") // 127.0.0.1 is considered LAN (loopback)
if err != nil {
t.Fatalf("failed to listen on LAN address: %v", err)
}
lanServer, lanAddr, err := runLocalPacketConnTestServer(t, lanPC, bigExtraSectionHandler())
if err != nil {
t.Fatalf("failed to run LAN test server: %v", err)
}
defer lanServer.Shutdown()
uc := &UpstreamConfig{
Name: "Legacy",
Type: ResolverTypeLegacy,
Endpoint: lanAddr,
}
uc.Init()
r, err := NewResolver(uc)
if err != nil {
t.Fatal(err)
}
_, err = r.Resolve(context.Background(), uc.VerifyMsg())
if err != nil {
t.Fatal(err)
}
}
func Test_upstreamTypeFromEndpoint(t *testing.T) { func Test_upstreamTypeFromEndpoint(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -370,6 +399,68 @@ func countHandler(call *atomic.Int64) dns.HandlerFunc {
} }
} }
func mustRR(s string) dns.RR {
r, err := dns.NewRR(s)
if err != nil {
panic(err)
}
return r
}
func bigExtraSectionHandler() dns.HandlerFunc {
return func(w dns.ResponseWriter, msg *dns.Msg) {
m := &dns.Msg{
Answer: []dns.RR{
mustRR(". 7149 IN NS m.root-servers.net."),
mustRR(". 7149 IN NS c.root-servers.net."),
mustRR(". 7149 IN NS e.root-servers.net."),
mustRR(". 7149 IN NS j.root-servers.net."),
mustRR(". 7149 IN NS g.root-servers.net."),
mustRR(". 7149 IN NS k.root-servers.net."),
mustRR(". 7149 IN NS l.root-servers.net."),
mustRR(". 7149 IN NS d.root-servers.net."),
mustRR(". 7149 IN NS h.root-servers.net."),
mustRR(". 7149 IN NS b.root-servers.net."),
mustRR(". 7149 IN NS a.root-servers.net."),
mustRR(". 7149 IN NS f.root-servers.net."),
mustRR(". 7149 IN NS i.root-servers.net."),
},
Extra: []dns.RR{
mustRR("m.root-servers.net. 656 IN A 202.12.27.33"),
mustRR("m.root-servers.net. 656 IN AAAA 2001:dc3::35"),
mustRR("c.root-servers.net. 656 IN A 192.33.4.12"),
mustRR("c.root-servers.net. 656 IN AAAA 2001:500:2::c"),
mustRR("e.root-servers.net. 656 IN A 192.203.230.10"),
mustRR("e.root-servers.net. 656 IN AAAA 2001:500:a8::e"),
mustRR("j.root-servers.net. 656 IN A 192.58.128.30"),
mustRR("j.root-servers.net. 656 IN AAAA 2001:503:c27::2:30"),
mustRR("g.root-servers.net. 656 IN A 192.112.36.4"),
mustRR("g.root-servers.net. 656 IN AAAA 2001:500:12::d0d"),
mustRR("k.root-servers.net. 656 IN A 193.0.14.129"),
mustRR("k.root-servers.net. 656 IN AAAA 2001:7fd::1"),
mustRR("l.root-servers.net. 656 IN A 199.7.83.42"),
mustRR("l.root-servers.net. 656 IN AAAA 2001:500:9f::42"),
mustRR("d.root-servers.net. 656 IN A 199.7.91.13"),
mustRR("d.root-servers.net. 656 IN AAAA 2001:500:2d::d"),
mustRR("h.root-servers.net. 656 IN A 198.97.190.53"),
mustRR("h.root-servers.net. 656 IN AAAA 2001:500:1::53"),
mustRR("b.root-servers.net. 656 IN A 170.247.170.2"),
mustRR("b.root-servers.net. 656 IN AAAA 2801:1b8:10::b"),
mustRR("a.root-servers.net. 656 IN A 198.41.0.4"),
mustRR("a.root-servers.net. 656 IN AAAA 2001:503:ba3e::2:30"),
mustRR("f.root-servers.net. 656 IN A 192.5.5.241"),
mustRR("f.root-servers.net. 656 IN AAAA 2001:500:2f::f"),
mustRR("i.root-servers.net. 656 IN A 192.36.148.17"),
mustRR("i.root-servers.net. 656 IN AAAA 2001:7fe::53"),
},
}
m.Compress = true
m.SetReply(msg)
w.WriteMsg(m)
}
}
func generateEdns0ClientCookie() string { func generateEdns0ClientCookie() string {
cookie := make([]byte, 8) cookie := make([]byte, 8)
if _, err := rand.Read(cookie); err != nil { if _, err := rand.Read(cookie); err != nil {