From ef1a39cb01d1231d76e503870360479f7addb957 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Thu, 18 Jul 2024 08:39:41 -0600 Subject: [PATCH] Refactor macOS system DNS configuration (#2284) On macOS use the recommended settings for providing split DNS. As per the docs an empty string will force the configuration to be the default. In order to to support split DNS an additional service config is added for the local server and search domain settings. see: https://developer.apple.com/documentation/devicemanagement/vpn/dns --- client/internal/dns/host.go | 6 ++ client/internal/dns/host_darwin.go | 161 +++++++++++++++++++---------- 2 files changed, 115 insertions(+), 52 deletions(-) diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index 3070763a6..e55a07055 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -15,6 +15,12 @@ type hostManager interface { restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error } +type SystemDNSSettings struct { + Domains []string + ServerIP string + ServerPort int +} + type HostDNSConfig struct { Domains []DomainConfig `json:"domains"` RouteAll bool `json:"routeAll"` diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 5ae84fb91..70d2443ef 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "net" "net/netip" "os/exec" "strconv" @@ -18,7 +19,7 @@ import ( const ( netbirdDNSStateKeyFormat = "State:/Network/Service/NetBird-%s/DNS" globalIPv4State = "State:/Network/Global/IPv4" - primaryServiceSetupKeyFormat = "Setup:/Network/Service/%s/DNS" + primaryServiceStateKeyFormat = "State:/Network/Service/%s/DNS" keySupplementalMatchDomains = "SupplementalMatchDomains" keySupplementalMatchDomainsNoSearch = "SupplementalMatchDomainsNoSearch" keyServerAddresses = "ServerAddresses" @@ -28,12 +29,12 @@ const ( scutilPath = "/usr/sbin/scutil" searchSuffix = "Search" matchSuffix = "Match" + localSuffix = "Local" ) type systemConfigurator struct { - // primaryServiceID primary interface in the system. AKA the interface with the default route - primaryServiceID string - createdKeys map[string]struct{} + createdKeys map[string]struct{} + systemDNSSettings SystemDNSSettings } func newHostManager() (hostManager, error) { @@ -49,20 +50,6 @@ func (s *systemConfigurator) supportCustomPort() bool { func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error { var err error - if config.RouteAll { - err = s.addDNSSetupForAll(config.ServerIP, config.ServerPort) - if err != nil { - return fmt.Errorf("add dns setup for all: %w", err) - } - } else if s.primaryServiceID != "" { - err = s.removeKeyFromSystemConfig(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID)) - if err != nil { - return fmt.Errorf("remote key from system config: %w", err) - } - s.primaryServiceID = "" - log.Infof("removed %s:%d as main DNS resolver for this peer", config.ServerIP, config.ServerPort) - } - // create a file for unclean shutdown detection if err := createUncleanShutdownIndicator(); err != nil { log.Errorf("failed to create unclean shutdown file: %s", err) @@ -73,6 +60,19 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error { matchDomains []string ) + err = s.recordSystemDNSSettings(true) + if err != nil { + log.Errorf("unable to update record of System's DNS config: %s", err.Error()) + } + + if config.RouteAll { + searchDomains = append(searchDomains, "\"\"") + err = s.addLocalDNS() + if err != nil { + log.Infof("failed to enable split DNS") + } + } + for _, dConf := range config.Domains { if dConf.Disabled { continue @@ -119,10 +119,6 @@ func (s *systemConfigurator) restoreHostDNS() error { } log.Infof("removing %s domains from system", keyType) } - if s.primaryServiceID != "" { - lines += buildRemoveKeyOperation(getKeyWithInput(primaryServiceSetupKeyFormat, s.primaryServiceID)) - log.Infof("restoring DNS resolver configuration for system") - } _, err := runSystemConfigCommand(wrapCommand(lines)) if err != nil { log.Errorf("got an error while cleaning the system configuration: %s", err) @@ -148,6 +144,97 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error { return nil } +func (s *systemConfigurator) addLocalDNS() error { + if s.systemDNSSettings.ServerIP == "" || len(s.systemDNSSettings.Domains) == 0 { + err := s.recordSystemDNSSettings(true) + log.Errorf("Unable to get system DNS configuration") + return err + } + localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix) + if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 { + err := s.addSearchDomains(localKey, strings.Join(s.systemDNSSettings.Domains, " "), s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort) + if err != nil { + return fmt.Errorf("couldn't add local network DNS conf: %w", err) + } + } else { + log.Info("Not enabling local DNS server") + } + + return nil +} + +func (s *systemConfigurator) recordSystemDNSSettings(force bool) error { + if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 && !force { + return nil + } + + systemDNSSettings, err := s.getSystemDNSSettings() + if err != nil { + return fmt.Errorf("couldn't get current DNS config: %w", err) + } + s.systemDNSSettings = systemDNSSettings + + return nil +} + +func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) { + primaryServiceKey, _, err := s.getPrimaryService() + if err != nil || primaryServiceKey == "" { + return SystemDNSSettings{}, fmt.Errorf("couldn't find the primary service key: %w", err) + } + dnsServiceKey := getKeyWithInput(primaryServiceStateKeyFormat, primaryServiceKey) + line := buildCommandLine("show", dnsServiceKey, "") + stdinCommands := wrapCommand(line) + + b, err := runSystemConfigCommand(stdinCommands) + if err != nil { + return SystemDNSSettings{}, fmt.Errorf("sending the command: %w", err) + } + + var dnsSettings SystemDNSSettings + inSearchDomainsArray := false + inServerAddressesArray := false + + scanner := bufio.NewScanner(bytes.NewReader(b)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + switch { + case strings.HasPrefix(line, "DomainName :"): + domainName := strings.TrimSpace(strings.Split(line, ":")[1]) + dnsSettings.Domains = append(dnsSettings.Domains, domainName) + case line == "SearchDomains : {": + inSearchDomainsArray = true + continue + case line == "ServerAddresses : {": + inServerAddressesArray = true + continue + case line == "}": + inSearchDomainsArray = false + inServerAddressesArray = false + } + + if inSearchDomainsArray { + searchDomains := strings.Split(line, " : ") + dnsSettings.Domains = append(dnsSettings.Domains, searchDomains...) + } else if inServerAddressesArray { + address := strings.Split(line, " : ")[1] + if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { + dnsSettings.ServerIP = address + inServerAddressesArray = false // Stop reading after finding the first IPv4 address + } + } + } + + if err := scanner.Err(); err != nil { + return dnsSettings, err + } + + // default to 53 port + dnsSettings.ServerPort = 53 + + return dnsSettings, nil +} + func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error { err := s.addDNSState(key, domains, ip, port, true) if err != nil { @@ -194,23 +281,6 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port return nil } -func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error { - primaryServiceKey, existingNameserver, err := s.getPrimaryService() - if err != nil || primaryServiceKey == "" { - return fmt.Errorf("couldn't find the primary service key: %w", err) - } - - err = s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver) - if err != nil { - return fmt.Errorf("add dns setup: %w", err) - } - - log.Infof("configured %s:%d as main DNS resolver for this peer", dnsServer, port) - s.primaryServiceID = primaryServiceKey - - return nil -} - func (s *systemConfigurator) getPrimaryService() (string, string, error) { line := buildCommandLine("show", globalIPv4State, "") stdinCommands := wrapCommand(line) @@ -239,19 +309,6 @@ func (s *systemConfigurator) getPrimaryService() (string, string, error) { return primaryService, router, nil } -func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error { - lines := buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+strconv.Itoa(0)) - lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer+" "+existingDNSServer) - lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port)) - addDomainCommand := buildCreateStateWithOperation(setupKey, lines) - stdinCommands := wrapCommand(addDomainCommand) - _, err := runSystemConfigCommand(stdinCommands) - if err != nil { - return fmt.Errorf("applying dns setup, error: %w", err) - } - return nil -} - func (s *systemConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { if err := s.restoreHostDNS(); err != nil { return fmt.Errorf("restoring dns via scutil: %w", err)