Merge pull request #233 from Control-D-Inc/release-branch-v1.4.3

[WIP] Release branch v1.4.3
This commit is contained in:
Cuong Manh Le
2025-04-28 17:08:34 +07:00
committed by GitHub
9 changed files with 131 additions and 84 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -10,7 +10,7 @@ import (
)
func dnsFns() []dnsFn {
return []dnsFn{dnsFromRIB}
return []dnsFn{dnsFromResolvConf, dnsFromRIB}
}
func dnsFromRIB() []string {

View File

@@ -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()

View File

@@ -17,7 +17,7 @@ const (
)
func dnsFns() []dnsFn {
return []dnsFn{dns4, dns6, dnsFromSystemdResolver}
return []dnsFn{dnsFromResolvConf, dns4, dns6, dnsFromSystemdResolver}
}
func dns4() []string {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {