diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1a14fe9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to this fork are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), version numbers follow [Semantic Versioning](https://semver.org/). + +This fork tracks [`syoul/Mycell-UI`](https://git.open.us.org/syoul/Mycell-UI) (the public-network variant) as `upstream`. Bugfixes from upstream are cherry-picked; this changelog only records changes specific to the private-network track. + +## [Unreleased] + +## [0.1.0] — 2026-04-27 + +Forked from `Mycell-UI@5229e2c` ("feat(packaging): pre-spawn cleanup wrapper for clean restarts"). + +### Added +- Bundling of the `mycelium-private` daemon (separate upstream release tarball — `mycelium-private-{triple}-unknown-linux-musl.tar.gz`) instead of the public `mycelium`. +- `SidecarConfig.networkName` field (UTF-8, 2..=64 bytes — public, agreed across all nodes of the same overlay). +- Network key management: `network_key_status`, `network_key_generate`, `network_key_import`, `network_key_export`, `network_key_delete` Tauri commands. Keys are 32 random bytes from the OS RNG, stored at `app_data_dir/network_key.bin` with mode `0600`. +- "Private network" section in **Settings** at the top: name input, generate / reveal-hex / import / delete buttons, configured / missing status badge. +- Pre-flight check in `sidecar::start`: refuses to spawn the daemon without a non-empty network name AND an existing key file. Surfaces a clear error rather than letting `mycelium-private` fail mid-startup. +- `mycellium-bootstrap-private` cleanup wrapper kills both `mycelium` and `mycelium-private` orphans on each spawn, since either would block UDP/9650 (multicast discovery) and TCP/8990 (hardcoded JSON-RPC port). + +### Changed +- App identifier: `tech.threefold.mycellium-ui` → `tech.threefold.mycellium-ui-private`. +- Cargo crate name + lib name: `mycellium_ui_lib` → `mycellium_ui_private_lib`. +- Display: `Mycellium UI` → `Mycellium UI Private`. Sidebar shows "Mycellium **Private**" with an amber accent. +- `.deb` package name: `mycellium-ui` → `mycellium-ui-private`. Distinct binary at `/usr/bin/mycellium-ui-private`. The two apps coexist on the same machine. +- polkit action ID: `tech.threefold.mycellium-ui.bootstrap` → `tech.threefold.mycellium-ui-private.bootstrap`. Path annotation points at `/usr/bin/mycellium-bootstrap-private`. +- Default peer list is now empty: a private network has no Threefold-operated seed; the user must explicitly add bootstrap peers they control. +- The "Static peers" textarea hint in **Settings** changed from "tcp://188.40.132.242:9651" to "tcp://your-node.example.org:9651". +- README rewritten to focus on the private-network model, key distribution flow, and the divergence policy from upstream. + +### Removed +- The two public Threefold seed peers from `SidecarConfig::default()` (TCP 188.40.132.242:9651 and QUIC [2a01:4f8:212:fa6::2]:9651). + +### Inherited from upstream `Mycell-UI@5229e2c` +- Sidecar lifecycle via pkexec with elevated cleanup wrapper (`auth_admin_keep` cached per session). +- Ephemeral ports for the REST API, peer-listen TCP/QUIC, and Prometheus metrics endpoint. +- reqwest connection pool disabled (`pool_max_idle_per_host(0)`) to dodge stale-connection errors on long-running sessions. +- Short-poll inbox loop (workaround for the upstream HTTP serialization bug — see `docs-syoul/upstream-bug-http-serialization.md` in the public repo). +- Status page surfaces the full overlay IPv6 (parsed from the daemon's `Node overlay IP:` log line) in addition to the `/64` subnet. +- `pkexec | policykit-1` Depends alternative for compatibility with both Debian 12 (bookworm) and Debian 13 (trixie). + +[Unreleased]: https://git.open.us.org/syoul/Mycell-UI-Private/compare/v0.1.0...HEAD +[0.1.0]: https://git.open.us.org/syoul/Mycell-UI-Private/releases/tag/v0.1.0 diff --git a/README.md b/README.md index b70a28b..6ee3ce0 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,26 @@ -# mycellium-ui +# mycellium-ui-private -Cross-platform desktop GUI for [Mycelium](https://github.com/threefoldtech/mycelium) — Threefold's end-to-end encrypted IPv6 overlay network. +Desktop GUI for joining a **private** [Mycelium](https://github.com/threefoldtech/mycelium) overlay network — a self-contained IPv6 mesh isolated from the public Mycelium network by a shared 32-byte key. -The app embeds the official `mycelium` binary as a Tauri sidecar and pilots it through its HTTP API on a loopback ephemeral port. Root privileges (required to create the TUN interface) are obtained via `pkexec`. +This is a fork of [`mycellium-ui`](https://git.open.us.org/syoul/Mycell-UI) (the public-network variant). Code structure is identical; only the bundled daemon (`mycelium-private` instead of `mycelium`), the default config (no public seed peer), and a new "Private network" panel in Settings differ. + +The two apps are designed to **coexist** on the same machine: distinct app identifier (`tech.threefold.mycellium-ui-private`), distinct binary (`/usr/bin/mycellium-ui-private`), distinct polkit action. + +## What's a private network? + +Mycelium 0.6.1 ships an opt-in mode where every packet is wrapped under an additional symmetric key. Two nodes only see each other if: + +1. They run the `mycelium-private` daemon (not the public `mycelium`). +2. They were both started with the same `--network-name` (UTF-8, 2–64 bytes — public, e.g. `acme-corp-private`). +3. They were both started with `--network-key-file` pointing at the same 32-byte secret. + +Without the right name+key, peers reject each other at the handshake. There's no Threefold-operated bootstrap node for private networks — the operator distributes the key out-of-band and brings up at least one reachable peer (typically a VPS) that other nodes can dial. ## Status -v1, Linux-only. Implements the full `docs/api.yaml` surface of mycelium v0.6.1: admin, peers (CRUD), routes (selected/fallback/queried), messages (send/receive/reply/status), topics (default + whitelist + sources + forward), pubkey lookup. +v0.1, Linux-only. Same UI surface as the public variant. **Marked experimental upstream** (`docs/private_network.md` in the mycelium repo). -## Architecture - -``` -┌──────────────────────────────────────────────────────────────┐ -│ WebView (Vue 3 + TS + Tailwind + radix-vue + Pinia) │ -│ Status / Peers / Routes / Messages / Topics / Settings │ -└────────────────┬─────────────────────────────────────────────┘ - │ invoke() / Tauri events -┌────────────────┴─────────────────────────────────────────────┐ -│ Tauri core (Rust, tokio + reqwest) │ -│ • sidecar.rs — supervises mycelium via pkexec │ -│ • api/* — typed REST client │ -│ • poller.rs — emits peers://, stats://, routes://, messages://incoming │ -└────────────────┬─────────────────────────────────────────────┘ - │ HTTP loopback :ephemeral -┌────────────────┴─────────────────────────────────────────────┐ -│ mycelium daemon (sidecar binary, runs as root via pkexec) │ -│ TUN0 ◄─► overlay network │ -└──────────────────────────────────────────────────────────────┘ -``` - -There is no Unix socket / named pipe IPC — the daemon's own HTTP API is the integration point. - -## Prerequisites (Debian / Ubuntu) +## Setup (dev) ```bash sudo apt install -y \ @@ -40,72 +29,56 @@ sudo apt install -y \ build-essential curl wget file libssl-dev libgtk-3-dev libxdo-dev \ pkg-config policykit-1 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -``` -Then Node 20+ and pnpm 10+. - -## Setup - -```bash -# 1. Install JS deps pnpm install - -# 2. Fetch the mycelium sidecar binary for your target triple -bash scripts/fetch-mycelium.sh # uses MYCELIUM_VERSION (default v0.6.1) -# or: MYCELIUM_VERSION=v0.6.1 bash scripts/fetch-mycelium.sh - -# 3. Run in dev +bash scripts/fetch-mycelium.sh # downloads mycelium-private v0.6.1 pnpm tauri dev ``` -The first start triggers a `pkexec` dialog asking you to authenticate; the polkit policy installed by the `.deb` caches the auth for the user session. +On first launch: -## Build +1. Open **Settings → Private network**. +2. Type a network name (e.g. `acme-corp-private`). +3. Click **Generate 32-byte key** (or **Import** a key already shared with you). +4. Click **Reveal hex** and share the hex string out-of-band with your other nodes. +5. Save daemon configuration (add at least one bootstrap peer). +6. Click **Start daemon** in the sidebar. + +Other nodes paste the same hex into their **Import** field and use the same network name. + +## Build a `.deb` ```bash -pnpm tauri build # → src-tauri/target/release/bundle/{deb,appimage}/ +pnpm tauri build --bundles deb +# → src-tauri/target/release/bundle/deb/Mycellium UI Private_*.deb ``` -The `.deb` declares `Depends: policykit-1` and ships the polkit policy under `/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy`. The AppImage relies on `pkexec` being present on the host — on systems without polkit, fall back to running with `sudo` after disabling the sidecar's pkexec wrapper. +The `.deb` declares `Depends: pkexec | policykit-1` (Debian 12 + 13 covered) and ships: +- `/usr/bin/mycellium-ui-private` (GUI) +- `/usr/bin/mycelium-private` (daemon) +- `/usr/bin/mycellium-bootstrap-private` (cleanup wrapper for orphan handling) +- `/usr/share/polkit-1/actions/tech.threefold.mycellium-ui-private.policy` -## Layout +## Diverging from upstream -``` -src/ # Vue 3 frontend - views/ # one file per nav item - components/ # shadcn-style UI primitives + dialogs - stores/ # Pinia: node, peers, routes, messages, topics, config - lib/ # api wrapper, events, base64 + format helpers -src-tauri/ - src/ - sidecar.rs # spawn + supervise mycelium - elevation.rs # pkexec command builder - poller.rs # 3 background loops (peers, routes, inbox long-poll) - api/ # REST client modules (admin, peers, routes, - # messages, topics, pubkey) - commands.rs # #[tauri::command] handlers, 1:1 with REST - error.rs # AppError + Serialize-as-string for invoke() - binaries/ # gitignored; populated by scripts/fetch-mycelium.sh - packaging/polkit/ # XML policy bundled into the .deb -scripts/fetch-mycelium.sh -.github/workflows/ci.yml # pnpm typecheck + cargo fmt/clippy/test +This fork tracks `Mycell-UI` upstream as `origin/upstream` (after running the `git remote add upstream ...` step). To pull bugfixes: + +```bash +git fetch upstream +git cherry-pick ``` -## Verification matrix +Most fixes will apply cleanly because the only divergence is in: +- `src-tauri/src/sidecar.rs` (network_name + key_file args) +- `src-tauri/src/commands.rs` (network_key_* commands) +- `src-tauri/packaging/` (different bootstrap script + polkit policy) +- `src-tauri/tauri.conf.json` (productName, identifier, externalBin) +- `src/views/Settings.vue` (Private network section) +- `src/stores/config.ts` (defaults) -| Test | How | -|------|-----| -| Sidecar starts under pkexec | `pnpm tauri dev`, daemon visible in `ps`, splash disappears in <10 s | -| Peers connect | Add `tcp://188.40.132.242:9651` from the Peers page; state turns to `alive` within ~10 s | -| Routes propagate | `Routes/Selected` becomes non-empty after ~30 s | -| Live event stream | Sidebar status dot tracks ready/idle, peers table updates without manual refresh | -| Bidirectional messages | Two instances on different VMs, exchange via Compose → Inbox | -| Identity regen | Settings → Regenerate; restart daemon; new IP appears on Status | -| `.deb` install | Fresh Ubuntu LTS / Debian 12; daemon spawns under polkit on first start | +## Known limitations (v0.1) -## Known limitations (v1) - -- Linux only. Windows is reachable (sidecar via `runas` / Wintun driver) but not implemented. -- Auto-start at login isn't wired — the desktop entry installed by the `.deb` is the manual launcher. -- The TOML config editor in Settings only exposes `peers`, `tunName`, `noTun`. Other keys (`metricsApiAddress`, etc.) are passed-through if you edit the file directly at `~/.local/share/tech.threefold.mycellium-ui/mycelium.toml` and restart the daemon. -- `message_status` is forwarded as opaque JSON; the upstream schema isn't pinned in the spec, so we don't strongly type it. +- Linux only. +- The `mycelium-private` binary is upstream's experimental track; the API may shift in a future release. +- Network keys are stored in `$XDG_DATA_HOME/tech.threefold.mycellium-ui-private/network_key.bin` with mode `0600`. Not encrypted at rest — host-level disk encryption is the only layer. +- No multi-network support — one private overlay at a time. diff --git a/index.html b/index.html index 1276439..565286c 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Mycellium UI + Mycellium Private
diff --git a/package.json b/package.json index b349c82..d18abf4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "mycellium-ui", + "name": "mycellium-ui-private", "private": true, "version": "0.1.0", "type": "module", diff --git a/release/README.md b/release/README.md index 8fb6ed0..61cb26c 100644 --- a/release/README.md +++ b/release/README.md @@ -1,16 +1,16 @@ -# Releases +# Releases — Mycellium UI Private -Pre-built `.deb` of the mycellium-ui desktop client. Tested on Debian 12 (bookworm); should work on any apt-based distro shipping `libwebkit2gtk-4.1-0` (Ubuntu 24.04+, Debian 12+). +Pre-built `.deb` of the private-network desktop client. Designed to coexist with the public-network variant `mycellium-ui` on the same machine (different identifier, different binary, different polkit action). ## Install ```bash -sudo apt install ./mycellium-ui_0.1.0_amd64.deb +sudo apt install ./mycellium-ui-private_0.1.0_amd64.deb ``` -`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. +`apt install` with a local path resolves runtime deps (`pkexec | policykit-1`, `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). +Tested on Debian 12 (bookworm) and Debian 13 (trixie). ## Verify @@ -22,13 +22,28 @@ sha256sum -c SHA256SUMS | Path | Purpose | |---|---| -| `/usr/bin/mycellium-ui` | GUI launcher | -| `/usr/bin/mycelium` | Mycelium daemon (v0.6.1, runs as root via pkexec) | -| `/usr/share/polkit-1/actions/tech.threefold.mycellium-ui.policy` | polkit action — auth cached per session (`auth_admin_keep`) | -| `/usr/share/applications/Mycellium UI.desktop` | Menu entry | +| `/usr/bin/mycellium-ui-private` | GUI launcher | +| `/usr/bin/mycelium-private` | Mycelium daemon (private-network track, v0.6.1, runs as root via pkexec) | +| `/usr/bin/mycellium-bootstrap-private` | Cleanup wrapper invoked by pkexec | +| `/usr/share/polkit-1/actions/tech.threefold.mycellium-ui-private.policy` | polkit action — auth cached per session (`auth_admin_keep`) | +| `/usr/share/applications/Mycellium UI Private.desktop` | Menu entry | + +## First run + +1. Open the app — sidebar pastille is grey (idle). +2. Go to **Settings → Private network** at the top. +3. Type a network name (UTF-8, 2–64 bytes — public, agreed with your peers). +4. Click **Generate 32-byte key**, then **Reveal hex** to copy and share with the other nodes through a secure channel. +5. Other nodes paste the hex into their **Import** field. +6. In Daemon configuration, add at least one bootstrap peer. +7. Click **Start daemon** in the sidebar — pkexec prompt the first time. + +Without a network name AND a key file, the daemon refuses to start (the app surfaces a clear error). ## Uninstall ```bash -sudo apt remove mycellium-ui +sudo apt remove mycellium-ui-private ``` + +The user data (identity key, network key) under `~/.local/share/tech.threefold.mycellium-ui-private/` is preserved across reinstall. Remove it manually if you want a fresh start. diff --git a/release/SHA256SUMS b/release/SHA256SUMS index d1400f6..85aa4ae 100644 --- a/release/SHA256SUMS +++ b/release/SHA256SUMS @@ -1 +1 @@ -38e0a3d03797490e1d6caa2681f9d5580493908834736b0db6a7d99558bb8823 release/mycellium-ui_0.1.0_amd64.deb +fe9e98ff6b2ee740345a9cac5bc1fd828cf7b53c96242c0c15fdc26dd8249b4e release/mycellium-ui-private_0.1.0_amd64.deb diff --git a/release/mycellium-ui_0.1.0_amd64.deb b/release/mycellium-ui-private_0.1.0_amd64.deb similarity index 58% rename from release/mycellium-ui_0.1.0_amd64.deb rename to release/mycellium-ui-private_0.1.0_amd64.deb index 2506c93..44e2ac3 100644 Binary files a/release/mycellium-ui_0.1.0_amd64.deb and b/release/mycellium-ui-private_0.1.0_amd64.deb differ diff --git a/scripts/fetch-mycelium.sh b/scripts/fetch-mycelium.sh index fe43259..d83d712 100755 --- a/scripts/fetch-mycelium.sh +++ b/scripts/fetch-mycelium.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# Fetches the official mycelium release binary and places it in src-tauri/binaries/ -# with the target-triple suffix expected by Tauri's externalBin bundler. +# Fetches the official mycelium-private release binary and places it in +# src-tauri/binaries/ with the target-triple suffix expected by Tauri's +# externalBin bundler. # # Usage: scripts/fetch-mycelium.sh [VERSION] # VERSION defaults to MYCELIUM_VERSION below. @@ -28,8 +29,8 @@ detect_target_triple() { # Map our target triple to the asset name pattern used by upstream releases. asset_for_triple() { case "$1" in - x86_64-unknown-linux-gnu) echo "mycelium-x86_64-unknown-linux-musl.tar.gz" ;; - aarch64-unknown-linux-gnu) echo "mycelium-aarch64-unknown-linux-musl.tar.gz" ;; + x86_64-unknown-linux-gnu) echo "mycelium-private-x86_64-unknown-linux-musl.tar.gz" ;; + aarch64-unknown-linux-gnu) echo "mycelium-private-aarch64-unknown-linux-musl.tar.gz" ;; *) echo "unsupported triple: $1" >&2; exit 1 ;; esac } @@ -42,23 +43,22 @@ TMP_DIR="$(mktemp -d)" trap 'rm -rf "${TMP_DIR}"' EXIT echo "→ downloading ${URL}" -curl -fsSL "${URL}" -o "${TMP_DIR}/mycelium.tar.gz" +curl -fsSL "${URL}" -o "${TMP_DIR}/mycelium-private.tar.gz" echo "→ extracting" -tar -xzf "${TMP_DIR}/mycelium.tar.gz" -C "${TMP_DIR}" +tar -xzf "${TMP_DIR}/mycelium-private.tar.gz" -C "${TMP_DIR}" -# The archive contains a single 'mycelium' binary at the root. -SRC="${TMP_DIR}/mycelium" +# The archive contains a single 'mycelium-private' binary at the root. +SRC="${TMP_DIR}/mycelium-private" if [[ ! -f "${SRC}" ]]; then - # Some releases nest the binary; find it. - SRC="$(find "${TMP_DIR}" -name 'mycelium' -type f -executable | head -n1)" + SRC="$(find "${TMP_DIR}" -name 'mycelium-private' -type f -executable | head -n1)" fi if [[ -z "${SRC}" || ! -f "${SRC}" ]]; then - echo "could not locate mycelium binary in archive" >&2 + echo "could not locate mycelium-private binary in archive" >&2 exit 1 fi -DEST="${DEST_DIR}/mycelium-${TRIPLE}" +DEST="${DEST_DIR}/mycelium-private-${TRIPLE}" install -m 0755 "${SRC}" "${DEST}" echo "✓ installed ${DEST}" echo " version: ${MYCELIUM_VERSION}" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2bba32a..9b98ebc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6ab49ed..97cf8f1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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"] diff --git a/src-tauri/packaging/mycellium-bootstrap b/src-tauri/packaging/mycellium-bootstrap deleted file mode 100755 index 2618eaf..0000000 --- a/src-tauri/packaging/mycellium-bootstrap +++ /dev/null @@ -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 "$@" diff --git a/src-tauri/packaging/mycellium-bootstrap-private b/src-tauri/packaging/mycellium-bootstrap-private new file mode 100755 index 0000000..a938988 --- /dev/null +++ b/src-tauri/packaging/mycellium-bootstrap-private @@ -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 "$@" diff --git a/src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy b/src-tauri/packaging/polkit/tech.threefold.mycellium-ui-private.policy similarity index 92% rename from src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy rename to src-tauri/packaging/polkit/tech.threefold.mycellium-ui-private.policy index 28c84a0..f9bd590 100644 --- a/src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy +++ b/src-tauri/packaging/polkit/tech.threefold.mycellium-ui-private.policy @@ -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. --> - + Run the Mycelium overlay daemon Lancer le démon de l'overlay Mycelium Authentication is required to start the Mycelium overlay daemon. @@ -23,6 +23,6 @@ auth_admin auth_admin_keep - /usr/bin/mycellium-bootstrap + /usr/bin/mycellium-bootstrap-private diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 08b74bc..51b98ac 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -285,10 +285,117 @@ fn default_key_path() -> Option { dirs_like_app_data().ok().map(|d| d.join("priv_key.bin")) } +fn network_key_path_for(app: &AppHandle) -> AppResult { + 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 { + 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 { + 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 { + 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 { + 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 { // We can't reach the AppHandle here, so we mirror Tauri's path: // $XDG_DATA_HOME// or $HOME/.local/share//. - 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)); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ac08af9..92429f5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 97fdb44..cc39089 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - mycellium_ui_lib::run(); + mycellium_ui_private_lib::run(); } diff --git a/src-tauri/src/sidecar.rs b/src-tauri/src/sidecar.rs index f54aee8..7d8cce3 100644 --- a/src-tauri/src/sidecar.rs +++ b/src-tauri/src/sidecar.rs @@ -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, pub tun_name: Option, 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, } /// 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 { - 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 { )) } -/// 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 /// `-` 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 { 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 = Vec::new(); // Bundled .deb / AppImage: the launcher lives next to the sidecar diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 40f7819..05c1cdb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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" } } } diff --git a/src/App.vue b/src/App.vue index 115cf5d..a4c1d56 100644 --- a/src/App.vue +++ b/src/App.vue @@ -30,7 +30,7 @@ const navItems = [ { to: "/settings", label: "Settings", icon: SettingsIcon }, ]; -const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium"); +const currentTitle = computed(() => (route.meta?.title as string) ?? "Mycellium Private"); onMounted(async () => { await config.load(); @@ -60,7 +60,7 @@ async function handleStop() { >