feat(packaging): pre-spawn cleanup wrapper for clean restarts

Symptom: each app restart that didn't go through Stop daemon left
an orphan mycelium running as root, claiming the TUN \"mycelium\",
UDP/9650 (multicast discovery) and TCP/8990 (JSON-RPC, hardcoded
in 0.6.1 — no flag). Subsequent starts panicked with EBUSY or
\"Address in use\" on whichever port the orphan held.

We can't SIGKILL the orphan from user-space (root process). Move
the cleanup into an elevated context that runs in the same pkexec
authentication as the daemon spawn:

  /usr/bin/mycellium-bootstrap   (new shell script in the .deb)
    pkill -9 -x mycelium
    ip link del mycelium / mycel0
    exec /usr/bin/mycelium \"\$@\"

The polkit policy now annotates this exact path with
auth_admin_keep so a single password prompt covers every
subsequent restart in the user's session.

Sidecar: when /usr/bin/mycellium-bootstrap exists (production
install) we hand pkexec that path instead of the bare daemon.
\`pnpm tauri dev\` falls back to the unwrapped binary path.
This commit is contained in:
syoul
2026-04-26 02:27:07 +02:00
parent 0c9277f687
commit 5229e2c774
6 changed files with 61 additions and 8 deletions

View File

@@ -1 +1 @@
acab8758c4ba7f3894e5e9bce28cbd759b8ec7e08a1ee00f54f6fdcac75a4221 release/mycellium-ui_0.1.0_amd64.deb
38e0a3d03797490e1d6caa2681f9d5580493908834736b0db6a7d99558bb8823 release/mycellium-ui_0.1.0_amd64.deb

Binary file not shown.

View File

@@ -0,0 +1,27 @@
#!/bin/sh
# /usr/bin/mycellium-bootstrap — installed by mycellium-ui.deb
#
# Wrapper around the mycelium daemon that guarantees a clean start
# every time. Without this, an orphan mycelium left over from a
# previous run (which the user-space launcher cannot SIGKILL because
# the daemon runs as root via pkexec) would block the next start
# with one of:
#
# * EBUSY on TUN device "mycelium" creation
# * "Address in use" on the JSON-RPC port (hardcoded 8990 in 0.6.1)
# * "Failed to bind multicast discovery socket" on UDP 9650
#
# This script runs under the same elevated context as the mycelium
# daemon itself (single pkexec call), so polkit's auth_admin_keep
# caching only fires one prompt per session.
set -e
# Best-effort cleanup. Errors ignored so the exec at the end always
# runs even on a clean machine.
pkill -9 -x mycelium 2>/dev/null || true
sleep 0.3
ip link del mycelium 2>/dev/null || true
ip link del mycel0 2>/dev/null || true
exec /usr/bin/mycelium "$@"

View File

@@ -6,7 +6,14 @@
<vendor>Threefold</vendor>
<vendor_url>https://threefold.io</vendor_url>
<action id="tech.threefold.mycellium-ui.spawn">
<!--
Bootstrap action: covers the wrapper that cleans up orphan
mycelium state and then execs the daemon. pkexec matches the
binary path against `org.freedesktop.policykit.exec.path` to
pick this action up; auth_admin_keep then caches the auth for
the user's session so subsequent restarts don't re-prompt.
-->
<action id="tech.threefold.mycellium-ui.bootstrap">
<description>Run the Mycelium overlay daemon</description>
<description xml:lang="fr">Lancer le démon de l'overlay Mycelium</description>
<message>Authentication is required to start the Mycelium overlay daemon.</message>
@@ -14,11 +21,8 @@
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<!-- Cache the authentication for the user's session so the polkit
dialog only appears once per login (5-minute window). To allow
passwordless start for trusted desktops, change to "yes" — be
aware this lets any process on the machine spawn the daemon. -->
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/mycellium-bootstrap</annotate>
</action>
</policyconfig>

View File

@@ -121,6 +121,17 @@ impl SidecarHandle {
}
let bin = locate_sidecar(app)?;
// In a `.deb` install, our pre-install script ships
// /usr/bin/mycellium-bootstrap that pkill+ip-link-del cleans
// any orphan state before exec-ing the real binary. Polkit
// is configured to auth_admin_keep that exact path, so
// subsequent starts are silent.
// Falls back to the bare binary in `tauri dev` where the
// bootstrap script isn't installed system-wide.
let elevation_target = match bootstrap_path() {
Some(p) => p,
None => bin.clone(),
};
// Three ports: HTTP API (loopback), TCP listen, QUIC (UDP) listen.
// mycelium defaults to 9651 for both peer-listen ports, which
// collides if another instance (or a leftover from a previous test)
@@ -176,11 +187,12 @@ impl SidecarHandle {
info!(
?bin,
elevation_target = %elevation_target.display(),
api_port, tcp_port, quic_port, metrics_port,
"spawning mycelium sidecar via pkexec"
);
let mut cmd = elevation::elevated(&bin, &args);
let mut cmd = elevation::elevated(&elevation_target, &args);
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
@@ -299,6 +311,15 @@ impl SidecarHandle {
}
}
/// Path of the privileged wrapper script shipped in our `.deb`. When
/// present, we invoke it instead of the mycelium binary directly so
/// the elevated context can clean up any orphan TUN / processes from
/// a previous crash before `exec /usr/bin/mycelium`.
fn bootstrap_path() -> Option<PathBuf> {
let p = PathBuf::from("/usr/bin/mycellium-bootstrap");
p.exists().then_some(p)
}
/// Mycelium logs the assigned overlay IPv6 once at startup like:
/// `INFO mycelium: Node overlay IP: 43d:956e:7877:d933:eecc:b305:21ff:77f9`
/// We don't pull a regex crate just for this — a hand-rolled parser is

View File

@@ -42,7 +42,8 @@
"deb": {
"depends": ["pkexec | policykit-1"],
"files": {
"/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy": "packaging/polkit/tech.threefold.mycellium-ui.policy"
"/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy": "packaging/polkit/tech.threefold.mycellium-ui.policy",
"/usr/bin/mycellium-bootstrap": "packaging/mycellium-bootstrap"
}
}
}