Compare commits
4 Commits
b5909ccb56
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5229e2c774 | |||
| 0c9277f687 | |||
| 3bf3cd162b | |||
| 70eb5c7b57 |
@@ -31,3 +31,6 @@ priv_key.bin
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Personal notes — not part of the public repo
|
||||
docs-syoul/
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ Pre-built `.deb` of the mycellium-ui desktop client. Tested on Debian 12 (bookwo
|
||||
sudo apt install ./mycellium-ui_0.1.0_amd64.deb
|
||||
```
|
||||
|
||||
`apt install` with a local path resolves runtime deps (`policykit-1`, `libwebkit2gtk-4.1-0`, `libgtk-3-0`) automatically. Plain `dpkg -i` will fail if any of those are missing.
|
||||
`apt install` with a local path resolves runtime deps (`pkexec` or `policykit-1` depending on the distro, `libwebkit2gtk-4.1-0`, `libgtk-3-0`) automatically. Plain `dpkg -i` will fail if any of those are missing.
|
||||
|
||||
The `pkexec | policykit-1` alternative covers both Debian 12 (where `pkexec` ships inside `policykit-1`) and Debian 13 (where `pkexec` is a standalone package).
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
c85a2f6584949bd04a2a56b7fc3a5f7ed6e99c3291dec6c7eef99e8b28a0b2be release/mycellium-ui_0.1.0_amd64.deb
|
||||
38e0a3d03797490e1d6caa2681f9d5580493908834736b0db6a7d99558bb8823 release/mycellium-ui_0.1.0_amd64.deb
|
||||
|
||||
Binary file not shown.
Executable
+27
@@ -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 "$@"
|
||||
@@ -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>
|
||||
|
||||
@@ -14,11 +14,16 @@ use serde::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DaemonStatus {
|
||||
pub running: bool,
|
||||
pub api_url: Option<String>,
|
||||
pub key_path: Option<String>,
|
||||
pub config_path: Option<String>,
|
||||
/// Full overlay IPv6 (e.g. `43d:956e:7877:d933:eecc:b305:21ff:77f9`).
|
||||
/// Surfaced separately from `nodeSubnet` so the UI can show users the
|
||||
/// address they should actually use as a message destination.
|
||||
pub overlay_ip: Option<String>,
|
||||
}
|
||||
|
||||
fn snapshot(state: &AppState) -> DaemonStatus {
|
||||
@@ -28,6 +33,7 @@ fn snapshot(state: &AppState) -> DaemonStatus {
|
||||
api_url: sc.client().map(|c| c.base_url().to_string()),
|
||||
key_path: sc.key_path().map(|p| p.display().to_string()),
|
||||
config_path: sc.config_path().map(|p| p.display().to_string()),
|
||||
overlay_ip: sc.overlay_ip(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ pub struct SidecarHandle {
|
||||
logs: Mutex<VecDeque<String>>,
|
||||
config_path: Mutex<Option<PathBuf>>,
|
||||
key_path: Mutex<Option<PathBuf>>,
|
||||
/// Full overlay IPv6, parsed out of mycelium's `Node overlay IP: ...`
|
||||
/// startup log line. The daemon's HTTP API only exposes the subnet,
|
||||
/// so we extract the host part from the binary's stderr.
|
||||
overlay_ip: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
impl SidecarHandle {
|
||||
@@ -58,6 +62,7 @@ impl SidecarHandle {
|
||||
logs: Mutex::new(VecDeque::with_capacity(LOG_RING_CAPACITY)),
|
||||
config_path: Mutex::new(None),
|
||||
key_path: Mutex::new(None),
|
||||
overlay_ip: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +89,21 @@ impl SidecarHandle {
|
||||
self.config_path.lock().clone()
|
||||
}
|
||||
|
||||
pub fn overlay_ip(&self) -> Option<String> {
|
||||
self.overlay_ip.lock().clone()
|
||||
}
|
||||
|
||||
fn push_log(&self, line: String) {
|
||||
// mycelium prefixes log lines with ANSI colour escapes; strip them
|
||||
// before scanning for the overlay IP marker so the regex stays
|
||||
// resilient to cosmetic changes in upstream's log format.
|
||||
if self.overlay_ip.lock().is_none() {
|
||||
let stripped = strip_ansi(&line);
|
||||
if let Some(ip) = extract_overlay_ip(&stripped) {
|
||||
tracing::info!(overlay_ip = %ip, "node overlay IP captured");
|
||||
*self.overlay_ip.lock() = Some(ip);
|
||||
}
|
||||
}
|
||||
let mut buf = self.logs.lock();
|
||||
if buf.len() >= LOG_RING_CAPACITY {
|
||||
buf.pop_front();
|
||||
@@ -102,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)
|
||||
@@ -157,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);
|
||||
@@ -262,6 +293,7 @@ impl SidecarHandle {
|
||||
|
||||
fn cleanup(&self) {
|
||||
*self.api_url.lock() = None;
|
||||
*self.overlay_ip.lock() = None;
|
||||
let _ = self.child.lock().take();
|
||||
}
|
||||
|
||||
@@ -279,6 +311,57 @@ 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
|
||||
/// resilient enough.
|
||||
fn extract_overlay_ip(line: &str) -> Option<String> {
|
||||
const NEEDLE: &str = "Node overlay IP:";
|
||||
let idx = line.find(NEEDLE)?;
|
||||
let rest = line[idx + NEEDLE.len()..].trim();
|
||||
// The IP is the first whitespace-bounded token; rejection criteria
|
||||
// (must contain `:`, must not contain `/`) keep us from accidentally
|
||||
// capturing the subnet.
|
||||
let token = rest.split_whitespace().next()?;
|
||||
if !token.contains(':') || token.contains('/') {
|
||||
return None;
|
||||
}
|
||||
Some(token.to_string())
|
||||
}
|
||||
|
||||
/// Strip ANSI SGR escape sequences (`ESC [ ... m`) from a log line so the
|
||||
/// overlay-IP scanner doesn't choke on tracing's coloured output.
|
||||
fn strip_ansi(s: &str) -> String {
|
||||
let bytes = s.as_bytes();
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == 0x1B && i + 1 < bytes.len() && bytes[i + 1] == b'[' {
|
||||
// Skip until 'm' or any letter terminator.
|
||||
i += 2;
|
||||
while i < bytes.len() && !bytes[i].is_ascii_alphabetic() {
|
||||
i += 1;
|
||||
}
|
||||
if i < bytes.len() {
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
out.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn pick_port() -> AppResult<u16> {
|
||||
portpicker::pick_unused_port().ok_or_else(|| AppError::Other("no free port available".into()))
|
||||
}
|
||||
|
||||
@@ -40,9 +40,10 @@
|
||||
"externalBin": ["binaries/mycelium"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["policykit-1"],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface DaemonStatus {
|
||||
apiUrl: string | null;
|
||||
keyPath: string | null;
|
||||
configPath: string | null;
|
||||
/** Full overlay IPv6 (e.g. `43d:956e:7877:d933:eecc:b305:21ff:77f9`). */
|
||||
overlayIp: string | null;
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
|
||||
+7
-1
@@ -19,7 +19,13 @@ export const useNodeStore = defineStore("node", () => {
|
||||
exitedUnlisten = await on<number>(Events.SidecarExited, (e) => {
|
||||
error.value = `daemon exited (code ${e.payload})`;
|
||||
phase.value = "error";
|
||||
status.value = { running: false, apiUrl: null, keyPath: null, configPath: null };
|
||||
status.value = {
|
||||
running: false,
|
||||
apiUrl: null,
|
||||
keyPath: null,
|
||||
configPath: null,
|
||||
overlayIp: null,
|
||||
};
|
||||
info.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ async function copy(field: string, value: string) {
|
||||
|
||||
<template>
|
||||
<div v-if="info" class="max-w-3xl space-y-4">
|
||||
<InfoCard
|
||||
v-if="status?.overlayIp"
|
||||
label="Overlay IP — use this when sending messages"
|
||||
:value="status.overlayIp"
|
||||
:copied="copiedField === 'ip'"
|
||||
@copy="copy('ip', status.overlayIp!)"
|
||||
/>
|
||||
<InfoCard
|
||||
label="Overlay subnet"
|
||||
:value="info.nodeSubnet"
|
||||
|
||||
Reference in New Issue
Block a user