Commit Graph

6 Commits

Author SHA1 Message Date
xah30 1f82bc41c0 feat(cli,aura-gui): v3.4.4 — graceful Shutdown via admin socket (#1)
Closes the long-standing "GUI Disconnect button doesn't actually kill aura"
bug. The previous kill path sent SIGTERM to sudo (our direct child) and
hoped sudo's signal forwarding would propagate to the aura child running
as root; in practice this is unreliable when the parent has no controlling
terminal (which Tauri-spawned children don't), so aura would survive the
"Disconnect" click with the TUN still up and the OS routes still installed.

## Implementation

Adds a `Shutdown` admin-socket request. The aura-cli main loops
(`client::run` and `server::run`) now `tokio::select!` between their normal
work (router.run() / accept loop) and a `tokio::sync::Notify` carried on
the shared `AdminState`. When an admin client posts `{"cmd":"shutdown"}`
the handler calls `state.shutdown.notify_one()`, the select! second arm
fires, the work future is dropped, `OsRouteGuard::Drop` rolls back the
installed system routes, and the process exits with `Ok(())` — clean exit
code 0, kernel reaps the TUN device, no orphan.

The whole round-trip is sub-500 ms in practice (the slow step is the
`route delete` invocations on macOS).

## What changed

* `aura-cli/src/admin.rs`: `Request::Shutdown` variant, `AdminState.shutdown:
  Arc<Notify>` field, handler that calls `notify_one()` + returns `Response::ok()`.
* `aura-cli/src/client.rs`: clones `admin_state.shutdown` before spawning the
  admin server task, then `tokio::select!`s between `router.run()` and
  `shutdown.notified()`. Whichever finishes first ends the function; OsRouteGuard
  Drop runs after.
* `aura-cli/src/server.rs`: same pattern around the `MultiServer::accept` loop —
  graceful exit on admin Shutdown leaves the accept loop, breaks, and the
  router_task is aborted on function return.
* `aura-cli/src/main.rs`: `aura shutdown --admin-socket <path>` subcommand for
  CLI control (also useful from launchd/systemd post-stop hooks).
* `aura-gui/src-tauri/src/admin.rs`: new `send_shutdown(path)` helper; factored
  out `round_trip()` for the common write-line + read-line pattern. Windows
  stub returns "not implemented".
* `aura-gui/src-tauri/src/cli_proc.rs`: `ClientHandle::kill` now tries admin
  Shutdown first (3 s poll for graceful exit), then SIGTERM to sudo (2 s),
  then SIGKILL as last resort. The admin path needs no sudo because the
  socket is already chmod 0666 from v3.4.1.

## Test

New `admin::tests::shutdown_request_fires_notify` unit test: spawns a
notified() waiter, calls `handle_request(Request::Shutdown)`, asserts the
waiter wakes within 200 ms. Combined with the existing 5 admin tests, all 6
pass.

`cargo test --workspace` — all green, `cargo clippy --workspace --all-targets
-- -D warnings` — clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:18:00 +03:00
xah30 fa452f00b1 fix(cli,aura-gui): v3.4.2 — break the infinite-tunnel loop + drop dead GUI handle
Two coupled bugs from the macOS half-Internet-route fix (v3.4.1) put together.

## 1) Infinite tunnel loop: server IP routed through itself

v3.4.1 made `default = "VPN"` actually win against the host's pre-existing
default by installing `0.0.0.0/1` and `128.0.0.0/1` via the TUN. Both
half-Internet routes are strictly more specific than `0.0.0.0/0`, so
outbound traffic finally went through Aura.

Side-effect: the server's outer IP (187.77.67.17) falls inside `128/1`.
That means Aura's own encrypted ciphertext to 187.77.67.17:443 also matched
the new route → re-entered the TUN → was about to be re-encrypted and
shipped to … itself. The kernel held the existing TCP socket on en0 for a
few seconds (sticky source), so the connection survived briefly. As soon
as anything triggered a re-route resolution (TCP retransmit on a different
socket, cover-traffic, new cipher frame), the socket flipped to utun4 and
the data plane died — exactly the "Aura умирает через пару секунд" the
user reported.

Fix: before calling `OsRouteGuard::install`, scan the dial config for
outer-endpoint IPs (the primary `server_addr` plus any `[client] bridges`
entries) and inject them into `SplitRoutes::direct_hosts`. The existing
macOS plan turns `direct_hosts` into `route add -host <ip> <gateway>` —
a /32 bypass via the original LAN gateway, more specific than `128/1`, so
the kernel routes the ciphertext via en0 even after the half-Internet
routes are in. No recursion, no flap, no death.

Only applied when `default = "VPN"` (the only mode where the bypass is
needed). Linux doesn't need it — the `metric 50` default-via-TUN doesn't
override more-specific kernel routes.

## 2) Dead GUI handle wedges the Connect button

`connect()` in lib.rs refused with "already running" whenever the
`Option<ClientHandle>` was `Some`, regardless of whether the child was
still alive. So when the aura-client died from bug #1 (within ~2 s of
Connect), the UI was permanently stuck — the only escape was quitting and
relaunching the GUI.

Now `connect()` checks `prev.is_alive()` first. If the previous handle is
dead, we reap it (calling `kill()` to consume the handle's drop path) and
spawn a fresh one transparently. Reconnect-after-crash now Just Works.

This also matches what a sensible "Connect" button does on every other
VPN GUI: clicking it when something looks stuck should make progress, not
demand a quit-and-relaunch dance.

## Verification

- `cargo test -p aura-cli --lib os_routes` — 21/21 ok
- `cargo build --release` — green
- Rebuilt /Applications/Aura.app against both fixes
- Server-side aura.service restarted to clear the leftover pool reservation
  the dead session never released (see v3.5 task #52 for the auto-cleanup
  follow-up)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 20:12:21 +03:00
xah30 f68a61f760 fix(tunnel,aura-gui): macOS TUN auto-assign + admin-access check
Two bugs found in the GUI's first end-to-end test:

## #41 was incomplete — `Some("")` is not the same as `None` for tun-rs

The agent's earlier #41 fix passed `""` to `Configuration::tun_name()`
expecting the tun crate to treat empty as "let the kernel auto-assign". It
doesn't. Looking at tun-0.8.9/src/platform/macos/device.rs:

    if !tun_name.starts_with("utun") {
        return Err(Error::InvalidName);
    }

An empty string fails `starts_with("utun")` so the create errors out before
the kernel is ever consulted. The auto-assign branch ONLY triggers when
`config.tun_name` is `None` — which requires us to skip the `.tun_name()`
call entirely, not pass a sentinel value.

Fix: split the builder chain so `.tun_name()` is only called when the
sanitized name is non-empty. The kernel now correctly auto-picks the next
free `utunN` for the standard provisioned `tun_name = "aura0"` config.

User-visible symptom this resolves: the GUI's Connect button consistently
died with `failed to create TUN device 'aura0'` followed by an InvalidName
chain, even though aura was running as root.

## check_admin_access tested the wrong command shape

`check_admin_access` ran `sudo -n <aura> --help` and inferred the sudoers
entry was installed iff that succeeded. But our sudoers entry is scoped to
`<aura> client *` — `<aura> --help` does NOT match, so even when the
entry was correctly installed and Connect was already working, the yellow
"One-time setup needed" banner stayed up forever.

Switched to `sudo -n -l <aura>` which lists matching sudoers entries for
the binary path itself. Returns 0 iff ANY entry covers it without a
password — works regardless of the per-command scope.

## Verification

- `cargo test -p aura-tunnel --lib tun` — all 3 sanitize / create tests pass
- Rebuilt `target/release/aura` and `/Applications/Aura.app` against the
  fixes
- Confirmed via `sudo -n -l /Users/xah30/AuraVPN/target/release/aura` that
  the installed sudoers entry is detectable by the new check

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:45:59 +03:00
xah30 dbee9d8b93 fix(aura-gui): rename product to 'Aura' + uppercase SETENV in sudoers
Two bugs visible after user's first Install-admin-access click:

1) `visudo -c` rejected the fragment because sudoers tags must be UPPERCASE.
   We wrote `setenv:` (lowercase) which sudoers does not recognise as a tag
   and treats as a command path, producing a syntax error at column 29 of
   /etc/sudoers.d/aura-gui:2 — and worse, the broken file stayed on disk
   so every subsequent `sudo` complained about the syntax error too (sudo
   still functions but the warning is noise).

   Fix: drop the `setenv:` tag entirely. We never needed it — the GUI only
   passes RUST_LOG to the child via env(), which `sudo -E` would forward
   but we deliberately chose not to (smaller surface). Removing the tag
   also removes the failure mode.

2) Product rename per user feedback ("переименуй пакет на просто Aura"):
   - tauri.conf.json `productName` and window title: `aura-gui` -> `Aura`
   - bundle now produces /Applications/Aura.app and Aura_0.1.0_aarch64.dmg
   - identifier `ru.undergr0und.aura` was already correct, no change
   - sudoers file is now /etc/sudoers.d/aura (was aura-gui), so the success
     message and revert hint are updated accordingly

