@@ -19,6 +19,7 @@ import (
1919 "github.com/urfave/cli/v2"
2020
2121 "github.com/NodeOps-app/createos-cli/internal/api"
22+ "github.com/NodeOps-app/createos-cli/internal/terminal"
2223)
2324
2425func newVPNCommand () * cli.Command {
@@ -109,16 +110,32 @@ func runVPNUp(c *cli.Context) error {
109110 or remove the conflicting route, then re-run 'createos sb vpn up'` , conflict )
110111 }
111112
112- // Defensive startup recovery: a prior CLI run that was killed (OOM,
113- // kernel panic, force-quit) leaves the cosvpn iface in the kernel
114- // without a matching server-side session. wg-quick up would then
115- // fail with "RTNETLINK answers: File exists". Wipe any stale iface
116- // before proceeding so the user doesn't have to manually intervene.
117- if out , _ := exec .CommandContext (c .Context , "ip" , "link" , "show" , "cosvpn" ).Output (); len (out ) > 0 { //nolint:errcheck // best-effort stale-iface probe; absent iface yields empty out
113+ // Detect a stale cosvpn iface left by a prior run (OOM, kernel panic,
114+ // force-quit). Presence check works cross-platform via `wg show`,
115+ // which ships with wg-quick on both Linux and macOS (unlike
116+ // `ip link show`, which doesn't exist on macOS). If it's up, ask
117+ // before tearing it down — silent removal could kill an intentional
118+ // tunnel the user set up manually.
119+ if probe := exec .CommandContext (c .Context , "wg" , "show" , "cosvpn" ).Run (); probe == nil {
120+ msg := "A WireGuard interface named 'cosvpn' is already up on this machine."
121+ if ! terminal .IsInteractive () {
122+ return fmt .Errorf ("%s Bring it down first with 'sudo wg-quick down cosvpn' and re-run" , msg )
123+ }
124+ pterm .Warning .Println (msg )
125+ ok , cErr := pterm .DefaultInteractiveConfirm .
126+ WithDefaultText ("Reset it and continue?" ).
127+ WithDefaultValue (false ).
128+ Show ()
129+ if cErr != nil {
130+ return fmt .Errorf ("could not read confirmation: %w" , cErr )
131+ }
132+ if ! ok {
133+ return fmt .Errorf ("cancelled — leaving existing cosvpn tunnel in place" )
134+ }
118135 cleanup := sudoCommand (c .Context , "wg-quick" , "down" , confPath )
119136 var cleanupBuf bytes.Buffer
120137 cleanup .Stdout , cleanup .Stderr = pickWGOutputs (debug , & cleanupBuf )
121- _ = cleanup .Run () //nolint:errcheck // best-effort; if cosvpn was never up, this is a no-op
138+ _ = cleanup .Run () //nolint:errcheck // best-effort teardown
122139 }
123140
124141 // Bring the tunnel up. wg-quick echoes every shell command it runs
0 commit comments