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:
syoul
2026-04-27 01:35:11 +02:00
parent 5229e2c774
commit 8b83fc10d5
22 changed files with 610 additions and 183 deletions

View File

@@ -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));
}

View File

@@ -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");

View File

@@ -1,5 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
mycellium_ui_lib::run();
mycellium_ui_private_lib::run();
}

View File

@@ -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