From 8549dc87460159da1305c8f80338ca859a9bceda Mon Sep 17 00:00:00 2001 From: Laurence Date: Thu, 26 Feb 2026 11:30:12 +0000 Subject: [PATCH 1/2] enhance(dns): expose stale cleanup functionality When the tunnel is forced close an integration may want to manually call cleanup function to fix stale issues without having the knowledge of which configuration to cleanup --- dns/override/dns_override_android.go | 5 +++ dns/override/dns_override_darwin.go | 16 ++++++++ dns/override/dns_override_ios.go | 5 +++ dns/override/dns_override_unix.go | 46 +++++++++++++++++++++++ dns/override/dns_override_windows.go | 15 ++++++++ dns/platform/darwin.go | 56 ++++++++++++++++++++++++++++ dns/platform/file.go | 24 ++++++++++++ dns/platform/network_manager.go | 38 +++++++++++++++++++ dns/platform/resolvconf.go | 34 +++++++++++++++++ 9 files changed, 239 insertions(+) diff --git a/dns/override/dns_override_android.go b/dns/override/dns_override_android.go index d3fd78e..97733e2 100644 --- a/dns/override/dns_override_android.go +++ b/dns/override/dns_override_android.go @@ -13,4 +13,9 @@ func SetupDNSOverride(interfaceName string, proxyIp netip.Addr) error { // RestoreDNSOverride is a no-op on Android func RestoreDNSOverride() error { return nil +} + +// CleanupStaleState is a no-op on Android as DNS configuration is handled by the VpnService API +func CleanupStaleState() error { + return nil } \ No newline at end of file diff --git a/dns/override/dns_override_darwin.go b/dns/override/dns_override_darwin.go index c1c3789..6f9f8e4 100644 --- a/dns/override/dns_override_darwin.go +++ b/dns/override/dns_override_darwin.go @@ -61,3 +61,19 @@ func RestoreDNSOverride() error { logger.Info("DNS configuration restored successfully") return nil } + +// CleanupStaleState removes any stale DNS configuration left over from a previous +// unclean shutdown (e.g., system crash, power loss while tunnel was active). +// This function should be called early during startup, before any network operations, +// to ensure DNS is working properly. +// +// On macOS, this cleans up any scutil DNS keys that were created but not removed. +func CleanupStaleState() error { + if err := platform.CleanupStaleDarwinDNS(); err != nil { + logger.Warn("Failed to cleanup stale Darwin DNS config: %v", err) + return fmt.Errorf("Darwin DNS cleanup: %w", err) + } + + logger.Info("Stale DNS state cleanup completed successfully") + return nil +} diff --git a/dns/override/dns_override_ios.go b/dns/override/dns_override_ios.go index 6c95c71..43251f1 100644 --- a/dns/override/dns_override_ios.go +++ b/dns/override/dns_override_ios.go @@ -12,4 +12,9 @@ func SetupDNSOverride(interfaceName string, proxyIp netip.Addr) error { // RestoreDNSOverride is a no-op on iOS as DNS configuration is handled by the system func RestoreDNSOverride() error { return nil +} + +// CleanupStaleState is a no-op on iOS as DNS configuration is handled by the system +func CleanupStaleState() error { + return nil } \ No newline at end of file diff --git a/dns/override/dns_override_unix.go b/dns/override/dns_override_unix.go index 12cb692..d9763fb 100644 --- a/dns/override/dns_override_unix.go +++ b/dns/override/dns_override_unix.go @@ -98,3 +98,49 @@ func RestoreDNSOverride() error { logger.Info("DNS configuration restored successfully") return nil } + +// CleanupStaleState removes any stale DNS configuration left over from a previous +// unclean shutdown (e.g., system crash, power loss while tunnel was active). +// This function should be called early during startup, before any network operations, +// to ensure DNS is working properly. +// +// It checks and cleans up stale state from all supported DNS managers: +// - NetworkManager: removes /etc/NetworkManager/conf.d/olm-dns.conf +// - resolvconf: removes entry for the "olm" interface +// - File-based: restores /etc/resolv.conf from backup if it exists +// +// This is safe to call even if no stale state exists. +func CleanupStaleState() error { + var errs []error + + // Clean up NetworkManager stale config + if err := platform.CleanupStaleNetworkManagerDNS(); err != nil { + logger.Warn("Failed to cleanup stale NetworkManager DNS config: %v", err) + errs = append(errs, fmt.Errorf("NetworkManager cleanup: %w", err)) + } else { + logger.Debug("NetworkManager DNS cleanup completed") + } + + // Clean up resolvconf stale entries (use default interface name "olm") + if err := platform.CleanupStaleResolvconfDNS("olm"); err != nil { + logger.Warn("Failed to cleanup stale resolvconf DNS config: %v", err) + errs = append(errs, fmt.Errorf("resolvconf cleanup: %w", err)) + } else { + logger.Debug("resolvconf DNS cleanup completed") + } + + // Clean up file-based stale backup + if err := platform.CleanupStaleFileDNS(); err != nil { + logger.Warn("Failed to cleanup stale file-based DNS config: %v", err) + errs = append(errs, fmt.Errorf("file DNS cleanup: %w", err)) + } else { + logger.Debug("File-based DNS cleanup completed") + } + + if len(errs) > 0 { + return fmt.Errorf("some DNS cleanup operations failed: %v", errs) + } + + logger.Info("Stale DNS state cleanup completed successfully") + return nil +} diff --git a/dns/override/dns_override_windows.go b/dns/override/dns_override_windows.go index 16bbca1..d6b6a91 100644 --- a/dns/override/dns_override_windows.go +++ b/dns/override/dns_override_windows.go @@ -61,3 +61,18 @@ func RestoreDNSOverride() error { logger.Info("DNS configuration restored successfully") return nil } + +// CleanupStaleState removes any stale DNS configuration left over from a previous +// unclean shutdown (e.g., system crash, power loss while tunnel was active). +// This function should be called early during startup, before any network operations, +// to ensure DNS is working properly. +// +// On Windows, DNS configuration is tied to the interface GUID. When the WireGuard +// interface is recreated, it gets a new GUID, so there's no stale state to clean up. +func CleanupStaleState() error { + // Windows DNS configuration via registry is interface-specific. + // When the WireGuard interface is recreated, it gets a new GUID, + // so there's no leftover state to clean up from previous sessions. + logger.Debug("Windows DNS cleanup: no stale state to clean (interface-specific)") + return nil +} diff --git a/dns/platform/darwin.go b/dns/platform/darwin.go index 8054c57..d928b77 100644 --- a/dns/platform/darwin.go +++ b/dns/platform/darwin.go @@ -417,3 +417,59 @@ func (d *DarwinDNSConfigurator) clearState() error { logger.Debug("Cleared DNS state file") return nil } + +// CleanupStaleDarwinDNS removes any stale DNS configuration left by the Darwin +// configurator from a previous unclean shutdown. This is a static function that can be +// called without creating a configurator instance, useful for cleanup before network operations. +func CleanupStaleDarwinDNS() error { + stateFilePath := getDNSStateFilePath() + + // Check if state file exists + data, err := os.ReadFile(stateFilePath) + if err != nil { + if os.IsNotExist(err) { + // No state file, nothing to clean up + return nil + } + return fmt.Errorf("read state file: %w", err) + } + + var state DNSPersistentState + if err := json.Unmarshal(data, &state); err != nil { + // Invalid state file, remove it + os.Remove(stateFilePath) + return nil + } + + if len(state.CreatedKeys) == 0 { + // No keys to clean up + return nil + } + + logger.Info("Found DNS state from previous session, cleaning up %d keys", len(state.CreatedKeys)) + + // Remove all keys from previous session using scutil directly + for _, key := range state.CreatedKeys { + logger.Debug("Removing leftover DNS key: %s", key) + cmd := fmt.Sprintf("open\nremove %s\nquit\n", key) + scutilCmd := exec.Command(scutilPath) + scutilCmd.Stdin = strings.NewReader(cmd) + if err := scutilCmd.Run(); err != nil { + logger.Warn("Failed to remove DNS key %s: %v", key, err) + } + } + + // Clear state file + if err := os.Remove(stateFilePath); err != nil && !os.IsNotExist(err) { + logger.Warn("Failed to clear DNS state file: %v", err) + } + + // Flush DNS cache after cleanup + cacheCmd := exec.Command(dscacheutilPath, "-flushcache") + _ = cacheCmd.Run() + + killCmd := exec.Command("killall", "-HUP", "mDNSResponder") + _ = killCmd.Run() + + return nil +} diff --git a/dns/platform/file.go b/dns/platform/file.go index 5f1cede..fe7ce56 100644 --- a/dns/platform/file.go +++ b/dns/platform/file.go @@ -218,3 +218,27 @@ func copyFile(src, dst string) error { return nil } + +// CleanupStaleFileDNS removes any stale DNS configuration left by the file-based +// configurator from a previous unclean shutdown. This is a static function that can be +// called without creating a configurator instance, useful for cleanup before network operations. +func CleanupStaleFileDNS() error { + // Check if backup file exists from a previous session + if _, err := os.Stat(resolvConfBackupPath); os.IsNotExist(err) { + // No backup file, nothing to clean up + return nil + } + + // A backup exists, which means we crashed while DNS was configured + // Restore the original resolv.conf + if err := copyFile(resolvConfBackupPath, resolvConfPath); err != nil { + return fmt.Errorf("restore from backup during cleanup: %w", err) + } + + // Remove backup file + if err := os.Remove(resolvConfBackupPath); err != nil { + return fmt.Errorf("remove backup file during cleanup: %w", err) + } + + return nil +} diff --git a/dns/platform/network_manager.go b/dns/platform/network_manager.go index 44eb655..a95f180 100644 --- a/dns/platform/network_manager.go +++ b/dns/platform/network_manager.go @@ -323,3 +323,41 @@ func GetNetworkManagerVersion() (string, error) { return version, nil } + +// CleanupStaleNetworkManagerDNS removes any stale DNS configuration left by NetworkManager +// configurator from a previous unclean shutdown. This is a static function that can be called +// without creating a configurator instance, useful for cleanup before network operations. +func CleanupStaleNetworkManagerDNS() error { + confPath := networkManagerConfDir + "/" + networkManagerDNSConfFile + + // Check if our config file exists from a previous session + if _, err := os.Stat(confPath); os.IsNotExist(err) { + // No config file, nothing to clean up + return nil + } + + // Remove the stale configuration file + if err := os.Remove(confPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove stale DNS config file: %w", err) + } + + // Try to reload NetworkManager if it's available + if IsNetworkManagerAvailable() { + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connect to system bus for reload: %w", err) + } + defer conn.Close() + + obj := conn.Object(networkManagerDest, networkManagerDbusObjectNode) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := obj.CallWithContext(ctx, networkManagerDest+".Reload", 0, uint32(0)).Store(); err != nil { + return fmt.Errorf("reload NetworkManager after cleanup: %w", err) + } + } + + return nil +} diff --git a/dns/platform/resolvconf.go b/dns/platform/resolvconf.go index 6f95c1f..d0259f3 100644 --- a/dns/platform/resolvconf.go +++ b/dns/platform/resolvconf.go @@ -219,3 +219,37 @@ func IsResolvconfAvailable() bool { cmd := exec.Command(resolvconfCommand, "--version") return cmd.Run() == nil } + +// CleanupStaleResolvconfDNS removes any stale DNS configuration left by the resolvconf +// configurator from a previous unclean shutdown. This is a static function that can be +// called without creating a configurator instance, useful for cleanup before network operations. +// The interfaceName parameter specifies which interface entry to clean up (typically "olm"). +func CleanupStaleResolvconfDNS(interfaceName string) error { + if !IsResolvconfAvailable() { + // resolvconf not available, nothing to clean up + return nil + } + + // Detect resolvconf implementation type + implType, err := detectResolvconfType() + if err != nil { + // Can't detect type, try default + implType = "resolvconf" + } + + // Try to delete any existing entry for this interface + // This is idempotent - if no entry exists, resolvconf will just return success + var cmd *exec.Cmd + + switch implType { + case "openresolv": + cmd = exec.Command(resolvconfCommand, "-f", "-d", interfaceName) + default: + cmd = exec.Command(resolvconfCommand, "-d", interfaceName) + } + + // Ignore errors - the entry may not exist, which is fine + _ = cmd.Run() + + return nil +} From f250702177d2b92db2ae92392e51b3af65793df5 Mon Sep 17 00:00:00 2001 From: Laurence Date: Thu, 12 Mar 2026 12:26:03 +0000 Subject: [PATCH 2/2] feat(DNS): Add static cleanup funcs To aid CLI in cleaning up configuration we expose static functions that know how to handle each provider and platform linked to https://github.com/fosrl/cli/issues/38 --- dns/override/dns_override_android.go | 5 +++-- dns/override/dns_override_darwin.go | 3 ++- dns/override/dns_override_ios.go | 5 +++-- dns/override/dns_override_unix.go | 8 ++++---- dns/override/dns_override_windows.go | 3 ++- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/dns/override/dns_override_android.go b/dns/override/dns_override_android.go index 97733e2..3c44a19 100644 --- a/dns/override/dns_override_android.go +++ b/dns/override/dns_override_android.go @@ -16,6 +16,7 @@ func RestoreDNSOverride() error { } // CleanupStaleState is a no-op on Android as DNS configuration is handled by the VpnService API -func CleanupStaleState() error { +func CleanupStaleState(interfaceName string) error { + _ = interfaceName return nil -} \ No newline at end of file +} diff --git a/dns/override/dns_override_darwin.go b/dns/override/dns_override_darwin.go index 6f9f8e4..77c267e 100644 --- a/dns/override/dns_override_darwin.go +++ b/dns/override/dns_override_darwin.go @@ -68,7 +68,8 @@ func RestoreDNSOverride() error { // to ensure DNS is working properly. // // On macOS, this cleans up any scutil DNS keys that were created but not removed. -func CleanupStaleState() error { +func CleanupStaleState(interfaceName string) error { + _ = interfaceName if err := platform.CleanupStaleDarwinDNS(); err != nil { logger.Warn("Failed to cleanup stale Darwin DNS config: %v", err) return fmt.Errorf("Darwin DNS cleanup: %w", err) diff --git a/dns/override/dns_override_ios.go b/dns/override/dns_override_ios.go index 43251f1..4178154 100644 --- a/dns/override/dns_override_ios.go +++ b/dns/override/dns_override_ios.go @@ -15,6 +15,7 @@ func RestoreDNSOverride() error { } // CleanupStaleState is a no-op on iOS as DNS configuration is handled by the system -func CleanupStaleState() error { +func CleanupStaleState(interfaceName string) error { + _ = interfaceName return nil -} \ No newline at end of file +} diff --git a/dns/override/dns_override_unix.go b/dns/override/dns_override_unix.go index d9763fb..fe2a7f6 100644 --- a/dns/override/dns_override_unix.go +++ b/dns/override/dns_override_unix.go @@ -106,11 +106,11 @@ func RestoreDNSOverride() error { // // It checks and cleans up stale state from all supported DNS managers: // - NetworkManager: removes /etc/NetworkManager/conf.d/olm-dns.conf -// - resolvconf: removes entry for the "olm" interface +// - resolvconf: removes entry for the provided interface // - File-based: restores /etc/resolv.conf from backup if it exists // // This is safe to call even if no stale state exists. -func CleanupStaleState() error { +func CleanupStaleState(interfaceName string) error { var errs []error // Clean up NetworkManager stale config @@ -121,8 +121,8 @@ func CleanupStaleState() error { logger.Debug("NetworkManager DNS cleanup completed") } - // Clean up resolvconf stale entries (use default interface name "olm") - if err := platform.CleanupStaleResolvconfDNS("olm"); err != nil { + // Clean up resolvconf stale entries for the provided interface + if err := platform.CleanupStaleResolvconfDNS(interfaceName); err != nil { logger.Warn("Failed to cleanup stale resolvconf DNS config: %v", err) errs = append(errs, fmt.Errorf("resolvconf cleanup: %w", err)) } else { diff --git a/dns/override/dns_override_windows.go b/dns/override/dns_override_windows.go index d6b6a91..fd57203 100644 --- a/dns/override/dns_override_windows.go +++ b/dns/override/dns_override_windows.go @@ -69,10 +69,11 @@ func RestoreDNSOverride() error { // // On Windows, DNS configuration is tied to the interface GUID. When the WireGuard // interface is recreated, it gets a new GUID, so there's no stale state to clean up. -func CleanupStaleState() error { +func CleanupStaleState(interfaceName string) error { // Windows DNS configuration via registry is interface-specific. // When the WireGuard interface is recreated, it gets a new GUID, // so there's no leftover state to clean up from previous sessions. + _ = interfaceName logger.Debug("Windows DNS cleanup: no stale state to clean (interface-specific)") return nil }