diff --git a/dns/override/dns_override_android.go b/dns/override/dns_override_android.go index d3fd78e..3c44a19 100644 --- a/dns/override/dns_override_android.go +++ b/dns/override/dns_override_android.go @@ -13,4 +13,10 @@ func SetupDNSOverride(interfaceName string, proxyIp netip.Addr) error { // RestoreDNSOverride is a no-op on Android func RestoreDNSOverride() error { return nil -} \ No newline at end of file +} + +// CleanupStaleState is a no-op on Android as DNS configuration is handled by the VpnService API +func CleanupStaleState(interfaceName string) error { + _ = interfaceName + return nil +} diff --git a/dns/override/dns_override_darwin.go b/dns/override/dns_override_darwin.go index c1c3789..77c267e 100644 --- a/dns/override/dns_override_darwin.go +++ b/dns/override/dns_override_darwin.go @@ -61,3 +61,20 @@ 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(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) + } + + 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..4178154 100644 --- a/dns/override/dns_override_ios.go +++ b/dns/override/dns_override_ios.go @@ -12,4 +12,10 @@ 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 -} \ No newline at end of file +} + +// CleanupStaleState is a no-op on iOS as DNS configuration is handled by the system +func CleanupStaleState(interfaceName string) error { + _ = interfaceName + return nil +} diff --git a/dns/override/dns_override_unix.go b/dns/override/dns_override_unix.go index 12cb692..fe2a7f6 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 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(interfaceName string) 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 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 { + 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..fd57203 100644 --- a/dns/override/dns_override_windows.go +++ b/dns/override/dns_override_windows.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 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(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 +} 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 +}