From f7fb555c8982defec1b5f61a4e9e1574b6e21c73 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 3 Jul 2025 15:25:16 +0700 Subject: [PATCH] Removing Windows Server support --- README.md | 3 +- cmd/cli/ad_others.go | 5 -- cmd/cli/cli.go | 45 +------------ cmd/cli/commands.go | 5 +- cmd/cli/dns_proxy.go | 26 -------- cmd/cli/os_windows.go | 115 -------------------------------- cmd/cli/os_windows_test.go | 8 +++ cmd/cli/prog.go | 32 --------- cmd/cli/prog_windows.go | 6 +- cmd/cli/service_others.go | 5 -- cmd/cli/service_windows.go | 81 ---------------------- cmd/cli/service_windows_test.go | 25 ------- config.go | 2 +- resolver.go | 18 ----- 14 files changed, 13 insertions(+), 363 deletions(-) delete mode 100644 cmd/cli/service_windows_test.go diff --git a/README.md b/README.md index 680ea36..f45b2f8 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,7 @@ All DNS protocols are supported, including: ## OS Support -- Windows (386, amd64, arm) -- Windows Server (386, amd64) +- Windows Desktop (386, amd64, arm) - MacOS (amd64, arm64) - Linux (386, amd64, arm, mips) - FreeBSD (386, amd64, arm) diff --git a/cmd/cli/ad_others.go b/cmd/cli/ad_others.go index b23476f..6a7417f 100644 --- a/cmd/cli/ad_others.go +++ b/cmd/cli/ad_others.go @@ -8,8 +8,3 @@ import ( // addExtraSplitDnsRule adds split DNS rule if present. func addExtraSplitDnsRule(_ *ctrld.Config) bool { return false } - -// getActiveDirectoryDomain returns AD domain name of this computer. -func getActiveDirectoryDomain() (string, error) { - return "", nil -} diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index c884f18..30fdba5 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1147,20 +1147,15 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( lcc := make(map[string]*listenerConfigCheck) cdMode := cdUID != "" nextdnsMode := nextdns != "" - // For Windows server with local Dns server running, we can only try on random local IP. - hasLocalDnsServer := hasLocalDnsServerRunning() isDesktop := ctrld.IsDesktopPlatform() for n, listener := range cfg.Listener { lcc[n] = &listenerConfigCheck{} if listener.IP == "" { listener.IP = "0.0.0.0" - // Windows Server lies to us that we could listen on 0.0.0.0:53 - // even there's a process already done that, stick to local IP only. - // // For desktop clients, also stick the listener to the local IP only. // Listening on 0.0.0.0 would expose it to the entire local network, potentially // creating security vulnerabilities (such as DNS amplification or abusing). - if hasLocalDnsServer || isDesktop { + if isDesktop { listener.IP = "127.0.0.1" } lcc[n].IP = true @@ -1171,15 +1166,9 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( } // In cd mode, we always try to pick an ip:port pair to work. // Same if nextdns resolver is used. - // - // Except on Windows Server with local Dns running, - // we could only listen on random local IP port 53. if cdMode || nextdnsMode { lcc[n].IP = true lcc[n].Port = true - if hasLocalDnsServer { - lcc[n].Port = false - } } updated = updated || lcc[n].IP || lcc[n].Port } @@ -1258,16 +1247,6 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( // config, so we can always listen on localhost port 53, but no traffic could be routed there. tryLocalhost := !isLoopback(listener.IP) tryAllPort53 := true - // We should not try to listen on any port other than 53, - // if we do, this will break the dns resolution for the system. - // TODO: cleanup these codes when refactoring this function. - tryOldIPPort5354 := false - tryPort5354 := false - if hasLocalDnsServer { - tryAllPort53 = false - tryOldIPPort5354 = false - tryPort5354 = false - } attempts := 0 maxAttempts := 10 @@ -1318,28 +1297,6 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( } continue } - if tryOldIPPort5354 { - tryOldIPPort5354 = false - if check.IP { - listener.IP = oldIP - } - if check.Port { - listener.Port = 5354 - } - logMsg(il.Info(), n, "could not listen on address: %s, trying current ip with port 5354", addr) - continue - } - if tryPort5354 { - tryPort5354 = false - if check.IP { - listener.IP = "0.0.0.0" - } - if check.Port { - listener.Port = 5354 - } - logMsg(il.Info(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr) - continue - } if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port. listener.IP = randomLocalIP() } else { diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index 31ca495..4978236 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -899,10 +899,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, } return nil }) - // Windows forwarders file. - if hasLocalDnsServerRunning() { - files = append(files, ctrld.AbsHomeDir(windowsForwardersFilename)) - } + // Binary itself. bin, _ := os.Executable() if bin != "" && supportedSelfDelete { diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 1c6d39d..030cc02 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -51,12 +51,6 @@ var privateUpstreamConfig = &ctrld.UpstreamConfig{ Timeout: 2000, } -var localUpstreamConfig = &ctrld.UpstreamConfig{ - Name: "Local resolver", - Type: ctrld.ResolverTypeLocal, - Timeout: 2000, -} - // proxyRequest contains data for proxying a DNS query to upstream. type proxyRequest struct { msg *dns.Msg @@ -500,17 +494,6 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { if len(upstreamConfigs) == 0 { upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreams = []string{upstreamOS} - // For OS resolver, local addresses are ignored to prevent possible looping. - // However, on Active Directory Domain Controller, where it has local DNS server - // running and listening on local addresses, these local addresses must be used - // as nameservers, so queries for ADDC could be resolved as expected. - if p.isAdDomainQuery(req.msg) { - ctrld.Log(ctx, p.Debug(), - "AD domain query detected for %s in domain %s", - req.msg.Question[0].Name, p.adDomain) - upstreamConfigs = []*ctrld.UpstreamConfig{localUpstreamConfig} - upstreams = []string{upstreamOSLocal} - } } res := &proxyResponse{} @@ -631,7 +614,6 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { logger := p.Debug(). Str("upstream", upstreamConfig.String()). Str("query", req.msg.Question[0].Name). - Bool("is_ad_query", p.isAdDomainQuery(req.msg)). Bool("is_lan_query", isLanOrPtrQuery) if p.isLoop(upstreamConfig) { @@ -747,14 +729,6 @@ func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.U return upstreamConfigs } -func (p *prog) isAdDomainQuery(msg *dns.Msg) bool { - if p.adDomain == "" { - return false - } - cDomainName := canonicalName(msg.Question[0].Name) - return dns.IsSubDomain(p.adDomain, cDomainName) -} - // canonicalName returns canonical name from FQDN with "." trimmed. func canonicalName(fqdn string) string { q := strings.TrimSpace(fqdn) diff --git a/cmd/cli/os_windows.go b/cmd/cli/os_windows.go index c0cd787..6311338 100644 --- a/cmd/cli/os_windows.go +++ b/cmd/cli/os_windows.go @@ -1,16 +1,12 @@ package cli import ( - "bytes" "errors" "fmt" "net" "net/netip" - "os" - "os/exec" "slices" "strings" - "sync" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" @@ -25,11 +21,6 @@ const ( v6InterfaceKeyPathFormat = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\` ) -var ( - setDNSOnce sync.Once - resetDNSOnce sync.Once -) - // setDnsIgnoreUnusableInterface likes setDNS, but return a nil error if the interface is not usable. func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) error { return setDNS(iface, nameservers) @@ -40,49 +31,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { if len(nameservers) == 0 { return errors.New("empty DNS nameservers") } - setDNSOnce.Do(func() { - // If there's a Dns server running, that means we are on AD with Dns feature enabled. - // Configuring the Dns server to forward queries to ctrld instead. - if hasLocalDnsServerRunning() { - mainLog.Load().Debug().Msg("Local DNS server detected, configuring forwarders") - file := ctrld.AbsHomeDir(windowsForwardersFilename) - mainLog.Load().Debug().Msgf("Using forwarders file: %s", file) - - oldForwardersContent, err := os.ReadFile(file) - if err != nil { - mainLog.Load().Debug().Err(err).Msg("Could not read existing forwarders file") - } else { - mainLog.Load().Debug().Msgf("Existing forwarders content: %s", string(oldForwardersContent)) - } - - hasLocalIPv6Listener := needLocalIPv6Listener() - mainLog.Load().Debug().Bool("has_ipv6_listener", hasLocalIPv6Listener).Msg("IPv6 listener status") - - forwarders := slices.DeleteFunc(slices.Clone(nameservers), func(s string) bool { - if !hasLocalIPv6Listener { - return false - } - return s == "::1" - }) - mainLog.Load().Debug().Strs("forwarders", forwarders).Msg("Filtered forwarders list") - - if err := os.WriteFile(file, []byte(strings.Join(forwarders, ",")), 0600); err != nil { - mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings") - } else { - mainLog.Load().Debug().Msg("Successfully wrote new forwarders file") - } - - oldForwarders := strings.Split(string(oldForwardersContent), ",") - mainLog.Load().Debug().Strs("old_forwarders", oldForwarders).Msg("Previous forwarders") - - if err := addDnsServerForwarders(forwarders, oldForwarders); err != nil { - mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings") - } else { - mainLog.Load().Debug().Msg("Successfully configured DNS server forwarders") - } - } - }) luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) if err != nil { return fmt.Errorf("setDNS: %w", err) @@ -126,25 +75,7 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error { return resetDNS(iface) } -// TODO(cuonglm): should we use system API? func resetDNS(iface *net.Interface) error { - resetDNSOnce.Do(func() { - // See corresponding comment in setDNS. - if hasLocalDnsServerRunning() { - file := ctrld.AbsHomeDir(windowsForwardersFilename) - content, err := os.ReadFile(file) - if err != nil { - mainLog.Load().Error().Err(err).Msg("could not read forwarders settings") - return - } - nameservers := strings.Split(string(content), ",") - if err := removeDnsServerForwarders(nameservers); err != nil { - mainLog.Load().Error().Err(err).Msg("could not remove forwarders settings") - return - } - } - }) - luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) if err != nil { return fmt.Errorf("resetDNS: %w", err) @@ -285,49 +216,3 @@ func parseDNSServers(val string) []string { } return servers } - -// addDnsServerForwarders adds given nameservers to DNS server forwarders list, -// and also removing old forwarders if provided. -func addDnsServerForwarders(nameservers, old []string) error { - newForwardersMap := make(map[string]struct{}) - newForwarders := make([]string, len(nameservers)) - for i := range nameservers { - newForwardersMap[nameservers[i]] = struct{}{} - newForwarders[i] = fmt.Sprintf("%q", nameservers[i]) - } - oldForwarders := old[:0] - for _, fwd := range old { - if _, ok := newForwardersMap[fwd]; !ok { - oldForwarders = append(oldForwarders, fwd) - } - } - // NOTE: It is important to add new forwarder before removing old one. - // Testing on Windows Server 2022 shows that removing forwarder1 - // then adding forwarder2 sometimes ends up adding both of them - // to the forwarders list. - cmd := fmt.Sprintf("Add-DnsServerForwarder -IPAddress %s", strings.Join(newForwarders, ",")) - if len(oldForwarders) > 0 { - cmd = fmt.Sprintf("%s ; Remove-DnsServerForwarder -IPAddress %s -Force", cmd, strings.Join(oldForwarders, ",")) - } - if out, err := powershell(cmd); err != nil { - return fmt.Errorf("%w: %s", err, string(out)) - } - return nil -} - -// removeDnsServerForwarders removes given nameservers from DNS server forwarders list. -func removeDnsServerForwarders(nameservers []string) error { - for _, ns := range nameservers { - cmd := fmt.Sprintf("Remove-DnsServerForwarder -IPAddress %s -Force", ns) - if out, err := powershell(cmd); err != nil { - return fmt.Errorf("%w: %s", err, string(out)) - } - } - return nil -} - -// powershell runs the given powershell command. -func powershell(cmd string) ([]byte, error) { - out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput() - return bytes.TrimSpace(out), err -} diff --git a/cmd/cli/os_windows_test.go b/cmd/cli/os_windows_test.go index 40be5ed..054b77c 100644 --- a/cmd/cli/os_windows_test.go +++ b/cmd/cli/os_windows_test.go @@ -1,8 +1,10 @@ package cli import ( + "bytes" "fmt" "net" + "os/exec" "slices" "strings" "testing" @@ -66,3 +68,9 @@ func currentStaticDnsPowershell(iface *net.Interface) ([]string, error) { } return ns, nil } + +// powershell runs the given powershell command. +func powershell(cmd string) ([]byte, error) { + out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput() + return bytes.TrimSpace(out), err +} diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 4b0fe97..616f9d4 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -128,8 +128,6 @@ type prog struct { internalLogSent time.Time runningIface string requiredMultiNICsConfig bool - adDomain string - runningOnDomainController bool selfUninstallMu sync.Mutex refusedQueryCount int @@ -279,11 +277,6 @@ func (p *prog) preRun() { func (p *prog) postRun() { if !service.Interactive() { - if runtime.GOOS == "windows" { - isDC, roleInt := isRunningOnDomainController() - p.runningOnDomainController = isDC - p.Debug().Msgf("running on domain controller: %t, role: %d", p.runningOnDomainController, roleInt) - } p.resetDNS(false, false) ns := ctrld.InitializeOsResolver(ctrld.LoggerCtx(context.Background(), p.logger.Load()), false) p.Debug().Msgf("initialized OS resolver with nameservers: %v", ns) @@ -481,10 +474,6 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) { } } } - if domain, err := getActiveDirectoryDomain(); err == nil && domain != "" && hasLocalDnsServerRunning() { - p.Debug().Msgf("active directory domain: %s", domain) - p.adDomain = domain - } var wg sync.WaitGroup wg.Add(len(p.cfg.Listener)) @@ -1481,26 +1470,5 @@ func (p *prog) leakOnUpstreamFailure() bool { if ptr := p.cfg.Service.LeakOnUpstreamFailure; ptr != nil { return *ptr } - - // if we are running on ADDC, we should not leak on upstream failure - if p.runningOnDomainController { - return false - } return true } - -// Domain controller role values from Win32_ComputerSystem -// https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-computersystem -const ( - BackupDomainController = 4 - PrimaryDomainController = 5 -) - -// isRunningOnDomainController checks if the current machine is a domain controller -// by querying the DomainRole property from Win32_ComputerSystem via WMI. -func isRunningOnDomainController() (bool, int) { - if runtime.GOOS != "windows" { - return false, 0 - } - return isRunningOnDomainControllerWindows() -} diff --git a/cmd/cli/prog_windows.go b/cmd/cli/prog_windows.go index e448625..35407a2 100644 --- a/cmd/cli/prog_windows.go +++ b/cmd/cli/prog_windows.go @@ -2,11 +2,7 @@ package cli import "github.com/kardianos/service" -func setDependencies(svc *service.Config) { - if hasLocalDnsServerRunning() { - svc.Dependencies = []string{"DNS"} - } -} +func setDependencies(svc *service.Config) {} func setWorkingDirectory(svc *service.Config, dir string) { // WorkingDirectory is not supported on Windows. diff --git a/cmd/cli/service_others.go b/cmd/cli/service_others.go index 954b228..0fe8ad9 100644 --- a/cmd/cli/service_others.go +++ b/cmd/cli/service_others.go @@ -14,9 +14,4 @@ func openLogFile(path string, flags int) (*os.File, error) { return os.OpenFile(path, flags, os.FileMode(0o600)) } -// hasLocalDnsServerRunning reports whether we are on Windows and having Dns server running. -func hasLocalDnsServerRunning() bool { return false } - func ConfigureWindowsServiceFailureActions(serviceName string) error { return nil } - -func isRunningOnDomainControllerWindows() (bool, int) { return false, 0 } diff --git a/cmd/cli/service_windows.go b/cmd/cli/service_windows.go index fddb0ef..fd185a1 100644 --- a/cmd/cli/service_windows.go +++ b/cmd/cli/service_windows.go @@ -2,18 +2,11 @@ package cli import ( "os" - "reflect" "runtime" - "strconv" - "strings" "syscall" "time" "unsafe" - "github.com/microsoft/wmi/pkg/base/host" - "github.com/microsoft/wmi/pkg/base/instance" - "github.com/microsoft/wmi/pkg/base/query" - "github.com/microsoft/wmi/pkg/constant" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc/mgr" ) @@ -151,77 +144,3 @@ func openLogFile(path string, mode int) (*os.File, error) { return os.NewFile(uintptr(handle), path), nil } - -const processEntrySize = uint32(unsafe.Sizeof(windows.ProcessEntry32{})) - -// hasLocalDnsServerRunning reports whether we are on Windows and having Dns server running. -func hasLocalDnsServerRunning() bool { - h, e := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) - if e != nil { - return false - } - p := windows.ProcessEntry32{Size: processEntrySize} - for { - e := windows.Process32Next(h, &p) - if e != nil { - return false - } - if strings.ToLower(windows.UTF16ToString(p.ExeFile[:])) == "dns.exe" { - return true - } - } -} - -func isRunningOnDomainControllerWindows() (bool, int) { - whost := host.NewWmiLocalHost() - q := query.NewWmiQuery("Win32_ComputerSystem") - instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.CimV2), q) - if err != nil { - mainLog.Load().Debug().Err(err).Msg("WMI query failed") - return false, 0 - } - if instances == nil { - mainLog.Load().Debug().Msg("WMI query returned nil instances") - return false, 0 - } - defer instances.Close() - - if len(instances) == 0 { - mainLog.Load().Debug().Msg("no rows returned from Win32_ComputerSystem") - return false, 0 - } - - val, err := instances[0].GetProperty("DomainRole") - if err != nil { - mainLog.Load().Debug().Err(err).Msg("failed to get DomainRole property") - return false, 0 - } - if val == nil { - mainLog.Load().Debug().Msg("DomainRole property is nil") - return false, 0 - } - - // Safely handle varied types: string or integer - var roleInt int - switch v := val.(type) { - case string: - // "4", "5", etc. - parsed, parseErr := strconv.Atoi(v) - if parseErr != nil { - mainLog.Load().Debug().Err(parseErr).Msgf("failed to parse DomainRole value %q", v) - return false, 0 - } - roleInt = parsed - case int8, int16, int32, int64: - roleInt = int(reflect.ValueOf(v).Int()) - case uint8, uint16, uint32, uint64: - roleInt = int(reflect.ValueOf(v).Uint()) - default: - mainLog.Load().Debug().Msgf("unexpected DomainRole type: %T value=%v", v, v) - return false, 0 - } - - // Check if role indicates a domain controller - isDC := roleInt == BackupDomainController || roleInt == PrimaryDomainController - return isDC, roleInt -} diff --git a/cmd/cli/service_windows_test.go b/cmd/cli/service_windows_test.go deleted file mode 100644 index 67c2725..0000000 --- a/cmd/cli/service_windows_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package cli - -import ( - "testing" - "time" -) - -func Test_hasLocalDnsServerRunning(t *testing.T) { - start := time.Now() - hasDns := hasLocalDnsServerRunning() - t.Logf("Using Windows API takes: %d", time.Since(start).Milliseconds()) - - start = time.Now() - hasDnsPowershell := hasLocalDnsServerRunningPowershell() - t.Logf("Using Powershell takes: %d", time.Since(start).Milliseconds()) - - if hasDns != hasDnsPowershell { - t.Fatalf("result mismatch, want: %v, got: %v", hasDnsPowershell, hasDns) - } -} - -func hasLocalDnsServerRunningPowershell() bool { - _, err := powershell("Get-Process -Name DNS") - return err == nil -} diff --git a/config.go b/config.go index 4aadff1..14dd76c 100644 --- a/config.go +++ b/config.go @@ -403,7 +403,7 @@ func (uc *UpstreamConfig) IsDiscoverable() bool { return *uc.Discoverable } switch uc.Type { - case ResolverTypeOS, ResolverTypeLegacy, ResolverTypePrivate, ResolverTypeLocal: + case ResolverTypeOS, ResolverTypeLegacy, ResolverTypePrivate: if ip, err := netip.ParseAddr(uc.Domain); err == nil { return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || tsaddr.CGNATRange().Contains(ip) } diff --git a/resolver.go b/resolver.go index 70c859f..1c4bf28 100644 --- a/resolver.go +++ b/resolver.go @@ -34,8 +34,6 @@ const ( ResolverTypeLegacy = "legacy" // ResolverTypePrivate is like ResolverTypeOS, but use for private resolver only. ResolverTypePrivate = "private" - // ResolverTypeLocal is like ResolverTypeOS, but use for local resolver only. - ResolverTypeLocal = "local" // ResolverTypeSDNS specifies resolver with information encoded using DNS Stamps. // See: https://dnscrypt.info/stamps-specifications/ ResolverTypeSDNS = "sdns" @@ -45,12 +43,6 @@ const controldPublicDns = "76.76.2.0" var controldPublicDnsWithPort = net.JoinHostPort(controldPublicDns, "53") -var localResolver Resolver - -func init() { - localResolver = newLocalResolver() -} - var ( resolverMutex sync.Mutex or *osResolver @@ -58,14 +50,6 @@ var ( defaultLocalIPv6 atomic.Value // holds net.IP (IPv6) ) -func newLocalResolver() Resolver { - var nss []string - for _, addr := range Rfc1918Addresses() { - nss = append(nss, net.JoinHostPort(addr, "53")) - } - return NewResolverWithNameserver(nss) -} - // LanQueryCtxKey is the context.Context key to indicate that the request is for LAN network. type LanQueryCtxKey struct{} @@ -198,8 +182,6 @@ func NewResolver(ctx context.Context, uc *UpstreamConfig) (Resolver, error) { return &legacyResolver{uc: uc}, nil case ResolverTypePrivate: return NewPrivateResolver(ctx), nil - case ResolverTypeLocal: - return localResolver, nil } return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ) }