From d1a7fc32c26bac463a16132ff13b0ae49c0e94b8 Mon Sep 17 00:00:00 2001 From: Laurence Date: Wed, 3 Jun 2026 13:48:44 +0100 Subject: [PATCH] feat: clean up stale DNS after unclean tunnel shutdown Wire olm's CleanupStaleState into up, down, and a new cleanup command so leftover DNS configuration is removed when the client did not shut down cleanly. Co-authored-by: Cursor --- cmd/cleanup/cleanup_unix.go | 43 ++++++++++++++++++++++++++++++++++ cmd/cleanup/cleanup_windows.go | 9 +++++++ cmd/down/client/client.go | 13 ++++++++-- cmd/root.go | 4 ++++ cmd/up/client/client.go | 6 +++++ internal/olmdns/cleanup.go | 16 +++++++++++++ 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 cmd/cleanup/cleanup_unix.go create mode 100644 cmd/cleanup/cleanup_windows.go create mode 100644 internal/olmdns/cleanup.go diff --git a/cmd/cleanup/cleanup_unix.go b/cmd/cleanup/cleanup_unix.go new file mode 100644 index 0000000..337d76b --- /dev/null +++ b/cmd/cleanup/cleanup_unix.go @@ -0,0 +1,43 @@ +//go:build !windows + +package cleanup + +import ( + "errors" + "os" + "runtime" + + "github.com/fosrl/cli/internal/logger" + "github.com/fosrl/cli/internal/olmdns" + "github.com/spf13/cobra" +) + +func CleanupCmd() *cobra.Command { + interfaceName := olmdns.DefaultInterfaceName + + cmd := &cobra.Command{ + Use: "cleanup", + Short: "Clean up stale DNS configuration", + Long: `Remove stale DNS configuration left from an unclean shutdown. + +This is useful if the client was killed while a tunnel was active and system DNS +was not restored. The same cleanup runs automatically before starting a connection.`, + RunE: func(cmd *cobra.Command, args []string) error { + if runtime.GOOS != "windows" && os.Geteuid() != 0 { + return errors.New("elevated permissions are required to clean up DNS configuration") + } + + if err := olmdns.CleanupStaleState(interfaceName); err != nil { + logger.Error("Failed to clean up stale DNS configuration: %v", err) + return err + } + + logger.Success("Stale DNS configuration cleaned up") + return nil + }, + } + + cmd.Flags().StringVar(&interfaceName, "interface-name", olmdns.DefaultInterfaceName, "WireGuard interface `name` used when the tunnel was active") + + return cmd +} diff --git a/cmd/cleanup/cleanup_windows.go b/cmd/cleanup/cleanup_windows.go new file mode 100644 index 0000000..1d55539 --- /dev/null +++ b/cmd/cleanup/cleanup_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package cleanup + +import "github.com/spf13/cobra" + +func CleanupCmd() *cobra.Command { + return nil +} diff --git a/cmd/down/client/client.go b/cmd/down/client/client.go index 6629481..b65e41b 100644 --- a/cmd/down/client/client.go +++ b/cmd/down/client/client.go @@ -6,27 +6,32 @@ import ( "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/logger" + "github.com/fosrl/cli/internal/olmdns" "github.com/fosrl/cli/internal/olm" "github.com/fosrl/cli/internal/tui" "github.com/spf13/cobra" ) func ClientDownCmd() *cobra.Command { + interfaceName := olmdns.DefaultInterfaceName + cmd := &cobra.Command{ Use: "client", Short: "Stop the client connection", Long: "Stop the currently running client connection", Run: func(cmd *cobra.Command, args []string) { - if err := clientDownMain(cmd); err != nil { + if err := clientDownMain(cmd, interfaceName); err != nil { os.Exit(1) } }, } + cmd.Flags().StringVar(&interfaceName, "interface-name", olmdns.DefaultInterfaceName, "WireGuard interface `name` used when the tunnel was active") + return cmd } -func clientDownMain(cmd *cobra.Command) error { +func clientDownMain(cmd *cobra.Command, interfaceName string) error { cfg := config.ConfigFromContext(cmd.Context()) // Get socket path from config or use default @@ -88,5 +93,9 @@ func clientDownMain(cmd *cobra.Command) error { logger.Info("Client shutdown initiated: %s", exitResp.Status) } + if err := olmdns.CleanupStaleState(interfaceName); err != nil { + logger.Warning("Failed to clean up stale DNS configuration: %v", err) + } + return nil } diff --git a/cmd/root.go b/cmd/root.go index add06f3..28c3afe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/fosrl/cli/cmd/auth/login" "github.com/fosrl/cli/cmd/auth/logout" "github.com/fosrl/cli/cmd/authdaemon" + "github.com/fosrl/cli/cmd/cleanup" "github.com/fosrl/cli/cmd/down" "github.com/fosrl/cli/cmd/list" "github.com/fosrl/cli/cmd/logs" @@ -60,6 +61,9 @@ func RootCommand(initResources bool) (*cobra.Command, error) { if downCmd := down.DownCmd(); downCmd != nil { cmd.AddCommand(downCmd) } + if cleanupCmd := cleanup.CleanupCmd(); cleanupCmd != nil { + cmd.AddCommand(cleanupCmd) + } if logsCmd := logs.LogsCmd(); logsCmd != nil { cmd.AddCommand(logsCmd) } diff --git a/cmd/up/client/client.go b/cmd/up/client/client.go index f497abb..56f4172 100644 --- a/cmd/up/client/client.go +++ b/cmd/up/client/client.go @@ -19,6 +19,7 @@ import ( "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/fingerprint" "github.com/fosrl/cli/internal/logger" + "github.com/fosrl/cli/internal/olmdns" "github.com/fosrl/cli/internal/olm" "github.com/fosrl/cli/internal/tui" "github.com/fosrl/cli/internal/utils" @@ -603,6 +604,11 @@ func clientUpMain(cmd *cobra.Command, opts *ClientUpCmdOpts, extraArgs []string) } } + // Clean up DNS left from an unclean shutdown before any network operations. + if err := olmdns.CleanupStaleState(opts.InterfaceName); err != nil { + logger.Warning("Failed to clean up stale DNS configuration: %v", err) + } + olm, err := olmpkg.Init(ctx, olmInitConfig) if err != nil { logger.Error("Error: failed to init olm: %v", err) diff --git a/internal/olmdns/cleanup.go b/internal/olmdns/cleanup.go new file mode 100644 index 0000000..a5a5f1c --- /dev/null +++ b/internal/olmdns/cleanup.go @@ -0,0 +1,16 @@ +package olmdns + +import ( + override "github.com/fosrl/olm/dns/override" +) + +const DefaultInterfaceName = "pangolin" + +// CleanupStaleState removes DNS configuration left from an unclean shutdown +// (for example, killing the process while the tunnel was active). +func CleanupStaleState(interfaceName string) error { + if interfaceName == "" { + interfaceName = DefaultInterfaceName + } + return override.CleanupStaleState(interfaceName) +}