Commit Graph

26 Commits

Author SHA1 Message Date
xah30 d2d3bc3e3c feat(cli): v3.5 — coexist routing with foreign VPNs (#2)
Closes the long-standing "Aura killed the internet while Clash Verge is
running" symptom. The cause is unsurprising once you stare at the routing
table: even after the user turns Tun mode off in Clash's GUI, the
clash-verge-service daemon does NOT remove the split-tunnel routes it
installed. They linger as `1/8`, `2/7`, `4/6`, `8/5`, `16/4`, `32/3`,
`64/2`, `128.0/1` → `198.18.0.1` (Clash's dead TUN). Aura's half-Internet
routes (`0.0.0.0/1` + `128.0.0.0/1`) lose by longest-prefix-match to those
foreign /8 / /7 / ... entries — so DNS goes to a non-functional foreign
interface and the user-visible internet looks dead.

## New module: aura-cli/src/coexist.rs

`scan_foreign_routes_macos(our_iface, pool_cidr) -> Vec<ForeignRoute>` —
shells out to `netstat -rn -f inet`, parses the output (incl. macOS's
classful shorthand: `1` = `1.0.0.0/8`, `169.254` = `169.254.0.0/16`, etc),
filters out: ourselves, loopback (`lo*`), link-local, LAN interfaces
(`en*` / `eth*` / `wlan*`), reserved ranges (127/8, 169.254/16, 224/4),
and the VPN's own pool. What's left is foreign-VPN territory.

`generate_override_cidrs(foreign, max_prefix=24) -> Vec<IpNetwork>` —
for each foreign /n, emits two strictly-more-specific /(n+1) routes that
together cover exactly the same range but point at Aura's TUN. By
longest-prefix-match the kernel routes that traffic through Aura;
foreign routes stay in the table untouched (which makes rollback trivial:
OsRouteGuard's Drop only undoes what Aura installed).

Routes /24 or narrower are skipped — those typically are LAN segments
operators don't want hijacked.

## Wired through SplitRoutes

`SplitRoutes` gains a `force_vpn_cidrs: Vec<IpNetwork>` field for the
override list. `macos_apply_plan`'s `DefaultAction::Vpn` arm now installs
them between the direct-host bypasses (most specific — server IP) and the
half-Internet catch-alls (least specific). Plan ordering becomes:

  [0..N]      direct CIDR / direct host bypasses (server IP, user-direct CIDRs)
  [N..N+2K]   override routes (2 per foreign /n the scan found)
  [N+2K..]    0.0.0.0/1 + 128.0.0.0/1 catch-alls

## Wired through client.rs

After the existing bypass-injection block, when `default == VPN` and we're
on macOS, scan foreign routes and append the generated overrides to
`split.force_vpn_cidrs`. Logged at INFO level so the operator can see in
the journal exactly which foreign VPN was detected and how many overrides
were emitted.

## Tests

9 new unit tests in `coexist::tests`: macOS shorthand parsing (`1` /
`2/7` / `192.168.1`), bare IP host routes, garbage rejection, full-table
netstat-output parsing against a real captured sample (the user's
machine's actual routing table with Clash Verge running), half-splitting,
classful Clash pattern coverage, the /24 skip rule, and the doubling
property of generate_override_cidrs.

All workspace tests still pass; `cargo clippy --workspace --all-targets
-- -D warnings` is clean.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:35:46 +03:00
xah30 b904d40fba fix(cli): v3.4.5 — static-reservation IpPool always succeeds (#1 user retest)
Empirical observation from the v3.4.4 retest: the GUI's Connect button
now goes through cleanly, but if the user did `Disconnect → Connect` after
any prior ungraceful exit (SIGKILL, crash, network flap that hadn't yet
timed out), the new handshake immediately died with:

    Error: router run loop
    Caused by: peer connection closed; router shutting down

That's our v3.4 "peer connection broke" guard firing. Looking at the
server log, the cause was:

    WARN aura_cli::server: refusing connection: ip pool denied an address
    (unknown id under static_only, duplicate static reservation, or pool
    exhausted)

i.e. the static reservation `mac-v34 → 10.7.0.10` for the new connection
was refused because the previous (now-dead) session never released
10.7.0.10 from the pool's `in_use` set. Without restarting the systemd
unit, no reconnect was possible.

This was the previously-documented v1 policy ("do not hand out the same IP
twice"). For dynamic allocation that policy is correct — two different
clients fighting for the same IP would corrupt routing. But for STATIC
reservations there is no ambiguity: the static map says "this IP is
reserved for THIS client id", so a reconnect with the same id is the
rightful owner; the previous holder (same id) is by definition stale.

Fix: in `IpPool::assign`, when a static reservation matches the requested
client_id, always return it — skip the `in_use.contains(&ip)` check. The
server's accept loop already runs `ServerRoutes::register(ip, new_conn)`
which evicts any previously-registered conn under the same IP, drops its
Arc, the transport closes, and the orphaned per-conn task ends and calls
`pool.release` naturally. So the in_use marker is correctly cleared by
the eviction cascade within milliseconds of the new assign.

Test updated to match new behaviour:
  `static_reservation_refused_when_already_in_use` →
  `static_reservation_always_honoured_even_if_in_use`

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:22:47 +03:00
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 a974abdaa2 fix(cli): v3.4.3 — install bypass routes BEFORE half-Internet routes
The v3.4.2 fix injected the server-IP bypass into `SplitRoutes::direct_hosts`
but the macOS apply plan emitted the bypass commands AFTER the two
half-Internet routes. There's a ~tens-of-ms race window during which:

1. `route add -net 0.0.0.0/1 -interface utunN`  ← installed
2. `route add -net 128.0.0.0/1 -interface utunN`  ← installed; 187.77.67.17
   now matches `128.0.0.0/1` and routes to utunN
3. *kernel re-resolves routes for the live TCP socket Aura is using to talk
   to 187.77.67.17* — packets briefly enter utunN → infinite recursion → the
   socket sees a stall and the inner data plane collapses
4. `route add -host 187.77.67.17 192.168.1.254`  ← finally bypasses, but
   too late — TCP is already in a bad state

This matches the user's "Aura умирает через пару секунд после подключения"
symptom verbatim. Server side saw `rx_packets` grow once (a few frames
from the cover-traffic loop) and then `tx_packets` flatline at zero — exactly
what happens when the upstream is dead.

Fix: reorder `macos_apply_plan` for `DefaultAction::Vpn` so all bypasses
(direct_cidrs + direct_hosts) install FIRST. When the half-Internet routes
finally land, the kernel's longest-prefix-match already has the /32 bypass
for the server IP ready, so the in-flight TCP socket keeps egressing via
en0 throughout.

Test updated to assert the new plan order:
  [0] direct CIDR via gateway
  [1] direct host via gateway (-host)
  [2] 0.0.0.0/1 via -interface utun
  [3] 128.0.0.0/1 via -interface utun

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:10:28 +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 cff8de14af fix(cli): v3.4.1 — macOS default-route override + admin sock chmod 0666
Two production-blocking bugs from the GUI's first end-to-end live test against
the production server.

## 1) os_routes: macOS `0.0.0.0/0` does not override the existing default

Empirical observation: client connects, server-side rx counter grows as we
send packets (TCP/443 handshake + frames arrive), but server-side tx never
ticks. From the Mac side `ping 10.7.0.1` returns 0/3, `curl https://1.0.0.1`
returns empty. Tracing it: even with `[tunnel.split] default = "VPN"` the
host's pre-existing default route (`default → 192.168.1.254 → en0`) was
still winning routing decisions. Aura's `route add -net 0.0.0.0/0
-interface utunN` had exit-zero'd but the new entry never beat the original
default — macOS happily accepts the route command, the kernel just doesn't
use it for outgoing packets.

This is a known macOS quirk that every long-lived VPN works around the same
way: install two **half-Internet** routes (`0.0.0.0/1` and `128.0.0.0/1`)
which are strictly more specific than `0.0.0.0/0` and so win by
longest-prefix match. Tailscale, WireGuard, OpenVPN all do this. We now do
too.

Updated the macos_plan_default_vpn unit test to assert the new plan shape
(4 steps for VPN + direct-cidr + direct-host instead of the old 3).

The split has a known limitation: the server's own outer endpoint (e.g.
187.77.67.17:443) is now routed into the tunnel too. The dialer's
already-established TCP source-IP keeps the *current* connection alive, but
a redial after a route flap would loop. Documented in the source comment;
v3.5 will add an explicit `<server_ip>/32 via <orig_gateway>` bypass at
install time.

## 2) admin: chmod 0666 the freshly-bound Unix socket

When `aura client` is spawned by `sudo` (the GUI does this on the user's
behalf), the admin Unix socket ends up owned by root with the default 0755
mode. macOS's `connect()` requires write permission on the socket file, so
the desktop-user GUI sees `Permission denied (os error 13)` and the status
panel stays empty — even though the tunnel itself works.

`transport::listen` now does `chmod 0666` on the socket immediately after
`UnixListener::bind`. The socket lives under `/tmp` (laptop) or `/run`
(systemd-managed server) so the directory permissions still gate access;
making the socket world-RW just lets the per-machine apps that already have
filesystem access actually use it.

## Verification

- `cargo test -p aura-cli os_routes::tests::macos_plan_default_vpn` — ok
- `cargo build --release -p aura-cli` — green
- Bug repro: pre-fix, server admin shows `rx: 13 tx: 5` while client sends
  ICMP that never returns. Post-fix (manual test): the half-Internet routes
  appear in `netstat -rn`, ping 10.7.0.1 succeeds, curl through the tunnel
  works.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 20:02:35 +03:00
xah30 7c2080321b feat(cli,tunnel): v3.4 client consumes manifest endpoints + fix #45 silent client exit
Two follow-ups to the previous v3.4 commit (ba8d6b7):

## #49 — client uses BridgeEndpoint ports as authoritative

BridgesDiscoveryWatcher now keeps a second snapshot
(`Arc<RwLock<Vec<BridgeEndpoint>>>`) for the per-transport endpoints carried by
v3.4 manifests, alongside the existing flat-bridges snapshot for v3.3
compatibility. `endpoints_snapshot()` and `primary_endpoint()` expose it to the
client.

In `client::run`, immediately after the watcher loads, the primary endpoint's
per-transport ports override the dial-time `dial_cfg.endpoints.{tcp,quic,udp}`
*ports*. The IP stays whatever the dialer already resolved (server_addr /
bridge list). This is what closes the loop on the user's friend's setup: the
server picks 8444 because sing-box has 443/8443, signs a manifest with
`endpoints = [{tcp: 8444, ...}]`, the client loads it on next refresh and
starts dialing the right port without an operator-side `client.toml` edit.

When the manifest has no `endpoints` field (old v3.3 format, or operator
chose not to publish per-transport ports), no override is applied and the
client.toml `[transport] *_port` values are used as before.

## #45 — silent client exit on broken connection

Root cause confirmed in `AuraRouter::run`:
- the inbound task did `let pkt = inbound_conn.recv_packet().await?;`, so any
  recv error returned silently via `?`
- the `to_tun_tx` channel sender dropped, `to_tun_rx.recv()` returned `None`
- the outbound `select!` arm matched `None => break Ok(())`
- the router returned `Ok(())`, the client's `run()` returned `Ok(())`, the
  process exited 0 with no log, no error message

We saw this empirically when the user disabled a co-resident VPN that had been
routing AuraVPN's UDP/444 traffic — the underlying QUIC socket broke, the
inbound task hit recv error, and the whole client vanished.

Fix:
- Inbound task now logs the error at `error` level with the underlying
  `recv_packet` cause before exiting.
- The outbound `select!`'s `None` arm now returns an Err (not Ok(())) so the
  caller knows the tunnel died and `aura client` exits non-zero — which is
  what a supervisor (systemd, launchd, or a future auto-redial loop) wants to
  see.
- The router waits up to 200ms for the inbound task to land cleanly before
  returning, so its error / panic is logged instead of being swallowed by
  `abort()`.

Existing tests still pass (12/12 in aura-tunnel router tests). Tested
manually: with the fix, killing the underlying transport now produces a
"peer connection broke (recv_packet failed): …" error line and a non-zero
exit, instead of silent process disappearance.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:22:10 +03:00
xah30 ba8d6b796f feat(transport,cli,tunnel): v3.4 port auto-detect + bug fixes from live test
Live macOS test against the production server uncovered six bugs (one of which
turned out to be a port collision with sing-box, not a real bug); this commit
addresses all of them and adds v3.4 port discovery so the same collision is
handled transparently next time.

## v3.4 server port-discovery

- Defaults moved off 443/444 to 8443/8443/8444 (TransportSection::default,
  ServerInitOpts, ProvisionClientOpts, CLI flags). 443 is heavily contested in
  practice (sing-box, Hysteria2, reverse proxies) and the previous default
  silently lost the bind when a co-tenant was already there.
- MultiServer::bind_with_outer_or_scan: scans forward up to
  DEFAULT_PORT_SCAN_MAX (20) candidates per transport when the requested port
  is occupied; QUIC keeps walking if it lands on the custom-UDP port.
- MultiServer::bound_addrs(): the actual addresses each transport bound to.
- Server logs the bound addresses and writes a runtime snapshot
  (server.toml.runtime.json) when they differ from the requested ones, so
  `aura sign-bridges` can re-sign the bridges manifest later.
- BridgeManifest gains an optional `endpoints: Vec<BridgeEndpoint>` field
  with per-transport ports. Backward-compatible: old v3.3 clients ignore the
  field and continue to use the v1 `bridges` line.
- `aura sign-bridges --endpoints HOST:tcp=N:quic=N:udp=N` to mint v3.4
  manifests; bridges line is auto-synthesised for v3.3 clients.

## Bug fixes from the live test

- macOS TUN naming (#41): the tun crate rejects names that don't match
  ^utun[0-9]+$. On macOS we now substitute `""` (kernel auto-assigns utunN),
  capture the assigned name via inner.tun_name(), and propagate it through to
  os_routes::OsRouteGuard::install — so `route add -interface utunN` uses
  the real interface, not "aura0".
- Packet counters (#42): Stats { tx_packets, rx_packets } are now actually
  bumped by the data path. `aura status` shows live numbers instead of
  permanent zeros.
- render_client_toml schema (#44): provisioner emits proper
  `[[tunnel.split.vpn]] cidr = "..."` / `[[tunnel.split.direct]]` blocks from
  new --vpn-cidrs / --direct-cidrs flags. The v3.3 `vpn_cidrs = [...]` flat
  array was silently ignored by serde, leaving users with `rules: 0` even
  when their CIDRs looked right.
- #43 / #46 (TCP/443 dial early-eof / no payload back): diagnosed as the
  sing-box port collision, not an Aura bug. The v3.4 port-scan path makes it
  go away — the server picks a free port and clients learn it from the
  manifest.

## Test coverage

Three new unit tests for the port-scanner (UDP busy, TCP busy, zero budget);
two new tests for v3.4 BridgeManifest round-trip with endpoints; one
integration test for the new `[[tunnel.split.vpn]]` rendering; tests for the
runtime-state file write/read round-trip; agent-added router-counter tests
in aura-tunnel/tests/routes.rs.

cargo test --workspace, cargo clippy --workspace -- -D warnings, and
cargo fmt --check all pass.

#45 (silent client exit when underlying QUIC transport breaks) is still
outstanding — needs deeper investigation; deferred to a follow-up.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:14:45 +03:00
xah30 a173ced9b2 feat(cli,pki): v3.3 bridge discovery via signed CA manifest
Closes the v3.3 "bridges by hand" honest limitation. Admins now publish a
CA-signed manifest with the current bridge list; clients re-read it from
disk on a timer and merge it with the static [client] bridges. Cuts the
"rotate the bridge list" cycle from "edit every client config" to
"distribute one signed file".

- New aura sign-bridges CLI:
    aura sign-bridges --ca /etc/aura/pki \
                      --bridges "ip1:443,ip2:443" \
                      --ttl-days 7 \
                      --out /var/aura/bridges.signed
- Manifest format (single file, text + signature block, same shape as the
  in-band CRL):
    AURA-BRIDGES-v1
    {"version":1,"generated_at":...,"expires_at":...,"bridges":[...]}
    --SIGNATURE--
    <hex ECDSA-P256/SHA-256 over body>
- aura-pki now exports `sign_ecdsa_p256` / `verify_ecdsa_p256` so CRL and
  bridges share ONE signing primitive (no copy-paste). CRL keeps working.
- aura-cli::bridges::BridgeManifest + BridgesDiscoveryWatcher: new
  module. encode_signed/load_signed_verified verifies signature + rejects
  expired manifests. Watcher spawns a tokio interval that re-reads the
  file; on load failure (truncated, expired, bad sig) the previous
  snapshot is kept — bridges never collapse to empty.
- New [client.bridges_discovery] {enabled, manifest_path,
  refresh_interval_secs}; serde(default) so v3.2 configs keep working.
- Merge strategy: manifest EXTENDS static [client] bridges, dedup by
  SocketAddr, static-first ordering. Static remains as fallback.
- 13 new tests (8 lib unit + 4 integration + 1 config). Workspace: 310
  tests passed (+13), clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:39:23 +03:00
xah30 5e553b79df feat(cli): v3.3 circuit rotation — background rebuild every N seconds
Adds RotatingCircuit: the multi-hop circuit is silently torn down and
rebuilt on a configurable interval (default off) so a long-running
client periodically rotates its on-wire path. Application packets never
see the swap.

- RotatingCircuit::new(hops, udp_opts, interval) seeds an initial
  CircuitConnection synchronously (errors surface), then spawns a
  background rotator that every `interval`:
    1. dial_circuit(&hops, udp_opts) -> next: CircuitConnection
    2. std::mem::replace inside Arc<RwLock<Arc<CircuitConnection>>>
    3. old Arc dropped when its last in-flight Arc clone is released
       (its Drop aborts forwarders / closes outers).
  send_packet/recv_packet grab a cheap snapshot of the current Arc
  before awaiting, so reads/writes never block under the rotator.
- [client.circuit] rotation_interval_secs: u64 (default 0 = disabled);
  serde(default) keeps old configs working. When 0, the path is exactly
  the v3.2 dial_circuit + optional CellPaddingConn wrap (back-compat).
- CellPaddingConn wraps RotatingCircuit on the OUTSIDE so every new
  circuit shares the same cell_size — on-wire size signature stays
  stable across rotations.
- Integration test multihop_rotation::rotating_circuit_swaps_inner_
  under_traffic: 6 s of 100-ms ping/echo at interval=1.5s -> 37 sent,
  37 received, 2 rotations counted via test-only AtomicU64 counter.
- Synchronous-failure test confirms initial dial errors bubble up from
  ::new without spawning the rotator task.

Workspace: 297 tests passed (+4), clippy -D warnings clean, fmt clean.
293 baseline tests unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:25:05 +03:00
xah30 5ea643a9e5 feat(cli,tunnel,docs): full Windows support — OS routes + wintun audit
Windows is now first-class for client use:

- aura-cli::os_routes Windows path is no longer a stub. Real install via
  `route ADD <net> MASK <mask> <gw> METRIC 1` for DIRECT bypass (rollback:
  `route DELETE ...`) and `netsh interface ipv4 add route <cidr> "Aura"
  <tun_local_ip> store=active` for VPN default/CIDR (rollback: `netsh ...
  delete route ...`). Default-gateway detection by parsing `route print 0`
  output via parse_windows_route_print_default; rejects `On-link` rows. Dry
  run works on every host.
- aura-tunnel::tun wintun audit fixed a real bug: AuraTun was holding only
  Arc<Session> while Session does NOT keep Arc<Adapter> alive (only the
  Wintun DLL handle). On Drop the adapter was being closed under the
  session. Fixed by adding _adapter: Arc<wintun::Adapter> to AuraTun, with
  field order ensuring Session is dropped before Adapter so end-session
  precedes close-adapter. Also wired mtu into write_packet (hard limit) +
  read_packet (warn).
- Cross-compile verified: cargo check --target x86_64-pc-windows-gnu
  --workspace and clippy on the windows target are both clean (added
  mingw-w64 + x86_64-pc-windows-gnu via rustup).
- docs/deployment.md: §6 updated (Windows OS-routes now Done), new §8
  «Windows как клиент» with download wintun.dll, Admin run, [tunnel.os_routes]
  enabled, known no-ops (run_as, [server.nat]).

9 new tests (7 parser/plan/undo unit + 1 windows dry-run integration + 1
existing). Workspace: 293 tests passed (+9), clippy -D warnings clean, fmt
clean. macOS host + windows-gnu cross-target both green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:14:23 +03:00
xah30 e0e53665f1 feat(crypto,cli,docs): russian SNI palette + RF-billing deployment scenario
Adds a way to make the outer-TLS SNI rotate among popular Russian-language
domains so that Russian carriers — who may start metering "foreign traffic"
separately — see the user's first hop as a domestic CDN/site request, not
as an exotic foreign destination.

- aura-crypto::masks:
  - SNI_PALETTE_RUSSIAN (15 real domains: mail.yandex.ru, vk.com, www.ozon.ru,
    dzen.ru, ya.ru, www.gosuslugi.ru, www.wildberries.ru, rutube.ru,
    news.rambler.ru, hh.ru, www.tinkoff.ru, lenta.ru, www.kinopoisk.ru,
    afisha.yandex.ru, music.yandex.ru).
  - enum SniPalette { Default, Russian, Mixed } (Default = v2 behavior).
  - derive_mask_for_msk_date_with_palette(...): pick from chosen palette,
    Mixed flips ~50/50 by HKDF okm[8]&1. Old derive_mask_for_msk_date kept
    as a thin wrapper -> byte-for-byte unchanged Default.
- aura-cli::masks::MaskRotator gains new_with_palette(...); the spawn loop
  uses the stored palette. Old new() preserves Default.
- aura-cli config: [transport.masks] palette = "default"|"russian"|"mixed"
  (serde rename_all = "lowercase", default Default).
- server.rs/client.rs read cfg.transport.masks.palette and pass it to the
  rotator at startup; logged at INFO so the operator sees the choice.
- docs/deployment.md: new §7 "Сервер в РФ против тарификации иностранного
  трафика" — context, ASCII topology, recommended RF providers, full
  server.toml + client.toml examples wiring [server.relay] + russian
  palette + LE outer cert + multi-hop, plus an honest list of what this
  does and does not give.
- config/{server,client}.toml.example updated with palette = "default".

Workspace: 284 tests passed (+8 new = 4 crypto + 2 cli masks + 2 config),
clippy -D warnings clean, fmt clean. 276 baseline tests untouched.
Backward-compatible: configs without palette default to Default, identical
to v2 wire behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:29:18 +03:00
xah30 9b98004424 feat(cli): v3.2 multi-hop — per-hop cert, cell padding, 3-hop, CIDR whitelist
Closes the v3.1 unlinkability gap and resists volume/timing correlation:

1) Per-hop client cert (identity-unlinkable hops). [[client.circuit.hops]]
   now accepts {addr, cert_path, key_path, [server_name]} per hop — each
   hop sees a different CN, so a relay and an exit cannot correlate the
   same client by certificate. Old flat `hops = ["ip:port"]` form still
   parses (serde untagged enum) and falls back to [pki] cert/key.
   `aura provision-client --circuit-hops N` mints N fresh UUIDv4 certs.

2) Cell padding. CellPaddingConn wrapper pads every outgoing packet to a
   fixed size (default 1280 bytes; `cell_size = N` configurable) before
   it hits the inner AEAD. Format: u16_be(real_len) || pkt || zero_pad.
   On-wire sizes become constant -> defeats volume/timing fingerprints.
   Opt-in via [client.circuit] cell_padding = true and the mirror
   [server] cell_padding_for_circuit_clients = true.

3) 3-hop support. dial_circuit now accepts N >= 2 hops; iterative
   ExtendBridge nests N-1 forwarders and N handshakes. Client owns the
   full chain via CircuitConnection (forwarders abort on drop).
   New integration test multihop_v3_2_three_hops_end_to_end runs three
   in-process actors (A relay -> B relay -> C exit) on loopback and
   verifies peer_id == C's CN.

4) CIDR whitelist. [server.relay] allow_extend_to entries accept
   "10.0.0.0/24" (subnet, any port), "10.0.0.0/24:443" (subnet + port),
   "[2001:db8::/32]:443" (IPv6 with port), as well as exact IP:port.
   Empty list keeps the v3.1 open-relay (warn).

19 new tests; workspace 276 passed (+19), clippy -D warnings clean, fmt clean.
257 baseline tests untouched; all v2 / v3.1 / LE configs work as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:07:12 +03:00
xah30 f26ed7fce0 feat(cli,transport): Let's Encrypt outer-cert support on TLS-443/QUIC
Server admins can now point the outer TLS layer at a real CA-signed cert
(e.g. Let's Encrypt fullchain.pem) so the on-wire HTTPS camouflage is
indistinguishable from a normal CA-trusted HTTPS server. The inner Aura
mutual-auth handshake still uses the Aura CA (necessarily — that's where
the PQ mutual auth lives).

- aura-cli config: optional [server.outer_cert] {cert_path, key_path}.
  Both fields together (or neither); resolve() reads PEMs and returns
  (cert, key) tuple. Absent section -> falls back to reusing the Aura
  server cert (v2 behavior, fully back-compat).
- aura-transport: additive MultiServer::bind_with_outer and
  TcpServer::bind_with_outer that accept an optional separate outer cert.
  Old MultiServer::bind / TcpServer::bind preserved as thin wrappers
  (back-compat: existing callers untouched). AuraServer::bind already
  took outer cert separately.
- UDP transport doesn't have outer TLS, so outer cert is irrelevant
  there — only QUIC + TCP layers benefit.
- 4 new tests (parsing, back-compat, partial-section validation, two-CA
  loopback verifying inner peer_id is the inner CN). Workspace: 257 tests
  passed (+4), clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:35:22 +03:00
xah30 fe618b839d feat(cli): v3.1 multi-hop runtime — circuit client + relay rendezvous
Completes v3.1 multi-hop / onion routing (2 hops: client → entry-relay →
exit-server). Combined with the scaffold commit (6c14c0d), the property
holds: entry-relay knows the client IP + client_id but cannot decrypt the
data; exit knows the destination but sees the relay's IP as source.

- aura-cli::circuit: dial_circuit(&[entry, exit], proto_cfg, udp_opts) →
  CircuitConnection. Connects to entry as a normal UdpClient, sends an
  ExtendBridge control envelope, awaits CircuitReady, then runs a SECOND
  Aura handshake to the exit through a local loopback UDP proxy — the
  forwarder ferries datagrams between that proxy socket and the outer
  relay PacketConnection. The inner handshake therefore authenticates the
  EXIT cert (verified by the integration test asserting
  circuit.peer_id() == "localhost-exit"); the relay never sees the inner
  session keys.
- aura-cli::relay: rendezvous(conn, whitelist) -> Bridged{bridge} |
  Fallback{first_pkt} | Refused. 2-second window after handshake to receive
  ExtendBridge. Whitelist enforced; CircuitFailed on miss. Empty whitelist
  logs a warning and runs open. Timeout / non-control → Fallback so the
  same server can be both relay (for circuit clients) and exit (for direct
  clients) simultaneously.
- aura-cli::client: when [client.circuit] enabled → dial_circuit; falls
  back to normal aura_transport::dial when disabled.
- aura-cli::server: relay rendezvous wired before pool/CRL/router path.
  run_bridge spawns two forwarder tasks (conn↔bridge UDP socket).
- 3 integration tests: end-to-end (with peer_id assertion), whitelist
  rejection, back-compat (relay disabled → Err). 3 unit tests in relay.rs.

Workspace: 253 tests passed (247 baseline + 6 new), clippy -D warnings clean,
fmt clean. No new workspace deps. All 28 tracked tasks (v1 + v2 + v3.1) now
complete.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:16:07 +03:00
xah30 6c14c0d103 feat(proto,cli): v3.1 multi-hop scaffold — control kinds + config sections
Foundation for v3.1 onion routing (client → entry-relay → exit-server).
The relay/circuit runtime is implemented in a follow-up commit; this
scaffold lands the wire-level control extensions and the config schema:

- aura-proto: ControlKind gains ExtendBridge (client→relay), CircuitReady
  (relay→client), CircuitFailed (relay→client, with utf-8 reason); helpers
  encode_extend_bridge / decode_extend_bridge (1-byte family + 4/16 addr
  bytes + u16 port). Integration test in tests/control_extend.rs covers
  IPv4/IPv6 roundtrip + full magic-envelope wrap.
- aura-cli config: [server.relay] {enabled, allow_extend_to} +
  [client.circuit] {enabled, hops} sections; relay_whitelist() helper
  parses IP:port literals. All new fields serde-default, back-compat.
- crl_push.rs touched only to leave the new ControlKinds passing through
  the existing magic-envelope dispatcher unchanged.

Workspace: 247 tests passed (+12), clippy/fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:54:12 +03:00
xah30 35d94dee33 feat(proto,pki,cli): in-band CRL push (closes last v2 limitation)
Server now pushes its signed CRL to each connecting client right after the
handshake; the client verifies the signature against the CA and applies the
revocation list to its verifier (and caches it on disk for restarts).
Removes the v1 "CRL distributed out-of-band" honest limitation.

Wire (multiplexed over existing PacketConnection, no trait change):
control envelope = MAGIC[4]=[0xAA,0xAA,0xC0,0x01] || kind(u8) || u32_be(len)
  || payload. IPv4/IPv6 start with 0x4X/0x6X, so 0xAA cannot collide; an old
peer just drops it as a junk packet in the TUN — back-compat preserved.

- aura-proto: ControlKind { CrlPush, CrlAck, Unknown }, encode/decode_control_
  envelope, CONTROL_ENVELOPE_MAGIC; 7 frame tests.
- aura-pki: CrlStore::{encode_signed, save_signed, decode_signed_verified,
  load_signed_verified} — ECDSA-P256/SHA-256 from the CA private key against
  a textual "CRL-Aura-v1" body + --SIGNATURE--; 7 signing tests. ring 0.17
  added crate-local (already in lockfile via rustls-webpki).
- aura-cli: crl_push module — server pushes via conn.send_packet on accept;
  client wraps the Arc<dyn PacketConnection> in AcceptPushedCrlConn which
  sniffs the magic in recv_packet, verifies the signature, updates the
  AuraCertVerifier, caches to disk. PkiSection gets ca_key, crl_push (default
  true), accept_pushed_crl (default true).
- 5 in_band_crl integration tests via mock PacketConnection.

Workspace: 235 tests passed (+28), clippy -D warnings clean, fmt clean. v2
COMPLETE — all 9 honest v1 limitations resolved (except sing-box, per user).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:35:16 +03:00
xah30 8f0cf1f017 feat(cli): automation bundle + identity-minimization features
Reduces manual setup steps and trims user-identifying data exposed by the
server/client, in the spirit of the deployment story: an operator on the
wire sees less, and the admin types fewer commands.

New CLI subcommands:
- `aura server-init`: one shot — pki init + issue-server + writes a ready
  server.toml with auto-detected egress iface; flags --enable-knock,
  --enable-cover-traffic, --no-nat, --run-as toggle the new transport
  defenses and privilege drop.
- `aura provision-client`: issues a client cert and assembles the full
  bundle (ca.crt + client.crt + client.key + client.toml in one directory)
  ready to hand over to the client device. --id is optional (defaults to
  a fresh UUIDv4, so client identities don't have to encode anything real).

Identity / log minimization:
- `aura pki issue-client --id` is now optional — UUIDv4 by default.
- `[server]/[client] no_logs = true` filters peer_id, client_ip,
  source_addr, client_id, local_ip, user, id, assigned_ip, peer field
  values through a custom tracing FormatFields layer (events still fire
  but the identifying fields are redacted before being written).
- `[client] bridges = [...]`: secondary server addresses; build_dial_targets
  shuffles them after the primary, so blocking one IP doesn't kill the
  client.
- Auto-detect egress iface in [server.nat] (via detect_default_egress_iface);
  egress_iface in config becomes optional with graceful fallback.

Config examples updated; backward-compatible (all new sections optional with
serde defaults). Workspace: 207 tests passed (+22), clippy -D warnings clean,
fmt clean. No new workspace deps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:14:57 +03:00
xah30 65b26b555d feat(cli): OS-level split-tunnel routes (removes send_direct stub)
DIRECT-destination traffic now bypasses the TUN entirely via OS routing
table edits, instead of going through user-space and hitting the v1
send_direct stub. The user-space router only sees VPN-bound packets,
making the split-tunnel real.

- aura_cli::os_routes::OsRouteGuard: RAII install + rollback of OS routes.
  Linux: `ip route show default` parser -> DIRECT CIDRs via original gw,
  VPN default via TUN with metric 50. macOS: `route -n get default`
  parser -> `route add -net/-host ... <gw>` for DIRECT, `route add -net
  ... -interface <tun>` for VPN. Windows: stub + warning (v3).
- dry_run works on every platform (logs `would run: ...`); useful for
  tests and operator confidence-checks.
- SplitRoutes::from_config folds [[tunnel.split.direct]]/[[...vpn]] +
  resolved domains (via AuraDns) into one declarative plan.
- New [tunnel.os_routes] {enabled (default true), dry_run, gateway,
  egress_iface}; absent section = old user-space behavior (back-compat).
- client::run installs routes after AuraTun::create, before privdrop;
  guard's Drop reverts everything on shutdown.
- aura-tunnel::router unchanged; AuraRouter::send_direct kept as a
  defensive fallback (in v2 it should never fire — OS routes prevent
  DIRECT packets from reaching the TUN at all).

20 new tests (linux/macos parser unit tests, install dry-run, config
back-compat). Workspace: 174 tests passed (+19), clippy -D warnings
clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:20:30 +03:00
xah30 c6f0d7af9b feat(cli): auto-NAT + privilege drop + Windows named-pipe admin
Three v2-hardening features in aura-cli, one pass:

- nat::NatGuard: RAII auto-config of IP forwarding + MASQUERADE on server
  startup. Linux (sysctl ip_forward + iptables -t nat MASQUERADE) and
  macOS (sysctl ip.forwarding + pfctl with /tmp/aura-nat.conf). dry_run
  works on every platform (logs "would run: ..."). Reverts everything in
  Drop. New [server.nat] {auto, egress_iface, dry_run}; absent section =
  back-compat no-op. Removes v1's "manual NAT/forwarding" step.
- privdrop::drop_to_user: drop euid/gid after binding TUN + privileged
  ports. Linux setresuid/setresgid, macOS setgid+setuid (permanent drop),
  Windows no-op with warning. New [server] / [client] run_as = "..."
  (optional). Skipped with info-log if already non-root.
- admin: split transport into cfg(unix) Unix-socket and cfg(windows) Tokio
  named-pipe modules sharing one JSON-line serve/request flow over
  AsyncRead/AsyncWrite. DEFAULT_SOCKET = "/tmp/aura-admin.sock" on Unix,
  r"\\.\pipe\aura-admin" on Windows. Removes v1's "admin Unix-only".

Deps: nix 0.29 user feature under [target.'cfg(unix)'.dependencies] (cli-
local, not workspace). Workspace: 155 tests passed (+13), clippy -D warnings
clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:09:38 +03:00
xah30 821f7711e7 feat(transport): real TLS-443 on the TCP backend (replaces HTTP/1.1 masquerade)
The TCP fallback now does a full outer TLS handshake (tokio-rustls 0.26 over
rustls 0.23, ring provider) before the Aura proto handshake, exactly like the
QUIC backend: on the wire it is indistinguishable from genuine HTTPS until the
inner Aura mutual-auth handshake starts. Removes v1's "light HTTP masquerade"
limitation; the real security boundary remains the inner PQ handshake.

- aura-transport::tcp: dropped the HTTP/1.1 preamble helpers and TcpOpts
  fields (masquerade, host, user_agent, server_header). New flow:
  TlsAcceptor::accept (server) / TlsConnector::connect (client) →
  tokio::io::split(TlsStream) → server_handshake / client_handshake → Session.
  Client reuses crate::quic::AcceptAnyServerCert (outer SNI not authenticated;
  inner handshake is the security boundary). Outer server cert auto-sourced
  from proto_cfg.server_cert_pem (no API change for the CLI's bind).
- ALPN default: ["h2", "http/1.1"] (DEFAULT_TCP_ALPN, exported).
- TcpOpts: now just { alpn: Option<Vec<Vec<u8>>> }.
- TcpClient::connect gains an outer-SNI &str param; DialConfig.sni passes it
  through (separate from the inner proto_cfg.server_name).
- tokio-rustls 0.26 added as a transport-local dependency (not workspace).

CLI updates: removed dead host/user_agent/server_header wiring; mask rotation
no longer touches TCP outer parameters (TLS doesn't have a Host header on
the wire). [transport] masquerade kept as a no-op for back-compat with old
configs (documented).

3 new tcp_loopback tests (default ALPN end-to-end, custom ALPN, outer SNI
mismatch still connects = proves accept-any is in effect). Workspace: 142
tests passed (+1), clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:53:02 +03:00
xah30 0a73d5298b feat(cli): server IP pool + per-client routing (multi-client VPN concentrator)
Server now assigns each connected client an IP from a configurable pool and
maintains a client_ip -> AuraConnection map so packets read from the shared
TUN are dispatched to the right client (and each client's recv loop writes
back to the TUN). Removes v1's "single shared TUN, no NAT/pool" limitation;
turns the server into a proper multi-client VPN concentrator (paired with the
already-landed UDP multi-client demux).

- aura_cli::pool: IpPool + PoolStrategy {StaticOnly, DynamicOnly,
  StaticOrDynamic}; reserves network/broadcast/server-own IP; 15 tests.
- aura_cli::server_router: ServerRouter + ServerRoutes (Arc<RwLock<HashMap>>);
  central TUN read loop dispatching by dst_ip; spawn_inbound_forwarder per
  conn auto-unregisters and releases the IP on disconnect; 4 tests via
  MockTun + MockConn.
- aura_cli::config: [server.pool] {cidr, strategy, static} added with
  serde(default); legacy configs (only [tunnel] pool_cidr) fall back to a
  DynamicOnly pool (backward compatible, tested).
- aura_cli::server: accept loop now: pool.assign(peer_id) -> register ->
  spawn_inbound_forwarder; rejected static_only mismatches dropped+logged.
- config/server.toml.example: documented [server.pool] section.

Workspace: 141 tests passed (+24), clippy -D warnings clean, fmt clean. No
new workspace deps (async-trait added to cli dev-deps for mock traits in tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:41:29 +03:00
xah30 c95e1a482c feat(crypto,cli,transport): daily protocol-mask rotation at 05:00 MSK
Both server and client deterministically rotate the on-wire obfuscation mask
(SNI, HTTP Host/User-Agent/Server headers, UDP padding profile) at 05:00 Moscow
time (02:00 UTC) every day, derived from the CA fingerprint + UTC date — no
network coordination needed.

- aura-crypto::masks: MaskSet + 4 palettes (16 SNI, 10 UA, 5 Server, 4 padding
  profiles); derive_mask_for_msk_date via HKDF-SHA256(salt="aura-mask-v1-salt",
  ikm=ca_fp||"YYYY-MM-DD", info="aura-mask-v1"); ca_fingerprint with built-in
  base64 PEM decode (no new deps).
- aura-cli::masks: MaskRotator (Arc<RwLock<MaskSet>>) + Hinnant's civil_from_days
  for manual UTC date math; scheduler picks next 02:00 UTC strictly (avoids
  busy-loop at boundary); spawned at startup in server::run/client::run.
- aura-transport: PADDING_PROFILES + next_bucket_for_profile (profile 0 byte-for-
  byte equals legacy pad_to_https_size); TcpOpts gains user_agent/server_header;
  UdpOpts gains padding_profile; MultiServer holds Arc<UdpServer>/Arc<TcpServer>
  with set_udp_opts/set_tcp_opts so rotation propagates without restart.
- Backward-compatible: defaults preserve previous behavior; existing 97 tests
  unchanged. 17 new tests (derive determinism + date variation, civil-from-days
  known points incl. 1970-01-01/2000-02-29/2024->2025, next-rotation boundary,
  msk_today offset, profile equivalence, base64 round-trip, full mask-driven
  UDP loopback). Total: 114 passed, clippy/fmt clean. No new workspace deps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:11:45 +03:00
xah30 d5b9a8611d feat(cli): select transport in config; server MultiServer + client dial handover
- aura-cli config gains [transport] (order + per-transport ports + obfuscate/
  masquerade); server binds all enabled transports via MultiServer, client uses
  dial() with UDP->TCP->QUIC handover. Config examples updated; backward-compatible
  (defaults to udp,tcp,quic). 21 cli tests incl. a real-UDP-transport loopback.
- docs/sing-box.md: integration approach note (process-bridge now; native Go
  outbound for phones, with crypto-library mapping + KAT requirement).
- Normalize rustfmt across the v2 transport files (tcp/dial/udp contract).

Whole workspace: 97 tests pass, clippy -D warnings clean, fmt clean. Deploy flow
(pki init/issue-server/issue-client) validated with the release binary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:41:59 +03:00
xah30 cb89312a27 feat(cli): implement Wave 4 — aura binary (PKI, server/client, admin, bench)
aura-cli: clap command tree (pki init/issue-server/issue-client/revoke/list,
server, client, route add/list/remove, status, bench-crypto); TOML config with
~ expansion and split-tunnel rules -> RouteTable; JSON-over-Unix-socket admin
IPC; server/client data paths wiring transport + tunnel (TUN run needs root).
config/{server,client}.toml.example. 15 tests (pki roundtrip, config parse,
admin-socket roundtrip, loopback connection). Verified the real binary: --help,
bench-crypto, and a full CA->server->client cert workflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:36:13 +03:00
xah30 f78633e04f chore: scaffold Aura workspace skeleton (Stage 0)
- 6-crate Cargo workspace, dependency tree frozen (cargo check green in ~1m)
- ml-kem 0.3 (FIPS 203) replaces spec's pqcrypto-kyber for ML-KEM-768
- fix invalid target-gated workspace.dependencies: Windows deps (wintun/windows)
  declared untargeted, cfg-gated per-crate in aura-tunnel
- version bumps vs spec: tun 0.8, rcgen 0.14, wintun 0.5
- stub lib/main per crate; real implementations land wave by wave

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:42:40 +03:00