fork: initialize Mycellium UI Private from Mycell-UI@5229e2c
This repo is a hard fork of mycellium-ui dedicated to the mycelium-private experimental track upstream. The two apps coexist on the same machine via distinct app identifiers, polkit actions, and binary names. Renames - package + crate: mycellium-ui → mycellium-ui-private - bundle identifier: tech.threefold.mycellium-ui-private - daemon binary: mycelium-private (separate upstream release tarball) - bootstrap wrapper: /usr/bin/mycellium-bootstrap-private - polkit policy file + action id Functional changes - SidecarConfig.network_name field (UTF-8, 2..=64 bytes) - start() refuses to spawn without a network name AND a 32-byte key file at app_data_dir/network_key.bin; surfaces a clear error rather than letting mycelium-private fail mid-startup - network_key_status / generate / import / export / delete commands; uses OS RNG (rand) and writes 0600 - empty default peers list (no Threefold seed for private overlays) - new Settings → Private network panel: name input, key generate / reveal-hex / import / delete, status indicator Adapted bootstrap script kills both `mycelium` and `mycelium-private` orphans (cross-clash on UDP/9650 + TCP/8990). CI workflow + sidebar branding updated. The README explains the divergence model and how to cherry-pick upstream fixes.
This commit is contained in:
4
src-tauri/Cargo.lock
generated
4
src-tauri/Cargo.lock
generated
@@ -1946,11 +1946,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mycellium-ui"
|
||||
name = "mycellium-ui-private"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"portpicker",
|
||||
"rand 0.8.6",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "mycellium-ui"
|
||||
name = "mycellium-ui-private"
|
||||
version = "0.1.0"
|
||||
description = "Mycelium overlay network desktop client"
|
||||
description = "Mycelium private network desktop client"
|
||||
authors = ["syoul"]
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
[lib]
|
||||
name = "mycellium_ui_lib"
|
||||
name = "mycellium_ui_private_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -27,6 +27,8 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
portpicker = "0.1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
hex = "0.4"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/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 "$@"
|
||||
29
src-tauri/packaging/mycellium-bootstrap-private
Executable file
29
src-tauri/packaging/mycellium-bootstrap-private
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# /usr/bin/mycellium-bootstrap-private — installed by mycellium-ui-private.deb
|
||||
#
|
||||
# Wrapper around the mycelium-private daemon that guarantees a clean
|
||||
# start every time. Without this, an orphan daemon left over from a
|
||||
# previous run (which the user-space launcher cannot SIGKILL because
|
||||
# it runs as root via pkexec) would block the next start with one of:
|
||||
#
|
||||
# * EBUSY on TUN device 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 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. We pkill both `mycelium` and
|
||||
# `mycelium-private` because the public-variant orphan would clash
|
||||
# on UDP/9650 and TCP/8990 just as readily.
|
||||
pkill -9 -x mycelium-private 2>/dev/null || true
|
||||
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-private "$@"
|
||||
@@ -13,7 +13,7 @@
|
||||
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">
|
||||
<action id="tech.threefold.mycellium-ui-private.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>
|
||||
@@ -23,6 +23,6 @@
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/mycellium-bootstrap</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/mycellium-bootstrap-private</annotate>
|
||||
</action>
|
||||
</policyconfig>
|
||||
@@ -285,10 +285,117 @@ fn default_key_path() -> Option<std::path::PathBuf> {
|
||||
dirs_like_app_data().ok().map(|d| d.join("priv_key.bin"))
|
||||
}
|
||||
|
||||
fn network_key_path_for(app: &AppHandle) -> AppResult<std::path::PathBuf> {
|
||||
use tauri::Manager;
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| AppError::TauriPath(e.to_string()))?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("network_key.bin"))
|
||||
}
|
||||
|
||||
// ─── Private network key ────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkKeyStatus {
|
||||
pub path: String,
|
||||
pub exists: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn network_key_status(app: AppHandle) -> AppResult<NetworkKeyStatus> {
|
||||
let path = network_key_path_for(&app)?;
|
||||
Ok(NetworkKeyStatus {
|
||||
path: path.display().to_string(),
|
||||
exists: path.exists(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a fresh 32-byte PSK using the OS RNG and write it to the
|
||||
/// canonical key location with mode 0600. Refuses to overwrite an
|
||||
/// existing file unless `overwrite=true`.
|
||||
#[tauri::command]
|
||||
pub fn network_key_generate(app: AppHandle, overwrite: bool) -> AppResult<NetworkKeyStatus> {
|
||||
use rand::RngCore;
|
||||
let path = network_key_path_for(&app)?;
|
||||
if path.exists() && !overwrite {
|
||||
return Err(AppError::BadInput(
|
||||
"network key already exists; pass overwrite=true to replace it".into(),
|
||||
));
|
||||
}
|
||||
let mut buf = [0u8; 32];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
write_key_0600(&path, &buf)?;
|
||||
Ok(NetworkKeyStatus {
|
||||
path: path.display().to_string(),
|
||||
exists: true,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn network_key_import(
|
||||
app: AppHandle,
|
||||
hex_key: String,
|
||||
overwrite: bool,
|
||||
) -> AppResult<NetworkKeyStatus> {
|
||||
let bytes = hex::decode(hex_key.trim()).map_err(|e| {
|
||||
AppError::BadInput(format!("invalid hex string: {e}"))
|
||||
})?;
|
||||
if bytes.len() != 32 {
|
||||
return Err(AppError::BadInput(format!(
|
||||
"network key must decode to exactly 32 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let path = network_key_path_for(&app)?;
|
||||
if path.exists() && !overwrite {
|
||||
return Err(AppError::BadInput(
|
||||
"network key already exists; pass overwrite=true to replace it".into(),
|
||||
));
|
||||
}
|
||||
write_key_0600(&path, &bytes)?;
|
||||
Ok(NetworkKeyStatus {
|
||||
path: path.display().to_string(),
|
||||
exists: true,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn network_key_export(app: AppHandle) -> AppResult<String> {
|
||||
let path = network_key_path_for(&app)?;
|
||||
let bytes = std::fs::read(&path).map_err(AppError::from)?;
|
||||
Ok(hex::encode(bytes))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn network_key_delete(app: AppHandle) -> AppResult<()> {
|
||||
let path = network_key_path_for(&app)?;
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path).map_err(AppError::from)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_key_0600(path: &std::path::Path, bytes: &[u8]) -> AppResult<()> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
.map_err(AppError::from)?;
|
||||
f.write_all(bytes).map_err(AppError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dirs_like_app_data() -> std::io::Result<std::path::PathBuf> {
|
||||
// We can't reach the AppHandle here, so we mirror Tauri's path:
|
||||
// $XDG_DATA_HOME/<identifier>/ or $HOME/.local/share/<identifier>/.
|
||||
let identifier = "tech.threefold.mycellium-ui";
|
||||
let identifier = "tech.threefold.mycellium-ui-private";
|
||||
if let Ok(d) = std::env::var("XDG_DATA_HOME") {
|
||||
return Ok(std::path::PathBuf::from(d).join(identifier));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ pub fn run() {
|
||||
commands::topic_forward_remove,
|
||||
commands::lookup_pubkey,
|
||||
commands::regenerate_identity,
|
||||
commands::network_key_status,
|
||||
commands::network_key_generate,
|
||||
commands::network_key_import,
|
||||
commands::network_key_export,
|
||||
commands::network_key_delete,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
mycellium_ui_lib::run();
|
||||
mycellium_ui_private_lib::run();
|
||||
}
|
||||
|
||||
@@ -16,27 +16,23 @@ const HEALTH_CHECK_TIMEOUT_SECS: u64 = 20;
|
||||
const HEALTH_CHECK_INTERVAL_MS: u64 = 400;
|
||||
const LOG_RING_CAPACITY: usize = 500;
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
/// All fields default to their natural empty/false value: a fresh app
|
||||
/// install has no peers, no TUN override, TUN enabled, and no network
|
||||
/// name set. The user is then guided to configure these in Settings
|
||||
/// before the daemon will accept a start.
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SidecarConfig {
|
||||
/// Bootstrap peers for the private overlay. Unlike the public app
|
||||
/// which seeds against `tcp://188.40.132.242:9651`, a private
|
||||
/// network has no Threefold-operated relay — the user must point
|
||||
/// at one or more nodes they control.
|
||||
pub peers: Vec<String>,
|
||||
pub tun_name: Option<String>,
|
||||
pub no_tun: bool,
|
||||
}
|
||||
|
||||
impl Default for SidecarConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// A small set of well-known public peers from the mycelium README,
|
||||
// used as bootstrap when the user hasn't configured their own.
|
||||
peers: vec![
|
||||
"tcp://188.40.132.242:9651".into(),
|
||||
"quic://[2a01:4f8:212:fa6::2]:9651".into(),
|
||||
],
|
||||
tun_name: None,
|
||||
no_tun: false,
|
||||
}
|
||||
}
|
||||
/// UTF-8 network identifier (2..=64 bytes). Public; not a secret.
|
||||
/// All nodes joining the same private overlay must agree on this.
|
||||
pub network_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Holds the running mycelium child process plus a small in-memory log
|
||||
@@ -120,9 +116,24 @@ impl SidecarHandle {
|
||||
return Err(AppError::DaemonAlreadyRunning);
|
||||
}
|
||||
|
||||
// A private network needs a name (1..=64 UTF-8 bytes) AND a 32-byte
|
||||
// pre-shared key. We surface a clear error rather than letting the
|
||||
// daemon fail with a less obvious message half-way through startup.
|
||||
let network_name = config.network_name.as_deref().unwrap_or("").trim();
|
||||
if network_name.is_empty() {
|
||||
return Err(AppError::BadInput(
|
||||
"private network name is required (Settings → Private network)".into(),
|
||||
));
|
||||
}
|
||||
if !(2..=64).contains(&network_name.len()) {
|
||||
return Err(AppError::BadInput(
|
||||
"private network name must be 2..=64 UTF-8 bytes".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let bin = locate_sidecar(app)?;
|
||||
// In a `.deb` install, our pre-install script ships
|
||||
// /usr/bin/mycellium-bootstrap that pkill+ip-link-del cleans
|
||||
// /usr/bin/mycellium-bootstrap-private 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.
|
||||
@@ -154,6 +165,15 @@ impl SidecarHandle {
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
let key_path = data_dir.join("priv_key.bin");
|
||||
let config_path = data_dir.join("mycelium.toml");
|
||||
let network_key_path = data_dir.join("network_key.bin");
|
||||
if !network_key_path.exists() {
|
||||
return Err(AppError::BadInput(
|
||||
format!(
|
||||
"network key file is missing at {} — generate or import one in Settings → Private network",
|
||||
network_key_path.display()
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
let mut args = vec![
|
||||
"--api-addr".to_string(),
|
||||
@@ -166,6 +186,10 @@ impl SidecarHandle {
|
||||
format!("127.0.0.1:{metrics_port}"),
|
||||
"--key-file".to_string(),
|
||||
key_path.display().to_string(),
|
||||
"--network-name".to_string(),
|
||||
network_name.to_string(),
|
||||
"--network-key-file".to_string(),
|
||||
network_key_path.display().to_string(),
|
||||
];
|
||||
if config_path.exists() {
|
||||
args.push("--config-file".to_string());
|
||||
@@ -312,11 +336,11 @@ 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`.
|
||||
/// present, we invoke it instead of the mycelium-private binary
|
||||
/// directly so the elevated context can clean up any orphan TUN /
|
||||
/// processes from a previous crash before `exec /usr/bin/mycelium-private`.
|
||||
fn bootstrap_path() -> Option<PathBuf> {
|
||||
let p = PathBuf::from("/usr/bin/mycellium-bootstrap");
|
||||
let p = PathBuf::from("/usr/bin/mycellium-bootstrap-private");
|
||||
p.exists().then_some(p)
|
||||
}
|
||||
|
||||
@@ -378,19 +402,20 @@ fn pick_port_skip(taken: &[u16]) -> AppResult<u16> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Resolve the bundled `mycelium` sidecar across our two build modes:
|
||||
/// Resolve the bundled `mycelium-private` sidecar across our two build
|
||||
/// modes:
|
||||
/// • `tauri dev` keeps the file under `src-tauri/binaries/` with the
|
||||
/// `-<target_triple>` suffix Tauri's externalBin convention requires.
|
||||
/// • `tauri build` for a `.deb` strips the suffix and places the
|
||||
/// binary at `/usr/bin/mycelium` next to the app launcher.
|
||||
/// binary at `/usr/bin/mycelium-private` next to the app launcher.
|
||||
/// We probe the bundled path first, then walk back to the dev location.
|
||||
fn locate_sidecar(app: &AppHandle) -> AppResult<PathBuf> {
|
||||
let triple = std::env::var("TAURI_ENV_TARGET_TRIPLE")
|
||||
.ok()
|
||||
.or_else(|| option_env!("TARGET").map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "x86_64-unknown-linux-gnu".to_string());
|
||||
let suffixed = format!("mycelium-{triple}");
|
||||
let plain = "mycelium".to_string();
|
||||
let suffixed = format!("mycelium-private-{triple}");
|
||||
let plain = "mycelium-private".to_string();
|
||||
let mut tried: Vec<PathBuf> = Vec::new();
|
||||
|
||||
// Bundled .deb / AppImage: the launcher lives next to the sidecar
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Mycellium UI",
|
||||
"productName": "Mycellium UI Private",
|
||||
"version": "0.1.0",
|
||||
"identifier": "tech.threefold.mycellium-ui",
|
||||
"identifier": "tech.threefold.mycellium-ui-private",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
@@ -14,7 +14,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Mycellium",
|
||||
"title": "Mycellium Private",
|
||||
"width": 1100,
|
||||
"height": 720,
|
||||
"minWidth": 800,
|
||||
@@ -30,20 +30,20 @@
|
||||
"active": true,
|
||||
"targets": ["deb", "appimage"],
|
||||
"category": "Utility",
|
||||
"shortDescription": "Mycelium overlay network client",
|
||||
"longDescription": "Desktop GUI for the Mycelium end-to-end encrypted IPv6 overlay network.",
|
||||
"shortDescription": "Mycelium private network client",
|
||||
"longDescription": "Desktop GUI for joining a private Mycelium overlay network — a self-contained IPv6 mesh isolated from the public Mycelium network by a shared 32-byte key.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png"
|
||||
],
|
||||
"externalBin": ["binaries/mycelium"],
|
||||
"externalBin": ["binaries/mycelium-private"],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["pkexec | policykit-1"],
|
||||
"files": {
|
||||
"/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy": "packaging/polkit/tech.threefold.mycellium-ui.policy",
|
||||
"/usr/bin/mycellium-bootstrap": "packaging/mycellium-bootstrap"
|
||||
"/usr/share/polkit-1/actions/tech.threefold.mycellium-ui-private.policy": "packaging/polkit/tech.threefold.mycellium-ui-private.policy",
|
||||
"/usr/bin/mycellium-bootstrap-private": "packaging/mycellium-bootstrap-private"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user