From 62f73bcaa291ca26da402ccbeb62c76e6d1855af Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 8 May 2025 22:29:59 +0700 Subject: [PATCH] all: preserve search domains settings So bare hostname will be resolved as expected when ctrld is running. --- cmd/cli/os_darwin.go | 5 +++- cmd/cli/os_freebsd.go | 15 ++++++++-- cmd/cli/os_linux.go | 8 +++++- cmd/cli/os_windows.go | 4 +++ cmd/cli/search_domains_unix.go | 14 ++++++++++ cmd/cli/search_domains_windows.go | 43 +++++++++++++++++++++++++++++ internal/resolvconffile/dns.go | 12 +++++++- internal/resolvconffile/dns_test.go | 2 +- nameservers_unix.go | 4 +-- 9 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 cmd/cli/search_domains_unix.go create mode 100644 cmd/cli/search_domains_windows.go diff --git a/cmd/cli/os_darwin.go b/cmd/cli/os_darwin.go index 841be76..4c358b0 100644 --- a/cmd/cli/os_darwin.go +++ b/cmd/cli/os_darwin.go @@ -47,6 +47,9 @@ func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) e // networksetup -setdnsservers Wi-Fi 8.8.8.8 1.1.1.1 // TODO(cuonglm): use system API func setDNS(iface *net.Interface, nameservers []string) error { + // Note that networksetup won't modify search domains settings, + // This assignment is just a placeholder to silent linter. + _ = searchDomains cmd := "networksetup" args := []string{"-setdnsservers", iface.Name} args = append(args, nameservers...) @@ -88,7 +91,7 @@ func restoreDNS(iface *net.Interface) (err error) { } func currentDNS(_ *net.Interface) []string { - return resolvconffile.NameServers("") + return resolvconffile.NameServers() } // currentStaticDNS returns the current static DNS settings of given interface. diff --git a/cmd/cli/os_freebsd.go b/cmd/cli/os_freebsd.go index 72da485..d66e4bf 100644 --- a/cmd/cli/os_freebsd.go +++ b/cmd/cli/os_freebsd.go @@ -7,6 +7,7 @@ import ( "tailscale.com/control/controlknobs" "tailscale.com/health" + "tailscale.com/util/dnsname" "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/resolvconffile" @@ -50,7 +51,17 @@ func setDNS(iface *net.Interface, nameservers []string) error { ns = append(ns, netip.MustParseAddr(nameserver)) } - if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil { + osConfig := dns.OSConfig{ + Nameservers: ns, + SearchDomains: []dnsname.FQDN{}, + } + if sds, err := searchDomains(); err == nil { + osConfig.SearchDomains = sds + } else { + mainLog.Load().Debug().Err(err).Msg("failed to get search domains list") + } + + if err := r.SetDNS(osConfig); err != nil { mainLog.Load().Error().Err(err).Msg("failed to set DNS") return err } @@ -83,7 +94,7 @@ func restoreDNS(iface *net.Interface) (err error) { } func currentDNS(_ *net.Interface) []string { - return resolvconffile.NameServers("") + return resolvconffile.NameServers() } // currentStaticDNS returns the current static DNS settings of given interface. diff --git a/cmd/cli/os_linux.go b/cmd/cli/os_linux.go index e2302a3..8caad63 100644 --- a/cmd/cli/os_linux.go +++ b/cmd/cli/os_linux.go @@ -71,6 +71,11 @@ func setDNS(iface *net.Interface, nameservers []string) error { Nameservers: ns, SearchDomains: []dnsname.FQDN{}, } + if sds, err := searchDomains(); err == nil { + osConfig.SearchDomains = sds + } else { + mainLog.Load().Debug().Err(err).Msg("failed to get search domains list") + } trySystemdResolve := false if err := r.SetDNS(osConfig); err != nil { if strings.Contains(err.Error(), "Rejected send message") && @@ -196,7 +201,8 @@ func restoreDNS(iface *net.Interface) (err error) { } func currentDNS(iface *net.Interface) []string { - for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconffile.NameServers} { + resolvconfFunc := func(_ string) []string { return resolvconffile.NameServers() } + for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconfFunc} { if ns := fn(iface.Name); len(ns) > 0 { return ns } diff --git a/cmd/cli/os_windows.go b/cmd/cli/os_windows.go index e1bcd9a..7ebc54a 100644 --- a/cmd/cli/os_windows.go +++ b/cmd/cli/os_windows.go @@ -100,6 +100,10 @@ func setDNS(iface *net.Interface, nameservers []string) error { } } + // Note that Windows won't modify the current search domains if passing nil to luid.SetDNS function. + // searchDomains is still implemented for Windows just in case Windows API changes in future versions. + _ = searchDomains + if len(serversV4) == 0 && len(serversV6) == 0 { return errors.New("invalid DNS nameservers") } diff --git a/cmd/cli/search_domains_unix.go b/cmd/cli/search_domains_unix.go new file mode 100644 index 0000000..de3998e --- /dev/null +++ b/cmd/cli/search_domains_unix.go @@ -0,0 +1,14 @@ +//go:build unix + +package cli + +import ( + "tailscale.com/util/dnsname" + + "github.com/Control-D-Inc/ctrld/internal/resolvconffile" +) + +// searchDomains returns the current search domains config. +func searchDomains() ([]dnsname.FQDN, error) { + return resolvconffile.SearchDomains() +} diff --git a/cmd/cli/search_domains_windows.go b/cmd/cli/search_domains_windows.go new file mode 100644 index 0000000..320a322 --- /dev/null +++ b/cmd/cli/search_domains_windows.go @@ -0,0 +1,43 @@ +package cli + +import ( + "fmt" + "syscall" + + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" + "tailscale.com/util/dnsname" +) + +// searchDomains returns the current search domains config. +func searchDomains() ([]dnsname.FQDN, error) { + flags := winipcfg.GAAFlagIncludeGateways | + winipcfg.GAAFlagIncludePrefix + + aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, flags) + if err != nil { + return nil, fmt.Errorf("winipcfg.GetAdaptersAddresses: %w", err) + } + + var sds []dnsname.FQDN + for _, aa := range aas { + if aa.OperStatus != winipcfg.IfOperStatusUp { + continue + } + + // Skip if software loopback or other non-physical types + // This is to avoid the "Loopback Pseudo-Interface 1" issue we see on windows + if aa.IfType == winipcfg.IfTypeSoftwareLoopback { + continue + } + + for a := aa.FirstDNSSuffix; a != nil; a = a.Next { + d, err := dnsname.ToFQDN(a.String()) + if err != nil { + mainLog.Load().Debug().Err(err).Msgf("failed to parse domain: %s", a.String()) + continue + } + sds = append(sds, d) + } + } + return sds, nil +} diff --git a/internal/resolvconffile/dns.go b/internal/resolvconffile/dns.go index 3ce0f91..0d532eb 100644 --- a/internal/resolvconffile/dns.go +++ b/internal/resolvconffile/dns.go @@ -6,6 +6,7 @@ import ( "net" "tailscale.com/net/dns/resolvconffile" + "tailscale.com/util/dnsname" ) const resolvconfPath = "/etc/resolv.conf" @@ -22,7 +23,7 @@ func NameServersWithPort() []string { return ns } -func NameServers(_ string) []string { +func NameServers() []string { c, err := resolvconffile.ParseFile(resolvconfPath) if err != nil { return nil @@ -33,3 +34,12 @@ func NameServers(_ string) []string { } return ns } + +// SearchDomains returns the current search domains config in /etc/resolv.conf file. +func SearchDomains() ([]dnsname.FQDN, error) { + c, err := resolvconffile.ParseFile(resolvconfPath) + if err != nil { + return nil, err + } + return c.SearchDomains, nil +} diff --git a/internal/resolvconffile/dns_test.go b/internal/resolvconffile/dns_test.go index ba571af..7f7a64c 100644 --- a/internal/resolvconffile/dns_test.go +++ b/internal/resolvconffile/dns_test.go @@ -9,7 +9,7 @@ import ( ) func TestNameServers(t *testing.T) { - ns := NameServers("") + ns := NameServers() require.NotNil(t, ns) t.Log(ns) } diff --git a/nameservers_unix.go b/nameservers_unix.go index d7af521..d8e6035 100644 --- a/nameservers_unix.go +++ b/nameservers_unix.go @@ -14,7 +14,7 @@ import ( // currentNameserversFromResolvconf returns the current nameservers set from /etc/resolv.conf file. func currentNameserversFromResolvconf() []string { - return resolvconffile.NameServers("") + return resolvconffile.NameServers() } // dnsFromResolvConf reads usable nameservers from /etc/resolv.conf file. @@ -34,7 +34,7 @@ func dnsFromResolvConf() []string { time.Sleep(retryInterval) } - nss := resolvconffile.NameServers("") + nss := resolvconffile.NameServers() var localDNS []string seen := make(map[string]bool)