From 7dfb77228f0924d87265562bbef22788070d37ee Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 8 Apr 2025 22:23:23 +0700 Subject: [PATCH 1/5] cmd/cli: handle ipc warning message more precisely If the socket file does not exist, it means that "ctrld start" was never run. In this case, the warning message should not be printed to avoid needless confusion. --- cmd/cli/cli.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index ecdd113..31e1fcb 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -227,7 +227,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { consoleWriter.Out = io.MultiWriter(os.Stdout, lc) p.logConn = lc } else { - mainLog.Load().Warn().Err(err).Msgf("unable to create log ipc connection") + if !errors.Is(err, os.ErrNotExist) { + mainLog.Load().Warn().Err(err).Msg("unable to create log ipc connection") + } } } else { mainLog.Load().Warn().Err(err).Msgf("unable to resolve socket address: %s", sockPath) From 8d6ea91f35ef649e4b69989a03af90cb3f6ddd96 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 14 Apr 2025 16:51:38 +0700 Subject: [PATCH 2/5] Allowing bootstrap IPs for ControlD sub-domains So protocol which uses sub-domain like doq/dot could be bootstrap in case of no DNS available. --- config.go | 12 +++++------ config_internal_test.go | 45 +++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/config.go b/config.go index f208f0d..626fc32 100644 --- a/config.go +++ b/config.go @@ -446,7 +446,7 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { uc.bootstrapIPs = uc.bootstrapIPs[:n] if len(uc.bootstrapIPs) == 0 { uc.bootstrapIPs = bootstrapIPsFromControlDDomain(uc.Domain) - ProxyLogger.Load().Warn().Msgf("no bootstrap IPs found for %q, fallback to direct IPs", uc.Domain) + ProxyLogger.Load().Warn().Msgf("no record found for %q, lookup from direct IP table", uc.Domain) } } if len(uc.bootstrapIPs) > 0 { @@ -951,14 +951,14 @@ func (uc *UpstreamConfig) String() string { // bootstrapIPsFromControlDDomain returns bootstrap IPs for ControlD domain. func bootstrapIPsFromControlDDomain(domain string) []string { - switch domain { - case PremiumDnsDomain: + switch { + case dns.IsSubDomain(PremiumDnsDomain, domain): return []string{PremiumDNSBoostrapIP, PremiumDNSBoostrapIPv6} - case FreeDnsDomain: + case dns.IsSubDomain(FreeDnsDomain, domain): return []string{FreeDNSBoostrapIP, FreeDNSBoostrapIPv6} - case premiumDnsDomainDev: + case dns.IsSubDomain(premiumDnsDomainDev, domain): return []string{premiumDNSBoostrapIP, premiumDNSBoostrapIPv6} - case freeDnsDomainDev: + case dns.IsSubDomain(freeDnsDomainDev, domain): return []string{freeDNSBoostrapIP, freeDNSBoostrapIPv6} } return nil diff --git a/config_internal_test.go b/config_internal_test.go index 7695eb5..3d1f0b7 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -8,19 +8,42 @@ import ( ) func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { - uc := &UpstreamConfig{ - Name: "test", - Type: ResolverTypeDOH, - Endpoint: "https://freedns.controld.com/p2", - Timeout: 5000, + tests := []struct { + name string + uc *UpstreamConfig + }{ + { + name: "doh/doh3", + uc: &UpstreamConfig{ + Name: "doh", + Type: ResolverTypeDOH, + Endpoint: "https://freedns.controld.com/p2", + Timeout: 5000, + }, + }, + { + name: "doq/dot", + uc: &UpstreamConfig{ + Name: "dot", + Type: ResolverTypeDOT, + Endpoint: "p2.freedns.controld.com", + Timeout: 5000, + }, + }, } - uc.Init() - uc.SetupBootstrapIP() - if len(uc.bootstrapIPs) == 0 { - t.Log(defaultNameservers()) - t.Fatal("could not bootstrap ip without bootstrap DNS") + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.uc.Init() + tc.uc.SetupBootstrapIP() + if len(tc.uc.bootstrapIPs) == 0 { + t.Log(defaultNameservers()) + t.Fatalf("could not bootstrap ip: %s", tc.uc.String()) + } + }) } - t.Log(uc) + } func TestUpstreamConfig_Init(t *testing.T) { From 0c2cc00c4f09d43c1d4884b24bbfe42ed6b2ac56 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 14 Apr 2025 22:58:04 +0700 Subject: [PATCH 3/5] Using ControlD bootstrap DNS again So on system where there's no available DNS, non-ControlD upstreams could be bootstrapped like before. While at it, also improving lookupIP to not initializing OS resolver anymore, removing the un-necessary contention for accquiring/releasing OS resolver mutex. --- config.go | 14 +++++++++++++- resolver.go | 23 +++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/config.go b/config.go index 626fc32..fdea19f 100644 --- a/config.go +++ b/config.go @@ -427,11 +427,18 @@ func (uc *UpstreamConfig) UID() string { // SetupBootstrapIP manually find all available IPs of the upstream. // The first usable IP will be used as bootstrap IP of the upstream. +// The upstream domain will be looked up using following orders: +// +// - Current system DNS settings. +// - Direct IPs table for ControlD upstreams. +// - ControlD Bootstrap DNS 76.76.2.22 +// +// The setup process will block until there's usable IPs found. func (uc *UpstreamConfig) SetupBootstrapIP() { b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 10*time.Second) isControlD := uc.IsControlD() for { - uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout) + uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, defaultNameservers()) // For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses, // filtering them out here to prevent weird behavior. if isControlD { @@ -449,6 +456,11 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { ProxyLogger.Load().Warn().Msgf("no record found for %q, lookup from direct IP table", uc.Domain) } } + if len(uc.bootstrapIPs) == 0 { + ProxyLogger.Load().Warn().Msgf("no record found for %q, using bootstrap server: %s", uc.Domain, PremiumDNSBoostrapIP) + uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, []string{net.JoinHostPort(PremiumDNSBoostrapIP, "53")}) + + } if len(uc.bootstrapIPs) > 0 { break } diff --git a/resolver.go b/resolver.go index 52a17fc..401a7f9 100644 --- a/resolver.go +++ b/resolver.go @@ -466,27 +466,26 @@ func (d dummyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, err return ans, nil } -// LookupIP looks up host using OS resolver. +// LookupIP looks up domain using current system nameservers settings. // It returns a slice of that host's IPv4 and IPv6 addresses. func LookupIP(domain string) []string { - return lookupIP(domain, -1) + return lookupIP(domain, -1, defaultNameservers()) } -func lookupIP(domain string, timeout int) (ips []string) { +// lookupIP looks up domain with given timeout and bootstrapDNS. +// If timeout is negative, default timeout 2000 ms will be used. +// It returns nil if bootstrapDNS is nil or empty. +func lookupIP(domain string, timeout int, bootstrapDNS []string) (ips []string) { if net.ParseIP(domain) != nil { return []string{domain} } - resolverMutex.Lock() - if or == nil { - ProxyLogger.Load().Debug().Msgf("Initialize OS resolver in lookupIP") - or = newResolverWithNameserver(defaultNameservers()) + if bootstrapDNS == nil { + ProxyLogger.Load().Debug().Msgf("empty bootstrap DNS") + return nil } - nss := *or.lanServers.Load() - nss = append(nss, *or.publicServers.Load()...) - resolverMutex.Unlock() - resolver := newResolverWithNameserver(nss) - ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, nss) + resolver := newResolverWithNameserver(bootstrapDNS) + ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, bootstrapDNS) timeoutMs := 2000 if timeout > 0 && timeout < timeoutMs { timeoutMs = timeout From c06c8aa8597cb6d2b1887275bd4c47dd0d4d6e0e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 15 Apr 2025 18:51:56 +0700 Subject: [PATCH 4/5] Unifying DNS from /etc/resolv.conf function As part of v1.4.0 release, reading DNS from /etc/resolv.conf file is only available for Macos. However, there's no reason to prevent this function from working on other *nix systems. This commit unify the function to *nix, so it could be added as DNS source for Linux and Freebsd. --- nameservers_bsd.go | 2 +- nameservers_darwin.go | 46 -------------------------------- nameservers_linux.go | 2 +- nameservers_unix.go | 59 ++++++++++++++++++++++++++++++++++++++++-- nameservers_windows.go | 5 ++-- resolver.go | 2 +- 6 files changed, 63 insertions(+), 53 deletions(-) diff --git a/nameservers_bsd.go b/nameservers_bsd.go index b835060..09c9516 100644 --- a/nameservers_bsd.go +++ b/nameservers_bsd.go @@ -10,7 +10,7 @@ import ( ) func dnsFns() []dnsFn { - return []dnsFn{dnsFromRIB} + return []dnsFn{dnsFromResolvConf, dnsFromRIB} } func dnsFromRIB() []string { diff --git a/nameservers_darwin.go b/nameservers_darwin.go index b6b1543..1bf4574 100644 --- a/nameservers_darwin.go +++ b/nameservers_darwin.go @@ -16,58 +16,12 @@ import ( "time" "tailscale.com/net/netmon" - - "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) func dnsFns() []dnsFn { return []dnsFn{dnsFromResolvConf, getDNSFromScutil, getAllDHCPNameservers} } -// dnsFromResolvConf reads nameservers from /etc/resolv.conf -func dnsFromResolvConf() []string { - const ( - maxRetries = 10 - retryInterval = 100 * time.Millisecond - ) - - regularIPs, loopbackIPs, _ := netmon.LocalAddresses() - - var dns []string - for attempt := 0; attempt < maxRetries; attempt++ { - if attempt > 0 { - time.Sleep(retryInterval) - } - - nss := resolvconffile.NameServers("") - var localDNS []string - seen := make(map[string]bool) - - for _, ns := range nss { - if ip := net.ParseIP(ns); ip != nil { - // skip loopback IPs - for _, v := range slices.Concat(regularIPs, loopbackIPs) { - ipStr := v.String() - if ip.String() == ipStr { - continue - } - } - if !seen[ip.String()] { - seen[ip.String()] = true - localDNS = append(localDNS, ip.String()) - } - } - } - - // If we successfully read the file and found nameservers, return them - if len(localDNS) > 0 { - return localDNS - } - } - - return dns -} - func getDNSFromScutil() []string { logger := *ProxyLogger.Load() diff --git a/nameservers_linux.go b/nameservers_linux.go index 1fad95b..13a5507 100644 --- a/nameservers_linux.go +++ b/nameservers_linux.go @@ -17,7 +17,7 @@ const ( ) func dnsFns() []dnsFn { - return []dnsFn{dns4, dns6, dnsFromSystemdResolver} + return []dnsFn{dnsFromResolvConf, dns4, dns6, dnsFromSystemdResolver} } func dns4() []string { diff --git a/nameservers_unix.go b/nameservers_unix.go index 39cc971..d7af521 100644 --- a/nameservers_unix.go +++ b/nameservers_unix.go @@ -2,8 +2,63 @@ package ctrld -import "github.com/Control-D-Inc/ctrld/internal/resolvconffile" +import ( + "net" + "slices" + "time" -func nameserversFromResolvconf() []string { + "tailscale.com/net/netmon" + + "github.com/Control-D-Inc/ctrld/internal/resolvconffile" +) + +// currentNameserversFromResolvconf returns the current nameservers set from /etc/resolv.conf file. +func currentNameserversFromResolvconf() []string { return resolvconffile.NameServers("") } + +// dnsFromResolvConf reads usable nameservers from /etc/resolv.conf file. +// A nameserver is usable if it's not one of current machine's IP addresses +// and loopback IP addresses. +func dnsFromResolvConf() []string { + const ( + maxRetries = 10 + retryInterval = 100 * time.Millisecond + ) + + regularIPs, loopbackIPs, _ := netmon.LocalAddresses() + + var dns []string + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + time.Sleep(retryInterval) + } + + nss := resolvconffile.NameServers("") + var localDNS []string + seen := make(map[string]bool) + + for _, ns := range nss { + if ip := net.ParseIP(ns); ip != nil { + // skip loopback IPs + for _, v := range slices.Concat(regularIPs, loopbackIPs) { + ipStr := v.String() + if ip.String() == ipStr { + continue + } + } + if !seen[ip.String()] { + seen[ip.String()] = true + localDNS = append(localDNS, ip.String()) + } + } + } + + // If we successfully read the file and found nameservers, return them + if len(localDNS) > 0 { + return localDNS + } + } + + return dns +} diff --git a/nameservers_windows.go b/nameservers_windows.go index 0c47e58..eb4f2b5 100644 --- a/nameservers_windows.go +++ b/nameservers_windows.go @@ -158,7 +158,7 @@ func getDNSServers(ctx context.Context) ([]string, error) { 0, // DomainGuid - not needed 0, // SiteName - not needed uintptr(flags), // Flags - uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output + uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output if ret != 0 { switch ret { @@ -330,7 +330,8 @@ func getDNSServers(ctx context.Context) ([]string, error) { return ns, nil } -func nameserversFromResolvconf() []string { +// currentNameserversFromResolvconf returns a nil slice of strings. +func currentNameserversFromResolvconf() []string { return nil } diff --git a/resolver.go b/resolver.go index 401a7f9..a44ddb2 100644 --- a/resolver.go +++ b/resolver.go @@ -584,7 +584,7 @@ func NewPrivateResolver() Resolver { } nss := *or.lanServers.Load() resolverMutex.Unlock() - resolveConfNss := nameserversFromResolvconf() + resolveConfNss := currentNameserversFromResolvconf() localRfc1918Addrs := Rfc1918Addresses() n := 0 for _, ns := range nss { From d1ea1ba08c9996489ace12839193a1cb158882d8 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 17 Apr 2025 18:47:24 +0700 Subject: [PATCH 5/5] Disable parallel test for TestUpstreamConfig_SetupBootstrapIP There's a bug in wmi library which causes race condition when getting wmi instance manager concurrently. The new tests for setup bootstrap ip concurrently thus failed unexpectedly. There's going to be a fix sent to the upstream, in the meantime, disable the parallel test temporary. See: https://github.com/microsoft/wmi/issues/165 --- config_internal_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_internal_test.go b/config_internal_test.go index 3d1f0b7..b37e982 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -34,7 +34,8 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() + // Enable parallel tests once https://github.com/microsoft/wmi/issues/165 fixed. + // t.Parallel() tc.uc.Init() tc.uc.SetupBootstrapIP() if len(tc.uc.bootstrapIPs) == 0 {