Commit Graph

15 Commits

Author SHA1 Message Date
syoul
5229e2c774 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.
2026-04-26 02:27:07 +02:00
syoul
0c9277f687 feat(ui): expose full overlay IP on Status page
The /api/v1/admin endpoint only returns the /64 subnet. Users were
copy-pasting the subnet straight into Compose Message and getting
HTTP 422 from the daemon (\"invalid IP address syntax\"). The full
host part (lower 64 bits) is logged once at boot:

    INFO mycelium: Node overlay IP: 43d:956e:7877:d933:eecc:b305:21ff:77f9

Capture it from stdout, surface as a new daemonStatus.overlayIp
field, and render it on Status above the subnet card with the hint
\"use this when sending messages\".

The line carries ANSI colour escapes from tracing's compact format,
so push_log strips SGR sequences before scanning. Hand-rolled to
avoid pulling a regex crate.

Also rebuilds the .deb release artifact.
2026-04-26 02:11:42 +02:00
syoul
3bf3cd162b release: rebuild .deb with cross-distro pkexec dependency
On Debian 13 (trixie), policykit-1 was retired and pkexec moved
into a standalone package. The previous Depends: policykit-1
made `apt install` fail with \"none of the choices are installable\"
on a fresh trixie VM.

Switch to Depends: pkexec | policykit-1 so apt can satisfy the
constraint on both:
  - bookworm: policykit-1 (which provides /usr/bin/pkexec)
  - trixie:   pkexec (standalone)

Also updates SHA256SUMS and the install README accordingly.
2026-04-26 01:47:20 +02:00
syoul
a31a40a477 fix(sidecar): probe installed and dev paths for the binary
dpkg-deb -c on the bundled .deb shows mycelium at /usr/bin/mycelium
(no triple suffix), next to the app launcher. Our previous resolver
only looked for the suffixed dev name in resource_dir, so the
installed app could not find its sidecar.

Probe order is now: directory of current_exe, then resource_dir,
then CARGO_MANIFEST_DIR/binaries, then $PATH, with both the plain
"mycelium" name and the dev-style "mycelium-<triple>" alias
checked at each location.
2026-04-26 01:09:44 +02:00
syoul
939565b88a fix(poller): short-poll inbox instead of 30s long-poll
The 10s-after-healthy failure pattern was reproducing even with
the connection pool disabled. Smoking gun: the inbox loop opens
GET /messages?timeout=30 right after start_daemon returns, and
every subsequent peers/routes call timed out exactly when our
client-side reqwest timeout (10s) fired.

Concluded mycelium 0.6.1's HTTP server serialises requests: while
the long-poll connection is held, no other admin endpoint can
respond. The sidecar process kept logging routes the whole time
(seen in the in-app log buffer) — proof the daemon was alive,
just unable to serve concurrent calls.

Switch to short-poll: timeout=0 returns immediately, sleep 2s
between iterations. Per-iteration server hold time is now
millisecond-scale instead of 30s.
2026-04-26 00:32:58 +02:00
syoul
7981fc571c fix(api): disable reqwest connection pool
Direct mycelium runs and our pkexec spawns are both healthy
(sidecar logs show acquired routes streaming for 20+s). Yet our
reqwest poller can't reach 127.0.0.1:port after the first
successful request. Smoking gun: failure happens ~10s after the
first reply — exactly when an idle keep-alive connection would
have been reaped.

Disable pooling (pool_max_idle_per_host(0)) so every call opens a
fresh TCP connection. Loopback overhead is negligible (~50us per
request) and we're immune to server-side idle closes. Also pin
connect_timeout to 3s so a wedged half-open doesn't block for
the full 10s request timeout.
2026-04-26 00:18:59 +02:00
syoul
45174ebe7d fix(sidecar): pin metrics-api-address to ephemeral port
Even with --api-addr set, mycelium also opens an internal JSON-RPC
endpoint on 127.0.0.1:8990 by default (visible in --debug as
\"Starting JSON-RPC server listen_addr=127.0.0.1:8990\"). When a
previous run left an orphan mycelium running as root — which we
can't SIGKILL from a user-level Child::kill_on_drop — the new
instance fails to bind 8990 and exits ~10s after startup, with no
sidecar://exited event because the kill never landed.

Pin --metrics-api-address to a fresh ephemeral port alongside the
other three. Known limitation: stopping the daemon still doesn't
reliably kill the root child; the user has to `sudo pkill mycelium`
between runs. Will be addressed by an elevated-shutdown hook.
2026-04-26 00:06:50 +02:00
syoul
2cd14f06ae fix(sidecar): use ephemeral ports for tcp/quic listen too
mycelium defaults --tcp-listen-port and --quic-listen-port to 9651
when not provided. If anything else holds 9651 (a previous test
instance, a Docker container from the level-1 procedure, another
mycelium running on the host), the daemon comes up far enough to
serve the loopback API for a few seconds before tearing itself
down on the listen failure.

Pick three distinct ephemeral ports (api, tcp, quic) per spawn.
Trade-off: inbound peers need the actual port number, which we
already log; the user can pin via a future SidecarConfig field.
2026-04-25 23:51:35 +02:00
syoul
4dd278e62a fix: remove tauri-plugin-log to avoid logger conflict
`tracing_subscriber::fmt().try_init()` installs a global log
implementation via the tracing-log shim. tauri-plugin-log then
calls `log::set_logger()` which panics with "attempted to set a
logger after the logging system was already initialized".

The plugin was never wired in JS code (no @tauri-apps/plugin-log
import) — backend logging via tracing is sufficient. Drop:
  - tauri-plugin-log dep + plugin registration
  - log:default capability permission
  - @tauri-apps/plugin-log JS dep

Verified: cargo check is clean, app starts past the panic.
2026-04-25 23:35:34 +02:00
syoul
eb86fdd182 P5: settings, persistence, polkit packaging, README
Backend
- regenerate_identity command stops the daemon, deletes
  priv_key.bin, leaves the user to restart for a fresh identity;
  falls back to the canonical XDG path when sidecar.key_path()
  isn't populated yet
- tauri.conf.json ships the polkit policy via deb.files mapping;
  src-tauri/packaging/polkit/tech.threefold.mycellium-ui.policy
  declares the spawn action with auth_admin_keep so the dialog
  appears once per session

Frontend
- config store persists SidecarConfig (peers, tunName, noTun)
  through tauri-plugin-store; App.vue reads it and forwards to
  start_daemon, replacing the hard-coded defaults
- Settings view: daemon-config form, identity panel with the
  destructive regenerate button, sidecar log viewer, About
- README rewritten end-to-end: HTTP-loopback architecture, polkit
  install path, build commands, verification matrix, and a
  honest "known limitations" section
2026-04-25 23:15:35 +02:00
syoul
f28d0e1338 P4: messages, topics, pubkey
Backend
- api/messages.rs covers send/pop/reply/status with an externally
  tagged MessageDestination enum that matches the daemon's
  {ip|pk: ...} body shape; pop_message uses an inflated request
  timeout to outlast the long-poll window
- api/topics.rs implements default action, topic CRUD, sources
  whitelist, and forward-socket get/set/remove. POST /topics ships
  the raw base64 string as the body (not JSON); path segments are
  percent-encoded inline (topics contain '/' and '+')
- api/pubkey.rs resolves an overlay IPv6 to a hex public key
- poller spawns a third long-poll loop on /messages?peek=false
  that fans every inbound message into a 200-deep ring buffer and
  emits messages://incoming for the UI

Frontend
- messages store: live inbox via the event, persisted outbox via
  tauri-plugin-store keyed under outbox.json
- ComposeMessage form: ip/pk toggle, optional UTF-8 topic and
  payload that get base64-encoded with a TextEncoder-based helper
- MessageList renders printable payloads decoded; binary payloads
  fall back to a "(N bytes binary)" hint
- Topics view: split layout with whitelist on the left, per-topic
  sources/forward editor on the right; default-action toggle is
  surfaced at the top
2026-04-25 23:10:21 +02:00
syoul
95e7cb4bd3 P3: routes (selected, fallback, queried)
Backend
- api/routes.rs models the Babel-style route shape; metric uses an
  untagged enum to round-trip both numeric hop counts and the
  literal "infinite" string the daemon emits for poisoned routes
- routes_snapshot() runs the three GETs concurrently with try_join
  so the snapshot is internally consistent
- poller spawns a second 5s loop emitting routes://updated; both
  loops are owned by the Poller and aborted together on stop_daemon

Frontend
- routes store mirrors the snapshot shape; tabbed view (radix-vue)
  with selected, fallback and queried lists
- RouteTable component shared by selected/fallback; metric column
  is colour-coded (0 green, low neutral, high yellow, infinite red)
- Queried subnets show a live `expires in 12s` countdown driven by
  a 1Hz tick ref instead of mutating the store
2026-04-25 23:02:32 +02:00
syoul
c1a81a9065 P2: peers CRUD and aggregated stats
Backend
- api/peers.rs: list/add/remove + aggregate() that derives totals,
  per-state counts, and tx/rx sums in one pass over the peer list
- poller.rs spawns a 3s tokio loop that emits peers://updated and
  stats://updated; cancelled via abort() on stop_daemon
- DELETE peer URL-encodes the endpoint (the path includes ://) with
  a small inline percent-encoder to avoid a url crate dep
- Tauri commands: peers_list, peer_add (with empty-string guard),
  peer_remove, peers_stats

Frontend
- peers store subscribes to the two events and refreshes after
  add/remove for immediate UI feedback
- Peers view renders endpoint, type, color-coded state badge, and
  formatBytes-formatted rx/tx; the four stat cards re-use a
  reusable Stat component
- AddPeerDialog uses radix-vue's Dialog primitive with regex
  validation for tcp:// and quic:// schemes
2026-04-25 22:56:50 +02:00
syoul
d737231123 P1: sidecar lifecycle and HTTP bridge
Backend
- sidecar.rs supervises the bundled `mycelium` binary launched via
  pkexec; locates it in resource_dir or CARGO_MANIFEST_DIR/binaries
  matching $TAURI_ENV_TARGET_TRIPLE
- ephemeral port via portpicker, key + config persisted in
  app_data_dir, kill_on_drop with explicit start_kill on stop
- health-check loop calls /api/v1/admin until 2xx (timeout 20s);
  emits sidecar://ready and sidecar://exited
- 500-line ring buffer of stdout/stderr surfaced via sidecar_logs
  command for the upcoming Settings page
- elevation::is_auth_failure(126|127) maps pkexec cancel to a
  dedicated AppError variant
- AppError uses thiserror, Serialize impl renders messages as
  plain strings for the JS side

Frontend
- typed `api` wrapper around invoke() in src/lib/api.ts
- node store (Pinia) bootstraps on mount, listens on
  sidecar://ready and sidecar://exited
- StartupOverlay covers the whole window for idle/starting/error
  phases; sidebar status dot + start/stop button
- Status view renders subnet, pubkey, api endpoint and key path
  with one-click clipboard copy
2026-04-25 22:45:52 +02:00
syoul
d79300caf8 P0: scaffold Vite + Vue 3 + Tauri v2
- pnpm + TS + Tailwind 3 + Pinia + Vue Router with hash history
- 6 placeholder views (Status, Peers, Routes, Messages, Topics, Settings)
  rendered via lucide-icon sidebar in App.vue
- Tauri v2: shell, store, log, dialog plugins; bundle targets deb +
  appimage; sidecar wired via externalBin = binaries/mycelium
- scripts/fetch-mycelium.sh pins v0.6.1, maps musl asset onto
  gnu target triple expected by Tauri bundler
- CI: pnpm typecheck + cargo fmt/clippy/test
2026-04-25 22:12:48 +02:00