The internal MacOS/ binary is still named `aura-gui` (Tauri uses the Cargo
crate name there) — not user-visible, only the dev internals see it.

Manual cleanup also performed on the dev host:
- /Applications/aura-gui.app removed
- /etc/sudoers.d/aura-gui (the broken fragment from the first failed
  install attempt) removed via `osascript ... with administrator
  privileges` so sudo is no longer logging syntax errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:39:04 +03:00
xah30 1635190797 feat(aura-gui): privilege escalation via sudo + one-click NOPASSWD installer
The v0.1 GUI's Connect button was broken in practice: the Tauri app launched
from /Applications runs as the desktop user, so `Command::new(aura).spawn()`
started aura without root. aura died in ms with EPERM at TUN creation, faster
than the 1.5 s status poller could catch — the UI just silently flipped back
to "disconnected" with no clue.

## Fix

* `cli_proc::spawn_client` now prepends `sudo -n` on Unix. After spawn it
  blocks for 1.5 s and checks `try_wait`; if the child already exited, it
  reads the stderr ring's last 20 lines and returns an anyhow Error with
  that tail + a hint list of common causes. The Tauri command surfaces it
  to the frontend's `error` state where the UI renders it as a multi-line
  `<pre>` block instead of the previous single-line text.
* `ClientHandle::kill` no longer uses `Child::kill` (SIGKILL) on its sudo
  parent — that would have left aura orphaned with the TUN lingering.
  Sends SIGTERM to sudo, which sudo forwards to aura, giving the inner
  `OsRouteGuard::Drop` 2 s to run cleanup. Falls back to SIGKILL only after
  the grace period.

