diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index b92e1cc05..d416afaac 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -11,11 +11,12 @@ import ( "github.com/kardianos/service" log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/server" "github.com/netbirdio/netbird/util" - "github.com/spf13/cobra" - "google.golang.org/grpc" ) func (p *program) Start(svc service.Service) error { @@ -109,7 +110,6 @@ var runCmd = &cobra.Command{ if err != nil { return err } - cmd.Printf("Netbird service is running") return nil }, } diff --git a/client/internal/connect.go b/client/internal/connect.go index 575269956..7e752ad79 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -2,6 +2,7 @@ package internal import ( "context" + "errors" "fmt" "strings" "time" @@ -93,6 +94,12 @@ func runClient( ) error { log.Infof("starting NetBird client version %s", version.NetbirdVersion()) + // Check if client was not shut down in a clean way and restore DNS config if required. + // Otherwise, we might not be able to connect to the management server to retrieve new config. + if err := dns.CheckUncleanShutdown(config.WgIface); err != nil { + log.Errorf("checking unclean shutdown error: %s", err) + } + backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, @@ -244,7 +251,7 @@ func runClient( log.Info("stopped NetBird client") - if _, err := state.Status(); err == ErrResetConnection { + if _, err := state.Status(); errors.Is(err, ErrResetConnection) { return err } diff --git a/client/internal/dns/dbus_linux.go b/client/internal/dns/dbus_linux.go index cb38cd9d9..b2604e9fa 100644 --- a/client/internal/dns/dbus_linux.go +++ b/client/internal/dns/dbus_linux.go @@ -4,9 +4,11 @@ package dns import ( "context" + "fmt" + "time" + "github.com/godbus/dbus/v5" log "github.com/sirupsen/logrus" - "time" ) const dbusDefaultFlag = 0 @@ -14,6 +16,7 @@ const dbusDefaultFlag = 0 func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool { obj, closeConn, err := getDbusObject(dest, path) if err != nil { + log.Tracef("error getting dbus object: %s", err) return false } defer closeConn() @@ -21,14 +24,18 @@ func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool { ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() - err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store() - return err == nil + if err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store(); err != nil { + log.Tracef("error calling dbus: %s", err) + return false + } + + return true } func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) { conn, err := dbus.SystemBus() if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("get dbus: %w", err) } obj := conn.Object(dest, path) diff --git a/client/internal/dns/file_linux.go b/client/internal/dns/file_linux.go index acb31b7f0..c62da7016 100644 --- a/client/internal/dns/file_linux.go +++ b/client/internal/dns/file_linux.go @@ -5,6 +5,7 @@ package dns import ( "bytes" "fmt" + "net/netip" "os" "strings" @@ -49,7 +50,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error { if backupFileExist { err = f.restore() if err != nil { - return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %s", err) + return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %w", err) } } return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") @@ -58,7 +59,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error { if !backupFileExist { err = f.backup() if err != nil { - return fmt.Errorf("unable to backup the resolv.conf file") + return fmt.Errorf("unable to backup the resolv.conf file: %w", err) } } @@ -67,7 +68,7 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error { resolvConf, err := parseBackupResolvConf() if err != nil { - log.Error(err) + log.Errorf("could not read original search domains from %s: %s", fileDefaultResolvConfBackupLocation, err) } f.repair.stopWatchFileChanges() @@ -96,10 +97,16 @@ func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP if restoreErr != nil { log.Errorf("attempt to restore default file failed with error: %s", err) } - return fmt.Errorf("got an creating resolver file %s. Error: %s", defaultResolvConfPath, err) + return fmt.Errorf("creating resolver file %s. Error: %w", defaultResolvConfPath, err) + } + + log.Infof("created a NetBird managed %s file with the DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList) + + // create another backup for unclean shutdown detection right after overwriting the original resolv.conf + if err := createUncleanShutdownIndicator(fileDefaultResolvConfBackupLocation, fileManager, nbNameserverIP); err != nil { + log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err) } - log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList) return nil } @@ -111,14 +118,14 @@ func (f *fileConfigurator) restoreHostDNS() error { func (f *fileConfigurator) backup() error { stats, err := os.Stat(defaultResolvConfPath) if err != nil { - return fmt.Errorf("got an error while checking stats for %s file. Error: %s", defaultResolvConfPath, err) + return fmt.Errorf("checking stats for %s file. Error: %w", defaultResolvConfPath, err) } f.originalPerms = stats.Mode() err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation) if err != nil { - return fmt.Errorf("got error while backing up the %s file. Error: %s", defaultResolvConfPath, err) + return fmt.Errorf("backing up %s: %w", defaultResolvConfPath, err) } return nil } @@ -126,12 +133,58 @@ func (f *fileConfigurator) backup() error { func (f *fileConfigurator) restore() error { err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath) if err != nil { - return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err) + return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err) + } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err) } return os.RemoveAll(fileDefaultResolvConfBackupLocation) } +func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error { + resolvConf, err := parseDefaultResolvConf() + if err != nil { + return fmt.Errorf("parse current resolv.conf: %w", err) + } + + // no current nameservers set -> restore + if len(resolvConf.nameServers) == 0 { + return restoreResolvConfFile() + } + + currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0]) + // not a valid first nameserver -> restore + if err != nil { + log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[1], err) + return restoreResolvConfFile() + } + + // current address is still netbird's non-available dns address -> restore + // comparing parsed addresses only, to remove ambiguity + if currentDNSAddress.String() == storedDNSAddress.String() { + return restoreResolvConfFile() + } + + log.Info("restoring unclean shutdown: first current nameserver differs from saved nameserver pre-netbird: not restoring") + return nil +} + +func restoreResolvConfFile() error { + log.Debugf("restoring unclean shutdown: restoring %s from %s", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation) + + if err := copyFile(fileUncleanShutdownResolvConfLocation, defaultResolvConfPath); err != nil { + return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation, err) + } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown resolv.conf file: %s", err) + } + + return nil +} + // generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list func generateNsList(nbNameserverIP string, cfg *resolvConf) []string { ns := make([]string, 1, len(cfg.nameServers)+1) @@ -231,17 +284,17 @@ func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string func copyFile(src, dest string) error { stats, err := os.Stat(src) if err != nil { - return fmt.Errorf("got an error while checking stats for %s file when copying it. Error: %s", src, err) + return fmt.Errorf("checking stats for %s file when copying it. Error: %s", src, err) } bytesRead, err := os.ReadFile(src) if err != nil { - return fmt.Errorf("got an error while reading the file %s file for copy. Error: %s", src, err) + return fmt.Errorf("reading the file %s file for copy. Error: %s", src, err) } err = os.WriteFile(dest, bytesRead, stats.Mode()) if err != nil { - return fmt.Errorf("got an writing the destination file %s for copy. Error: %s", dest, err) + return fmt.Errorf("writing the destination file %s for copy. Error: %s", dest, err) } return nil } diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index ee50b39d0..3070763a6 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "net/netip" "strings" nbdns "github.com/netbirdio/netbird/dns" @@ -11,6 +12,7 @@ type hostManager interface { applyDNSConfig(config HostDNSConfig) error restoreHostDNS() error supportCustomPort() bool + restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error } type HostDNSConfig struct { @@ -27,9 +29,10 @@ type DomainConfig struct { } type mockHostConfigurator struct { - applyDNSConfigFunc func(config HostDNSConfig) error - restoreHostDNSFunc func() error - supportCustomPortFunc func() bool + applyDNSConfigFunc func(config HostDNSConfig) error + restoreHostDNSFunc func() error + supportCustomPortFunc func() bool + restoreUncleanShutdownDNSFunc func(*netip.Addr) error } func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig) error { @@ -53,11 +56,19 @@ func (m *mockHostConfigurator) supportCustomPort() bool { return false } +func (m *mockHostConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error { + if m.restoreUncleanShutdownDNSFunc != nil { + return m.restoreUncleanShutdownDNSFunc(storedDNSAddress) + } + return fmt.Errorf("method restoreUncleanShutdownDNS is not implemented") +} + func newNoopHostMocker() hostManager { return &mockHostConfigurator{ - applyDNSConfigFunc: func(config HostDNSConfig) error { return nil }, - restoreHostDNSFunc: func() error { return nil }, - supportCustomPortFunc: func() bool { return true }, + applyDNSConfigFunc: func(config HostDNSConfig) error { return nil }, + restoreHostDNSFunc: func() error { return nil }, + supportCustomPortFunc: func() bool { return true }, + restoreUncleanShutdownDNSFunc: func(*netip.Addr) error { return nil }, } } diff --git a/client/internal/dns/host_android.go b/client/internal/dns/host_android.go index 169cc7c47..9230cb257 100644 --- a/client/internal/dns/host_android.go +++ b/client/internal/dns/host_android.go @@ -1,9 +1,11 @@ package dns +import "net/netip" + type androidHostManager struct { } -func newHostManager(wgInterface WGIface) (hostManager, error) { +func newHostManager() (hostManager, error) { return &androidHostManager{}, nil } @@ -18,3 +20,7 @@ func (a androidHostManager) restoreHostDNS() error { func (a androidHostManager) supportCustomPort() bool { return false } + +func (a androidHostManager) restoreUncleanShutdownDNS(*netip.Addr) error { + return nil +} diff --git a/client/internal/dns/host_darwin.go b/client/internal/dns/host_darwin.go index 0f16b7828..5ae84fb91 100644 --- a/client/internal/dns/host_darwin.go +++ b/client/internal/dns/host_darwin.go @@ -6,6 +6,8 @@ import ( "bufio" "bytes" "fmt" + "io" + "net/netip" "os/exec" "strconv" "strings" @@ -34,7 +36,7 @@ type systemConfigurator struct { createdKeys map[string]struct{} } -func newHostManager(_ WGIface) (hostManager, error) { +func newHostManager() (hostManager, error) { return &systemConfigurator{ createdKeys: make(map[string]struct{}), }, nil @@ -50,17 +52,22 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error { if config.RouteAll { err = s.addDNSSetupForAll(config.ServerIP, config.ServerPort) if err != nil { - return err + 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 err + 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) + } + var ( searchDomains []string matchDomains []string @@ -85,7 +92,7 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error { err = s.removeKeyFromSystemConfig(matchKey) } if err != nil { - return err + return fmt.Errorf("add match domains: %w", err) } searchKey := getKeyWithInput(netbirdDNSStateKeyFormat, searchSuffix) @@ -96,7 +103,7 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig) error { err = s.removeKeyFromSystemConfig(searchKey) } if err != nil { - return err + return fmt.Errorf("add search domains: %w", err) } return nil @@ -119,7 +126,11 @@ func (s *systemConfigurator) restoreHostDNS() error { _, err := runSystemConfigCommand(wrapCommand(lines)) if err != nil { log.Errorf("got an error while cleaning the system configuration: %s", err) - return err + return fmt.Errorf("clean system: %w", err) + } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown file: %s", err) } return nil @@ -129,7 +140,7 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error { line := buildRemoveKeyOperation(key) _, err := runSystemConfigCommand(wrapCommand(line)) if err != nil { - return err + return fmt.Errorf("remove key: %w", err) } delete(s.createdKeys, key) @@ -140,7 +151,7 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error { func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error { err := s.addDNSState(key, domains, ip, port, true) if err != nil { - return err + return fmt.Errorf("add dns state: %w", err) } log.Infof("added %d search domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains) @@ -153,7 +164,7 @@ func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, po func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error { err := s.addDNSState(key, domains, dnsServer, port, false) if err != nil { - return err + return fmt.Errorf("add dns state: %w", err) } log.Infof("added %d match domains to the state. Domain list: %s", len(strings.Split(domains, " ")), domains) @@ -178,33 +189,37 @@ func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port _, err := runSystemConfigCommand(stdinCommands) if err != nil { - return fmt.Errorf("got error while applying state for domains %s, error: %s", domains, err) + return fmt.Errorf("applying state for domains %s, error: %w", domains, err) } return nil } func (s *systemConfigurator) addDNSSetupForAll(dnsServer string, port int) error { - primaryServiceKey, existingNameserver := s.getPrimaryService() - if primaryServiceKey == "" { - return fmt.Errorf("couldn't find the primary service key") + 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) + + err = s.addDNSSetup(getKeyWithInput(primaryServiceSetupKeyFormat, primaryServiceKey), dnsServer, port, existingNameserver) if err != nil { - return err + 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) { +func (s *systemConfigurator) getPrimaryService() (string, string, error) { line := buildCommandLine("show", globalIPv4State, "") stdinCommands := wrapCommand(line) + b, err := runSystemConfigCommand(stdinCommands) if err != nil { - log.Error("got error while sending the command: ", err) - return "", "" + return "", "", fmt.Errorf("sending the command: %w", err) } + scanner := bufio.NewScanner(bytes.NewReader(b)) primaryService := "" router := "" @@ -217,7 +232,11 @@ func (s *systemConfigurator) getPrimaryService() (string, string) { router = strings.TrimSpace(strings.Split(text, ":")[1]) } } - return primaryService, router + if err := scanner.Err(); err != nil && err != io.EOF { + return primaryService, router, fmt.Errorf("scan: %w", err) + } + + return primaryService, router, nil } func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, existingDNSServer string) error { @@ -228,7 +247,14 @@ func (s *systemConfigurator) addDNSSetup(setupKey, dnsServer string, port int, e stdinCommands := wrapCommand(addDomainCommand) _, err := runSystemConfigCommand(stdinCommands) if err != nil { - return fmt.Errorf("got error while applying dns setup, error: %s", err) + 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) } return nil } @@ -266,7 +292,7 @@ func runSystemConfigCommand(command string) ([]byte, error) { cmd.Stdin = strings.NewReader(command) out, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("got error while running system configuration command: \"%s\", error: %s", command, err) + return nil, fmt.Errorf("running system configuration command: \"%s\", error: %w", command, err) } return out, nil } diff --git a/client/internal/dns/host_ios.go b/client/internal/dns/host_ios.go index 5058d6ba4..ad8b14fb8 100644 --- a/client/internal/dns/host_ios.go +++ b/client/internal/dns/host_ios.go @@ -2,6 +2,8 @@ package dns import ( "encoding/json" + "fmt" + "net/netip" log "github.com/sirupsen/logrus" ) @@ -20,7 +22,7 @@ func newHostManager(dnsManager IosDnsManager) (hostManager, error) { func (a iosHostManager) applyDNSConfig(config HostDNSConfig) error { jsonData, err := json.Marshal(config) if err != nil { - return err + return fmt.Errorf("marshal: %w", err) } jsonString := string(jsonData) log.Debugf("Applying DNS settings: %s", jsonString) @@ -35,3 +37,7 @@ func (a iosHostManager) restoreHostDNS() error { func (a iosHostManager) supportCustomPort() bool { return false } + +func (a iosHostManager) restoreUncleanShutdownDNS(*netip.Addr) error { + return nil +} diff --git a/client/internal/dns/host_linux.go b/client/internal/dns/host_linux.go index 931cda28d..37d8f704a 100644 --- a/client/internal/dns/host_linux.go +++ b/client/internal/dns/host_linux.go @@ -4,7 +4,9 @@ package dns import ( "bufio" + "errors" "fmt" + "io" "os" "strings" @@ -19,8 +21,27 @@ const ( resolvConfManager ) +var ErrUnknownOsManagerType = errors.New("unknown os manager type") + type osManagerType int +func newOsManagerType(osManager string) (osManagerType, error) { + switch osManager { + case "netbird": + return fileManager, nil + case "file": + return netbirdManager, nil + case "networkManager": + return networkManager, nil + case "systemd": + return systemdManager, nil + case "resolvconf": + return resolvConfManager, nil + default: + return 0, ErrUnknownOsManagerType + } +} + func (t osManagerType) String() string { switch t { case netbirdManager: @@ -38,13 +59,17 @@ func (t osManagerType) String() string { } } -func newHostManager(wgInterface WGIface) (hostManager, error) { +func newHostManager(wgInterface string) (hostManager, error) { osManager, err := getOSDNSManagerType() if err != nil { return nil, err } log.Debugf("discovered mode is: %s", osManager) + return newHostManagerFromType(wgInterface, osManager) +} + +func newHostManagerFromType(wgInterface string, osManager osManagerType) (hostManager, error) { switch osManager { case networkManager: return newNetworkManagerDbusConfigurator(wgInterface) @@ -58,12 +83,15 @@ func newHostManager(wgInterface WGIface) (hostManager, error) { } func getOSDNSManagerType() (osManagerType, error) { - file, err := os.Open(defaultResolvConfPath) if err != nil { - return 0, fmt.Errorf("unable to open %s for checking owner, got error: %s", defaultResolvConfPath, err) + return 0, fmt.Errorf("unable to open %s for checking owner, got error: %w", defaultResolvConfPath, err) } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + log.Errorf("close file %s: %s", defaultResolvConfPath, err) + } + }() scanner := bufio.NewScanner(file) for scanner.Scan() { @@ -101,6 +129,10 @@ func getOSDNSManagerType() (osManagerType, error) { return resolvConfManager, nil } } + if err := scanner.Err(); err != nil && err != io.EOF { + return 0, fmt.Errorf("scan: %w", err) + } + return fileManager, nil } diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 3a574c4ee..c8bf2e552 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -2,6 +2,8 @@ package dns import ( "fmt" + "io" + "net/netip" "strings" log "github.com/sirupsen/logrus" @@ -9,7 +11,7 @@ import ( ) const ( - dnsPolicyConfigMatchPath = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig\\NetBird-Match" + dnsPolicyConfigMatchPath = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\NetBird-Match` dnsPolicyConfigVersionKey = "Version" dnsPolicyConfigVersionValue = 2 dnsPolicyConfigNameKey = "Name" @@ -19,7 +21,7 @@ const ( ) const ( - interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces" + interfaceConfigPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces` interfaceConfigNameServerKey = "NameServer" interfaceConfigSearchListKey = "SearchList" ) @@ -34,12 +36,16 @@ func newHostManager(wgInterface WGIface) (hostManager, error) { if err != nil { return nil, err } + return newHostManagerWithGuid(guid) +} + +func newHostManagerWithGuid(guid string) (hostManager, error) { return ®istryConfigurator{ guid: guid, }, nil } -func (s *registryConfigurator) supportCustomPort() bool { +func (r *registryConfigurator) supportCustomPort() bool { return false } @@ -48,17 +54,22 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig) error { if config.RouteAll { err = r.addDNSSetupForAll(config.ServerIP) if err != nil { - return err + return fmt.Errorf("add dns setup: %w", err) } } else if r.routingAll { err = r.deleteInterfaceRegistryKeyProperty(interfaceConfigNameServerKey) if err != nil { - return err + return fmt.Errorf("delete interface registry key property: %w", err) } r.routingAll = false log.Infof("removed %s as main DNS forwarder for this peer", config.ServerIP) } + // create a file for unclean shutdown detection + if err := createUncleanShutdownIndicator(r.guid); err != nil { + log.Errorf("failed to create unclean shutdown file: %s", err) + } + var ( searchDomains []string matchDomains []string @@ -80,12 +91,12 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig) error { err = removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath) } if err != nil { - return err + return fmt.Errorf("add dns match policy: %w", err) } err = r.updateSearchDomains(searchDomains) if err != nil { - return err + return fmt.Errorf("update search domains: %w", err) } return nil @@ -94,7 +105,7 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig) error { func (r *registryConfigurator) addDNSSetupForAll(ip string) error { err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip) if err != nil { - return fmt.Errorf("adding dns setup for all failed with error: %s", err) + return fmt.Errorf("adding dns setup for all failed with error: %w", err) } r.routingAll = true log.Infof("configured %s:53 as main DNS forwarder for this peer", ip) @@ -106,33 +117,33 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er if err == nil { err = registry.DeleteKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath) if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err) + return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) } } regKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, dnsPolicyConfigMatchPath, registry.SET_VALUE) if err != nil { - return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", dnsPolicyConfigMatchPath, err) + return fmt.Errorf("unable to create registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", dnsPolicyConfigMatchPath, err) } err = regKey.SetDWordValue(dnsPolicyConfigVersionKey, dnsPolicyConfigVersionValue) if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigVersionKey, err) + return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigVersionKey, err) } err = regKey.SetStringsValue(dnsPolicyConfigNameKey, domains) if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigNameKey, err) + return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigNameKey, err) } err = regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip) if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigGenericDNSServersKey, err) + return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigGenericDNSServersKey, err) } err = regKey.SetDWordValue(dnsPolicyConfigConfigOptionsKey, dnsPolicyConfigConfigOptionsValue) if err != nil { - return fmt.Errorf("unable to set registry value for %s, error: %s", dnsPolicyConfigConfigOptionsKey, err) + return fmt.Errorf("unable to set registry value for %s, error: %w", dnsPolicyConfigConfigOptionsKey, err) } log.Infof("added %d match domains to the state. Domain list: %s", len(domains), domains) @@ -141,18 +152,25 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) er } func (r *registryConfigurator) restoreHostDNS() error { - err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath) - if err != nil { - log.Error(err) + if err := removeRegistryKeyFromDNSPolicyConfig(dnsPolicyConfigMatchPath); err != nil { + log.Errorf("remove registry key from dns policy config: %s", err) } - return r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey) + if err := r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey); err != nil { + return fmt.Errorf("remove interface registry key: %w", err) + } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown file: %s", err) + } + + return nil } func (r *registryConfigurator) updateSearchDomains(domains []string) error { err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")) if err != nil { - return fmt.Errorf("adding search domain failed with error: %s", err) + return fmt.Errorf("adding search domain failed with error: %w", err) } log.Infof("updated the search domains in the registry with %d domains. Domain list: %s", len(domains), domains) @@ -163,13 +181,13 @@ func (r *registryConfigurator) updateSearchDomains(domains []string) error { func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value string) error { regKey, err := r.getInterfaceRegistryKey() if err != nil { - return err + return fmt.Errorf("get interface registry key: %w", err) } - defer regKey.Close() + defer closer(regKey) err = regKey.SetStringValue(key, value) if err != nil { - return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %s", key, value, err) + return fmt.Errorf("applying key %s with value \"%s\" for interface failed with error: %w", key, value, err) } return nil @@ -178,13 +196,13 @@ func (r *registryConfigurator) setInterfaceRegistryKeyStringValue(key, value str func (r *registryConfigurator) deleteInterfaceRegistryKeyProperty(propertyKey string) error { regKey, err := r.getInterfaceRegistryKey() if err != nil { - return err + return fmt.Errorf("get interface registry key: %w", err) } - defer regKey.Close() + defer closer(regKey) err = regKey.DeleteValue(propertyKey) if err != nil { - return fmt.Errorf("deleting registry key %s for interface failed with error: %s", propertyKey, err) + return fmt.Errorf("deleting registry key %s for interface failed with error: %w", propertyKey, err) } return nil @@ -197,20 +215,33 @@ func (r *registryConfigurator) getInterfaceRegistryKey() (registry.Key, error) { regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.SET_VALUE) if err != nil { - return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err) + return regKey, fmt.Errorf("unable to open the interface registry key, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) } return regKey, nil } +func (r *registryConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { + if err := r.restoreHostDNS(); err != nil { + return fmt.Errorf("restoring dns via registry: %w", err) + } + return nil +} + func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error { k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyPath, registry.QUERY_VALUE) if err == nil { - k.Close() + defer closer(k) err = registry.DeleteKey(registry.LOCAL_MACHINE, regKeyPath) if err != nil { - return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %s", regKeyPath, err) + return fmt.Errorf("unable to remove existing key from registry, key: HKEY_LOCAL_MACHINE\\%s, error: %w", regKeyPath, err) } } return nil } + +func closer(closer io.Closer) { + if err := closer.Close(); err != nil { + log.Errorf("failed to close: %s", err) + } +} diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go index 170eaee08..ba4ae42d9 100644 --- a/client/internal/dns/local.go +++ b/client/internal/dns/local.go @@ -52,7 +52,7 @@ func (d *localResolver) lookupRecord(r *dns.Msg) dns.RR { func (d *localResolver) registerRecord(record nbdns.SimpleRecord) error { fullRecord, err := dns.NewRR(record.String()) if err != nil { - return err + return fmt.Errorf("register record: %w", err) } fullRecord.Header().Rdlength = record.Len() diff --git a/client/internal/dns/network_manager_linux.go b/client/internal/dns/network_manager_linux.go index b4a7a2514..dfd4cf4d3 100644 --- a/client/internal/dns/network_manager_linux.go +++ b/client/internal/dns/network_manager_linux.go @@ -5,8 +5,10 @@ package dns import ( "context" "encoding/binary" + "errors" "fmt" "net/netip" + "strings" "time" "github.com/godbus/dbus/v5" @@ -41,9 +43,13 @@ const ( networkManagerDbusPrimaryDNSPriority int32 = -500 networkManagerDbusWithMatchDomainPriority int32 = 0 networkManagerDbusSearchDomainOnlyPriority int32 = 50 - supportedNetworkManagerVersionConstraint = ">= 1.16, < 1.28" ) +var supportedNetworkManagerVersionConstraints = []string{ + ">= 1.16, < 1.27", + ">= 1.44, < 1.45", +} + type networkManagerDbusConfigurator struct { dbusLinkObject dbus.ObjectPath routingAll bool @@ -71,19 +77,19 @@ func (s networkManagerConnSettings) cleanDeprecatedSettings() { } } -func newNetworkManagerDbusConfigurator(wgInterface WGIface) (hostManager, error) { +func newNetworkManagerDbusConfigurator(wgInterface string) (hostManager, error) { obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusObjectNode) if err != nil { - return nil, err + return nil, fmt.Errorf("get nm dbus: %w", err) } defer closeConn() var s string - err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface.Name()).Store(&s) + err = obj.Call(networkManagerDbusGetDeviceByIPIfaceMethod, dbusDefaultFlag, wgInterface).Store(&s) if err != nil { - return nil, err + return nil, fmt.Errorf("call: %w", err) } - log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface.Name()) + log.Debugf("got network manager dbus Link Object: %s from net interface %s", s, wgInterface) return &networkManagerDbusConfigurator{ dbusLinkObject: dbus.ObjectPath(s), @@ -97,14 +103,14 @@ func (n *networkManagerDbusConfigurator) supportCustomPort() bool { func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { connSettings, configVersion, err := n.getAppliedConnectionSettings() if err != nil { - return fmt.Errorf("got an error while retrieving the applied connection settings, error: %s", err) + return fmt.Errorf("retrieving the applied connection settings, error: %w", err) } connSettings.cleanDeprecatedSettings() dnsIP, err := netip.ParseAddr(config.ServerIP) if err != nil { - return fmt.Errorf("unable to parse ip address, error: %s", err) + return fmt.Errorf("unable to parse ip address, error: %w", err) } convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice()) connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP}) @@ -145,23 +151,37 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig) er connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSPriorityKey] = dbus.MakeVariant(priority) connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSSearchKey] = dbus.MakeVariant(newDomainList) + // create a backup for unclean shutdown detection before adding domains, as these might end up in the resolv.conf file. + // The file content itself is not important for network-manager restoration + if err := createUncleanShutdownIndicator(defaultResolvConfPath, networkManager, dnsIP.String()); err != nil { + log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err) + } + log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains) err = n.reApplyConnectionSettings(connSettings, configVersion) if err != nil { - return fmt.Errorf("got an error while reapplying the connection with new settings, error: %s", err) + return fmt.Errorf("reapplying the connection with new settings, error: %w", err) } return nil } func (n *networkManagerDbusConfigurator) restoreHostDNS() error { // once the interface is gone network manager cleans all config associated with it - return n.deleteConnectionSettings() + if err := n.deleteConnectionSettings(); err != nil { + return fmt.Errorf("delete connection settings: %w", err) + } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err) + } + + return nil } func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (networkManagerConnSettings, networkManagerConfigVersion, error) { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { - return nil, 0, fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err) + return nil, 0, fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err) } defer closeConn() @@ -176,7 +196,7 @@ func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (network err = obj.CallWithContext(ctx, networkManagerDbusDeviceGetAppliedConnectionMethod, dbusDefaultFlag, networkManagerDbusDefaultBehaviorFlag).Store(&connSettings, &configVersion) if err != nil { - return nil, 0, fmt.Errorf("got error while calling GetAppliedConnection method with context, err: %s", err) + return nil, 0, fmt.Errorf("calling GetAppliedConnection method with context, err: %w", err) } return connSettings, configVersion, nil @@ -185,7 +205,7 @@ func (n *networkManagerDbusConfigurator) getAppliedConnectionSettings() (network func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings networkManagerConnSettings, configVersion networkManagerConfigVersion) error { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err) + return fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err) } defer closeConn() @@ -195,7 +215,7 @@ func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings err = obj.CallWithContext(ctx, networkManagerDbusDeviceReapplyMethod, dbusDefaultFlag, connSettings, configVersion, networkManagerDbusDefaultBehaviorFlag).Store() if err != nil { - return fmt.Errorf("got error while calling ReApply method with context, err: %s", err) + return fmt.Errorf("calling ReApply method with context, err: %w", err) } return nil @@ -204,21 +224,34 @@ func (n *networkManagerDbusConfigurator) reApplyConnectionSettings(connSettings func (n *networkManagerDbusConfigurator) deleteConnectionSettings() error { obj, closeConn, err := getDbusObject(networkManagerDest, n.dbusLinkObject) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the applied connection settings, err: %s", err) + return fmt.Errorf("attempting to retrieve the applied connection settings, err: %w", err) } defer closeConn() ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) defer cancel() + // this call is required to remove the device for DNS cleanup, even if it fails err = obj.CallWithContext(ctx, networkManagerDbusDeviceDeleteMethod, dbusDefaultFlag).Store() if err != nil { - return fmt.Errorf("got error while calling delete method with context, err: %s", err) + var dbusErr dbus.Error + if errors.As(err, &dbusErr) && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name { + // interface is gone already + return nil + } + return fmt.Errorf("calling delete method with context, err: %s", err) } return nil } +func (n *networkManagerDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { + if err := n.restoreHostDNS(); err != nil { + return fmt.Errorf("restoring dns via network-manager: %w", err) + } + return nil +} + func isNetworkManagerSupported() bool { return isNetworkManagerSupportedVersion() && isNetworkManagerSupportedMode() } @@ -250,13 +283,13 @@ func isNetworkManagerSupportedMode() bool { func getNetworkManagerDNSProperty(property string, store any) error { obj, closeConn, err := getDbusObject(networkManagerDest, networkManagerDbusDNSManagerObjectNode) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the network manager dns manager object, error: %s", err) + return fmt.Errorf("attempting to retrieve the network manager dns manager object, error: %w", err) } defer closeConn() v, e := obj.GetProperty(property) if e != nil { - return fmt.Errorf("got an error getting property %s: %v", property, e) + return fmt.Errorf("getting property %s: %w", property, e) } return v.Store(store) @@ -278,15 +311,26 @@ func isNetworkManagerSupportedVersion() bool { } versionValue, err := parseVersion(value.Value().(string)) if err != nil { + log.Errorf("nm: parse version: %s", err) return false } - constraints, err := version.NewConstraint(supportedNetworkManagerVersionConstraint) - if err != nil { - return false + var supported bool + for _, constraint := range supportedNetworkManagerVersionConstraints { + constr, err := version.NewConstraint(constraint) + if err != nil { + log.Errorf("nm: create constraint: %s", err) + return false + } + + if met := constr.Check(versionValue); met { + supported = true + break + } } - return constraints.Check(versionValue) + log.Debugf("network manager constraints [%s] met: %t", strings.Join(supportedNetworkManagerVersionConstraints, " | "), supported) + return supported } func parseVersion(inputVersion string) (*version.Version, error) { diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_linux.go index 1dad7627a..b8f753e28 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_linux.go @@ -5,6 +5,7 @@ package dns import ( "bytes" "fmt" + "net/netip" "os/exec" log "github.com/sirupsen/logrus" @@ -21,14 +22,14 @@ type resolvconf struct { } // supported "openresolv" only -func newResolvConfConfigurator(wgInterface WGIface) (hostManager, error) { +func newResolvConfConfigurator(wgInterface string) (hostManager, error) { resolvConfEntries, err := parseDefaultResolvConf() if err != nil { - log.Error(err) + log.Errorf("could not read original search domains from %s: %s", defaultResolvConfPath, err) } return &resolvconf{ - ifaceName: wgInterface.Name(), + ifaceName: wgInterface, originalSearchDomains: resolvConfEntries.searchDomains, originalNameServers: resolvConfEntries.nameServers, othersConfigs: resolvConfEntries.others, @@ -44,7 +45,7 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error { if !config.RouteAll { err = r.restoreHostDNS() if err != nil { - log.Error(err) + log.Errorf("restore host dns: %s", err) } return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured") } @@ -57,9 +58,14 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error { append([]string{config.ServerIP}, r.originalNameServers...), r.othersConfigs) + // create a backup for unclean shutdown detection before the resolv.conf is changed + if err := createUncleanShutdownIndicator(defaultResolvConfPath, resolvConfManager, config.ServerIP); err != nil { + log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err) + } + err = r.applyConfig(buf) if err != nil { - return err + return fmt.Errorf("apply config: %w", err) } log.Infof("added %d search domains. Search list: %s", len(searchDomainList), searchDomainList) @@ -67,20 +73,34 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig) error { } func (r *resolvconf) restoreHostDNS() error { + // openresolv only, debian resolvconf doesn't support "-f" cmd := exec.Command(resolvconfCommand, "-f", "-d", r.ifaceName) _, err := cmd.Output() if err != nil { - return fmt.Errorf("got an error while removing resolvconf configuration for %s interface, error: %s", r.ifaceName, err) + return fmt.Errorf("removing resolvconf configuration for %s interface, error: %w", r.ifaceName, err) } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err) + } + return nil } func (r *resolvconf) applyConfig(content bytes.Buffer) error { + // openresolv only, debian resolvconf doesn't support "-x" cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) cmd.Stdin = &content _, err := cmd.Output() if err != nil { - return fmt.Errorf("got an error while applying resolvconf configuration for %s interface, error: %s", r.ifaceName, err) + return fmt.Errorf("applying resolvconf configuration for %s interface, error: %w", r.ifaceName, err) + } + return nil +} + +func (r *resolvconf) restoreUncleanShutdownDNS(*netip.Addr) error { + if err := r.restoreHostDNS(); err != nil { + return fmt.Errorf("restoring dns for interface %s: %w", r.ifaceName, err) } return nil } diff --git a/client/internal/dns/response_writer.go b/client/internal/dns/response_writer.go index af02971b9..edc65a5d9 100644 --- a/client/internal/dns/response_writer.go +++ b/client/internal/dns/response_writer.go @@ -31,10 +31,13 @@ func (r *responseWriter) RemoteAddr() net.Addr { func (r *responseWriter) WriteMsg(msg *dns.Msg) error { buff, err := msg.Pack() if err != nil { - return err + return fmt.Errorf("pack: %w", err) } - _, err = r.Write(buff) - return err + + if _, err := r.Write(buff); err != nil { + return fmt.Errorf("write: %w", err) + } + return nil } // Write writes a raw buffer back to the client. diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index c22672cd0..9986f632e 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -142,12 +142,15 @@ func (s *DefaultServer) Initialize() (err error) { if s.permanent { err = s.service.Listen() if err != nil { - return err + return fmt.Errorf("service listen: %w", err) } } s.hostManager, err = s.initialize() - return err + if err != nil { + return fmt.Errorf("initialize: %w", err) + } + return nil } // DnsIP returns the DNS resolver server IP address @@ -225,7 +228,7 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro } if err := s.applyConfiguration(update); err != nil { - return err + return fmt.Errorf("apply configuration: %w", err) } s.updateSerial = serial diff --git a/client/internal/dns/server_android.go b/client/internal/dns/server_android.go index 5e1494e9e..7ca12d69d 100644 --- a/client/internal/dns/server_android.go +++ b/client/internal/dns/server_android.go @@ -1,5 +1,5 @@ package dns func (s *DefaultServer) initialize() (manager hostManager, err error) { - return newHostManager(s.wgInterface) + return newHostManager() } diff --git a/client/internal/dns/server_darwin.go b/client/internal/dns/server_darwin.go index feeb69352..d5a018f09 100644 --- a/client/internal/dns/server_darwin.go +++ b/client/internal/dns/server_darwin.go @@ -3,5 +3,5 @@ package dns func (s *DefaultServer) initialize() (manager hostManager, err error) { - return newHostManager(s.wgInterface) + return newHostManager() } diff --git a/client/internal/dns/server_linux.go b/client/internal/dns/server_linux.go index 7d7027839..aeb24b511 100644 --- a/client/internal/dns/server_linux.go +++ b/client/internal/dns/server_linux.go @@ -3,5 +3,5 @@ package dns func (s *DefaultServer) initialize() (manager hostManager, err error) { - return newHostManager(s.wgInterface) + return newHostManager(s.wgInterface.Name()) } diff --git a/client/internal/dns/service_listener.go b/client/internal/dns/service_listener.go index 232f6ebc2..9c783d9df 100644 --- a/client/internal/dns/service_listener.go +++ b/client/internal/dns/service_listener.go @@ -63,7 +63,7 @@ func (s *serviceViaListener) Listen() error { s.listenIP, s.listenPort, err = s.evalListenAddress() if err != nil { log.Errorf("failed to eval runtime address: %s", err) - return err + return fmt.Errorf("eval listen address: %w", err) } s.server.Addr = fmt.Sprintf("%s:%d", s.listenIP, s.listenPort) diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index 128dda840..757cd962a 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -44,7 +44,7 @@ func (s *serviceViaMemory) Listen() error { var err error s.udpFilterHookID, err = s.filterDNSTraffic() if err != nil { - return err + return fmt.Errorf("filter dns traffice: %w", err) } s.listenerIsRunning = true diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go index 3cd4342ad..27a93fbe1 100644 --- a/client/internal/dns/systemd_linux.go +++ b/client/internal/dns/systemd_linux.go @@ -4,6 +4,7 @@ package dns import ( "context" + "errors" "fmt" "net" "net/netip" @@ -30,6 +31,8 @@ const ( systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute" systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains" systemdDbusResolvConfModeForeign = "foreign" + + dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject" ) type systemdDbusConfigurator struct { @@ -52,22 +55,22 @@ type systemdDbusLinkDomainsInput struct { MatchOnly bool } -func newSystemdDbusConfigurator(wgInterface WGIface) (hostManager, error) { - iface, err := net.InterfaceByName(wgInterface.Name()) +func newSystemdDbusConfigurator(wgInterface string) (hostManager, error) { + iface, err := net.InterfaceByName(wgInterface) if err != nil { - return nil, err + return nil, fmt.Errorf("get interface: %w", err) } obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode) if err != nil { - return nil, err + return nil, fmt.Errorf("get dbus resolved dest: %w", err) } defer closeConn() var s string err = obj.Call(systemdDbusGetLinkMethod, dbusDefaultFlag, iface.Index).Store(&s) if err != nil { - return nil, err + return nil, fmt.Errorf("get dbus link method: %w", err) } log.Debugf("got dbus Link interface: %s from net interface %s and index %d", s, iface.Name, iface.Index) @@ -84,7 +87,7 @@ func (s *systemdDbusConfigurator) supportCustomPort() bool { func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { parsedIP, err := netip.ParseAddr(config.ServerIP) if err != nil { - return fmt.Errorf("unable to parse ip address, error: %s", err) + return fmt.Errorf("unable to parse ip address, error: %w", err) } ipAs4 := parsedIP.As4() defaultLinkInput := systemdDbusDNSInput{ @@ -93,7 +96,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { } err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}) if err != nil { - return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %s", config.ServerIP, config.ServerPort, err) + return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %w", config.ServerIP, config.ServerPort, err) } var ( @@ -121,7 +124,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { log.Infof("configured %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort) err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true) if err != nil { - return fmt.Errorf("setting link as default dns router, failed with error: %s", err) + return fmt.Errorf("setting link as default dns router, failed with error: %w", err) } domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{ Domain: nbdns.RootZone, @@ -132,6 +135,12 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { log.Infof("removing %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort) } + // create a backup for unclean shutdown detection before adding domains, as these might end up in the resolv.conf file. + // The file content itself is not important for systemd restoration + if err := createUncleanShutdownIndicator(defaultResolvConfPath, systemdManager, parsedIP.String()); err != nil { + log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err) + } + log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains) err = s.setDomainsForInterface(domainsInput) if err != nil { @@ -143,7 +152,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig) error { func (s *systemdDbusConfigurator) setDomainsForInterface(domainsInput []systemdDbusLinkDomainsInput) error { err := s.callLinkMethod(systemdDbusSetDomainsMethodSuffix, domainsInput) if err != nil { - return fmt.Errorf("setting domains configuration failed with error: %s", err) + return fmt.Errorf("setting domains configuration failed with error: %w", err) } return s.flushCaches() } @@ -153,17 +162,29 @@ func (s *systemdDbusConfigurator) restoreHostDNS() error { if !isDbusListenerRunning(systemdResolvedDest, s.dbusLinkObject) { return nil } + + // this call is required for DNS cleanup, even if it fails err := s.callLinkMethod(systemdDbusRevertMethodSuffix, nil) if err != nil { - return fmt.Errorf("unable to revert link configuration, got error: %s", err) + var dbusErr dbus.Error + if errors.As(err, &dbusErr) && dbusErr.Name == dbusErrorUnknownObject { + // interface is gone already + return nil + } + return fmt.Errorf("unable to revert link configuration, got error: %w", err) } + + if err := removeUncleanShutdownIndicator(); err != nil { + log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err) + } + return s.flushCaches() } func (s *systemdDbusConfigurator) flushCaches() error { obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the object %s, err: %s", systemdDbusObjectNode, err) + return fmt.Errorf("attempting to retrieve the object %s, err: %w", systemdDbusObjectNode, err) } defer closeConn() ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) @@ -171,7 +192,7 @@ func (s *systemdDbusConfigurator) flushCaches() error { err = obj.CallWithContext(ctx, systemdDbusFlushCachesMethod, dbusDefaultFlag).Store() if err != nil { - return fmt.Errorf("got error while calling the FlushCaches method with context, err: %s", err) + return fmt.Errorf("calling the FlushCaches method with context, err: %w", err) } return nil @@ -180,7 +201,7 @@ func (s *systemdDbusConfigurator) flushCaches() error { func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error { obj, closeConn, err := getDbusObject(systemdResolvedDest, s.dbusLinkObject) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the object, err: %s", err) + return fmt.Errorf("attempting to retrieve the object, err: %w", err) } defer closeConn() @@ -194,22 +215,29 @@ func (s *systemdDbusConfigurator) callLinkMethod(method string, value any) error } if err != nil { - return fmt.Errorf("got error while calling command with context, err: %s", err) + return fmt.Errorf("calling command with context, err: %w", err) } return nil } +func (s *systemdDbusConfigurator) restoreUncleanShutdownDNS(*netip.Addr) error { + if err := s.restoreHostDNS(); err != nil { + return fmt.Errorf("restoring dns via systemd: %w", err) + } + return nil +} + func getSystemdDbusProperty(property string, store any) error { obj, closeConn, err := getDbusObject(systemdResolvedDest, systemdDbusObjectNode) if err != nil { - return fmt.Errorf("got error while attempting to retrieve the systemd dns manager object, error: %s", err) + return fmt.Errorf("attempting to retrieve the systemd dns manager object, error: %w", err) } defer closeConn() v, e := obj.GetProperty(property) if e != nil { - return fmt.Errorf("got an error getting property %s: %v", property, e) + return fmt.Errorf("getting property %s: %w", property, e) } return v.Store(store) diff --git a/client/internal/dns/unclean_shutdown_android.go b/client/internal/dns/unclean_shutdown_android.go new file mode 100644 index 000000000..105fb00bf --- /dev/null +++ b/client/internal/dns/unclean_shutdown_android.go @@ -0,0 +1,5 @@ +package dns + +func CheckUncleanShutdown(string) error { + return nil +} diff --git a/client/internal/dns/unclean_shutdown_darwin.go b/client/internal/dns/unclean_shutdown_darwin.go new file mode 100644 index 000000000..e077ec84d --- /dev/null +++ b/client/internal/dns/unclean_shutdown_darwin.go @@ -0,0 +1,59 @@ +//go:build !ios + +package dns + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" +) + +const fileUncleanShutdownFileLocation = "/var/lib/netbird/unclean_shutdown_dns" + +func CheckUncleanShutdown(string) error { + if _, err := os.Stat(fileUncleanShutdownFileLocation); err != nil { + if errors.Is(err, fs.ErrNotExist) { + // no file -> clean shutdown + return nil + } else { + return fmt.Errorf("state: %w", err) + } + } + + log.Warnf("detected unclean shutdown, file %s exists. Restoring unclean shutdown dns settings.", fileUncleanShutdownFileLocation) + + manager, err := newHostManager() + if err != nil { + return fmt.Errorf("create host manager: %w", err) + } + + if err := manager.restoreUncleanShutdownDNS(nil); err != nil { + return fmt.Errorf("restore unclean shutdown backup: %w", err) + } + + return nil +} + +func createUncleanShutdownIndicator() error { + dir := filepath.Dir(fileUncleanShutdownFileLocation) + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return fmt.Errorf("create dir %s: %w", dir, err) + } + + if err := os.WriteFile(fileUncleanShutdownFileLocation, nil, 0644); err != nil { //nolint:gosec + return fmt.Errorf("create %s: %w", fileUncleanShutdownFileLocation, err) + } + + return nil +} + +func removeUncleanShutdownIndicator() error { + if err := os.Remove(fileUncleanShutdownFileLocation); err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("remove %s: %w", fileUncleanShutdownFileLocation, err) + } + return nil +} diff --git a/client/internal/dns/unclean_shutdown_ios.go b/client/internal/dns/unclean_shutdown_ios.go new file mode 100644 index 000000000..105fb00bf --- /dev/null +++ b/client/internal/dns/unclean_shutdown_ios.go @@ -0,0 +1,5 @@ +package dns + +func CheckUncleanShutdown(string) error { + return nil +} diff --git a/client/internal/dns/unclean_shutdown_linux.go b/client/internal/dns/unclean_shutdown_linux.go new file mode 100644 index 000000000..afd587720 --- /dev/null +++ b/client/internal/dns/unclean_shutdown_linux.go @@ -0,0 +1,96 @@ +//go:build !android + +package dns + +import ( + "errors" + "fmt" + "io/fs" + "net/netip" + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" +) + +const ( + fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf" + fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager" +) + +func CheckUncleanShutdown(wgIface string) error { + if _, err := os.Stat(fileUncleanShutdownResolvConfLocation); err != nil { + if errors.Is(err, fs.ErrNotExist) { + // no file -> clean shutdown + return nil + } else { + return fmt.Errorf("state: %w", err) + } + } + + log.Warnf("detected unclean shutdown, file %s exists", fileUncleanShutdownResolvConfLocation) + + managerData, err := os.ReadFile(fileUncleanShutdownManagerTypeLocation) + if err != nil { + return fmt.Errorf("read %s: %w", fileUncleanShutdownManagerTypeLocation, err) + } + + managerFields := strings.Split(string(managerData), ",") + if len(managerFields) < 2 { + return errors.New("split manager data: insufficient number of fields") + } + osManagerTypeStr, dnsAddressStr := managerFields[0], managerFields[1] + + dnsAddress, err := netip.ParseAddr(dnsAddressStr) + if err != nil { + return fmt.Errorf("parse dns address %s failed: %w", dnsAddressStr, err) + } + + log.Warnf("restoring unclean shutdown dns settings via previously detected manager: %s", osManagerTypeStr) + + // determine os manager type, so we can invoke the respective restore action + osManagerType, err := newOsManagerType(osManagerTypeStr) + if err != nil { + return fmt.Errorf("detect previous host manager: %w", err) + } + + manager, err := newHostManagerFromType(wgIface, osManagerType) + if err != nil { + return fmt.Errorf("create previous host manager: %w", err) + } + + if err := manager.restoreUncleanShutdownDNS(&dnsAddress); err != nil { + return fmt.Errorf("restore unclean shutdown backup: %w", err) + } + + return nil +} + +func createUncleanShutdownIndicator(sourcePath string, managerType osManagerType, dnsAddress string) error { + dir := filepath.Dir(fileUncleanShutdownResolvConfLocation) + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return fmt.Errorf("create dir %s: %w", dir, err) + } + + if err := copyFile(sourcePath, fileUncleanShutdownResolvConfLocation); err != nil { + return fmt.Errorf("create %s: %w", sourcePath, err) + } + + managerData := fmt.Sprintf("%s,%s", managerType, dnsAddress) + + if err := os.WriteFile(fileUncleanShutdownManagerTypeLocation, []byte(managerData), 0644); err != nil { //nolint:gosec + return fmt.Errorf("create %s: %w", fileUncleanShutdownManagerTypeLocation, err) + } + return nil +} + +func removeUncleanShutdownIndicator() error { + if err := os.Remove(fileUncleanShutdownResolvConfLocation); err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("remove %s: %w", fileUncleanShutdownResolvConfLocation, err) + } + if err := os.Remove(fileUncleanShutdownManagerTypeLocation); err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("remove %s: %w", fileUncleanShutdownManagerTypeLocation, err) + } + return nil +} diff --git a/client/internal/dns/unclean_shutdown_windows.go b/client/internal/dns/unclean_shutdown_windows.go new file mode 100644 index 000000000..41db46768 --- /dev/null +++ b/client/internal/dns/unclean_shutdown_windows.go @@ -0,0 +1,75 @@ +package dns + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +const ( + netbirdProgramDataLocation = "Netbird" + fileUncleanShutdownFile = "unclean_shutdown_dns.txt" +) + +func CheckUncleanShutdown(string) error { + file := getUncleanShutdownFile() + + if _, err := os.Stat(file); err != nil { + if errors.Is(err, fs.ErrNotExist) { + // no file -> clean shutdown + return nil + } else { + return fmt.Errorf("state: %w", err) + } + } + + logrus.Warnf("detected unclean shutdown, file %s exists. Restoring unclean shutdown dns settings.", file) + + guid, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("read %s: %w", file, err) + } + + manager, err := newHostManagerWithGuid(string(guid)) + if err != nil { + return fmt.Errorf("create host manager: %w", err) + } + + if err := manager.restoreUncleanShutdownDNS(nil); err != nil { + return fmt.Errorf("restore unclean shutdown backup: %w", err) + } + + return nil +} + +func createUncleanShutdownIndicator(guid string) error { + file := getUncleanShutdownFile() + + dir := filepath.Dir(file) + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + return fmt.Errorf("create dir %s: %w", dir, err) + } + + if err := os.WriteFile(file, []byte(guid), 0600); err != nil { + return fmt.Errorf("create %s: %w", file, err) + } + + return nil +} + +func removeUncleanShutdownIndicator() error { + file := getUncleanShutdownFile() + + if err := os.Remove(file); err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("remove %s: %w", file, err) + } + return nil +} + +func getUncleanShutdownFile() string { + return filepath.Join(os.Getenv("PROGRAMDATA"), netbirdProgramDataLocation, fileUncleanShutdownFile) +} diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index 4e08841dc..9fd524700 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -219,7 +219,7 @@ func (u *upstreamResolverBase) waitUntilResponse() { } log.Tracef("checking connectivity with upstreams %s failed. Retrying in %s", u.upstreamServers, exponentialBackOff.NextBackOff()) - return fmt.Errorf("got an error from upstream check call") + return fmt.Errorf("upstream check call error") } err := backoff.Retry(operation, exponentialBackOff) diff --git a/client/internal/engine.go b/client/internal/engine.go index 4493c75e8..cfb947b13 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1080,6 +1080,11 @@ func (e *Engine) close() { log.Errorf("failed closing ebpf proxy: %s", err) } + // stop/restore DNS first so dbus and friends don't complain because of a missing interface + if e.dnsServer != nil { + e.dnsServer.Stop() + } + log.Debugf("removing Netbird interface %s", e.config.WgIfaceName) if e.wgInterface != nil { if err := e.wgInterface.Close(); err != nil { @@ -1098,10 +1103,6 @@ func (e *Engine) close() { e.routeManager.Stop() } - if e.dnsServer != nil { - e.dnsServer.Stop() - } - if e.firewall != nil { err := e.firewall.Reset() if err != nil { diff --git a/iface/iface_linux.go b/iface/iface_linux.go index 73606a25c..62ae0f0de 100644 --- a/iface/iface_linux.go +++ b/iface/iface_linux.go @@ -39,7 +39,6 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, wgIFace.tun = newTunUSPDevice(iFaceName, wgAddress, wgPort, wgPrivKey, mtu, transportNet) wgIFace.userspaceBind = true return wgIFace, nil - } // CreateOnAndroid this function make sense on mobile only