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) diff --git a/config.go b/config.go index f208f0d..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 { @@ -446,9 +453,14 @@ 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 { + 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 } @@ -951,14 +963,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..b37e982 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -8,19 +8,43 @@ 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) { + // 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 { + t.Log(defaultNameservers()) + t.Fatalf("could not bootstrap ip: %s", tc.uc.String()) + } + }) } - t.Log(uc) + } func TestUpstreamConfig_Init(t *testing.T) { 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 52a17fc..a44ddb2 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 @@ -585,7 +584,7 @@ func NewPrivateResolver() Resolver { } nss := *or.lanServers.Load() resolverMutex.Unlock() - resolveConfNss := nameserversFromResolvconf() + resolveConfNss := currentNameserversFromResolvconf() localRfc1918Addrs := Rfc1918Addresses() n := 0 for _, ns := range nss {