feat(cli): background the VPN daemon with re-exec + clean shutdown
`sunbeam connect` now fork-execs itself with a hidden `__vpn-daemon`
subcommand instead of running the daemon in-process. The user-facing
command spawns the child detached (stdio → log file, setsid for no
controlling TTY), polls the IPC socket until the daemon reaches
Running, prints a one-line status, and exits. The user gets back to
their shell immediately.
- src/cli.rs: `Connect { foreground }` instead of unit. Add hidden
`__vpn-daemon` Verb that the spawned child runs.
- src/vpn_cmds.rs: split into spawn_background_daemon (default path)
and run_daemon_foreground (used by both `connect --foreground` and
`__vpn-daemon`). Detached child uses pre_exec(setsid) and inherits
--context from the parent so it resolves the same VPN config.
Refuses to start if a daemon is already running on the control
socket; cleans up stale socket files. Switches the proxy bind from
16443 (sienna's existing SSH tunnel uses it) to 16579.
- sunbeam-net/src/daemon/lifecycle: add a SocketGuard RAII type so the
IPC control socket is unlinked when the daemon exits, regardless of
shutdown path. Otherwise `vpn status` after a clean disconnect would
see a stale socket and report an error.
End-to-end smoke test against the docker stack:
$ sunbeam connect
==> VPN daemon spawned (pid 90072, ...)
Connected (100.64.0.154, fd7a:115c:a1e0::9a) — 2 peers visible
$ sunbeam vpn status
VPN: running
addresses: 100.64.0.154, fd7a:115c:a1e0::9a
peers: 2
derp home: region 0
$ sunbeam disconnect
==> Asking VPN daemon to stop...
Daemon acknowledged shutdown.
$ sunbeam vpn status
VPN: not running
This commit is contained in:
@@ -42,6 +42,11 @@ async fn run_daemon_loop(
|
||||
status: Arc<RwLock<DaemonStatus>>,
|
||||
shutdown: tokio_util::sync::CancellationToken,
|
||||
) -> crate::Result<()> {
|
||||
// Make sure the IPC control socket is cleaned up no matter how the
|
||||
// daemon exits — otherwise `sunbeam vpn status` after a clean shutdown
|
||||
// would see a stale socket file and report "stale socket".
|
||||
let _socket_guard = SocketGuard::new(config.control_socket.clone());
|
||||
|
||||
let keys = crate::keys::NodeKeys::load_or_generate(&config.state_dir)?;
|
||||
let mut attempt: u32 = 0;
|
||||
let max_backoff = Duration::from_secs(60);
|
||||
@@ -86,6 +91,26 @@ enum SessionExit {
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// RAII guard that removes a Unix socket file when dropped. Used by
|
||||
/// `run_daemon_loop` to make sure the IPC control socket is cleaned up
|
||||
/// when the daemon exits, regardless of whether shutdown was triggered
|
||||
/// via DaemonHandle, IPC Stop, signal, or panic.
|
||||
struct SocketGuard {
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SocketGuard {
|
||||
fn new(path: std::path::PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SocketGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a single VPN session. Returns when the session ends (error or shutdown).
|
||||
async fn run_session(
|
||||
config: &VpnConfig,
|
||||
|
||||
Reference in New Issue
Block a user