## One-click NOPASSWD installer

Two new Tauri commands plus a UI banner:

* `check_admin_access` — runs `sudo -n aura --help` and returns whether the
  sudoers entry is in place. Used by the React side to decide whether to
  show the banner.
* `install_sudoers_admin` — runs `osascript ... with administrator
  privileges` which surfaces the native macOS auth dialog, then writes
  `/etc/sudoers.d/aura-gui` scoped to `<aura> client *` only (not arbitrary
  aura invocations), runs `visudo -c` for syntax validation, and reports
  success or the syntax error.

The frontend shows a yellow "One-time setup needed" banner above the
profile list whenever `adminReady === false`. Clicking the button pops the
Mac password dialog once; from then on Connect is a single click with no
prompt.

## UI feedback

* "Connecting…" disabled state on the Connect button while spawn_client's
  1.5 s wait is in progress
* Errors render as monospace `<pre>` so the multi-line stderr tail is
  readable
* `.error` and `.admin-banner` CSS classes added to App.css

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:32:38 +03:00
xah30 40b38beb11 feat(aura-gui): v0.1 Tauri-based desktop client — system tray + profile manager + admin status
New crate (kept out of the cargo workspace so the protocol-side check/test cycle stays fast):
a Tauri 2 + React 19 + TypeScript desktop app that runs in the system tray and manages
`aura client` for the user. The clash-verge replacement we settled on instead of trying to
shoehorn AuraVPN's L3 IP-tunnel into a clash-verge L4 outbound.

## What's wired

- **Profile manager** — `aura-gui/src-tauri/src/profiles.rs`. App-data layout
  (`~/Library/Application Support/ru.undergr0und.aura/profiles/<id>/` on macOS, the
  equivalent on Linux + Windows). `import_profile_from_tgz` accepts the same bundle shape
  `aura provision-client` emits, detects flat vs single-dir layouts, and refuses overwrites
  unless the operator deletes first. `delete_profile` refuses symlinks.

- **Connection control** — `cli_proc.rs`. Spawns `aura client --config <profile>/client.toml
  --admin-socket /tmp/aura-admin-<uid>-<profile>.sock`, captures stderr into a bounded
  in-memory ring (200 lines) for the UI to tail, kills via `Child::kill` on disconnect.
  Per-profile / per-uid socket paths so two GUIs (or two profiles) don't collide.

- **Live status** — `admin.rs`. Tiny JSON-line client for the v3.3 admin socket. Polled by
  the React app every 1.5 s: peer id, rx/tx packets, default action, rule count. Falls back
  gracefully (admin_error in the response) when the handshake hasn't completed yet.

- **System tray** — `lib.rs` `setup` callback. Three-item menu (Open AuraVPN / Disconnect /
  Quit). The window's close button hides to the tray instead of exiting — the app keeps
  running so the VPN stays connected; the user explicitly chooses Quit.

- **Frontend** — `src/App.tsx`. Single-page layout: profile list (with badge for missing
  files), connect/disconnect button per profile, status table, collapsible logs panel,
  binary-path picker at the bottom. Dark-mode CSS by default; the same look as a typical
  WireGuard / Tailscale-style tray app.

## What's deferred for v0.2

- Auto-start at login (launchd plist / systemd user unit / Windows Run key)
- Code signing + notarization
- Persisting the aura binary path between sessions
- Per-profile route overrides editor
- Live log streaming (today the frontend polls the ring buffer)
- Admin status query on Windows (today's `admin.rs` Unix-only; Windows path returns a clear
  "not supported yet" error)
- Polkit / authorization-services prompt for the TUN-needs-root step (today the operator
  has to launch the GUI from a privileged context, e.g. `sudo open -a aura-gui` on macOS)

## Workspace hygiene

Cargo workspace at the repo root now has `exclude = ["aura-gui"]` so the protocol crates'
`cargo check --workspace` / `cargo test --workspace` don't pull in the tauri + wry + webview
dep graph. The GUI builds standalone from `aura-gui/` via `npm run tauri build`.

## Validation

- `cd aura-gui/src-tauri && cargo check` — green
- `cd aura-gui/src-tauri && cargo clippy -- -D warnings` — clean
- `cd aura-gui/src-tauri && cargo fmt --check` — clean
- `cd aura-gui && npm run build` — frontend tsc + vite build succeeds
- Full `npm run tauri dev` not exercised in this session (would open a real window) — should
  work; if it breaks the surface area is small enough that next session fixes it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:47:51 +03:00