Skip to content

Server admin API + ctl, then session-hello wire refactor (v0.7.0 + v0.8.0) - #39

Merged
guyte149 merged 2 commits into
masterfrom
feat/admin-api
May 4, 2026
Merged

Server admin API + ctl, then session-hello wire refactor (v0.7.0 + v0.8.0)#39
guyte149 merged 2 commits into
masterfrom
feat/admin-api

Conversation

@guyte149

@guyte149 guyte149 commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Two related commits on this branch:

  1. 066a9ee — Add server admin API + rusnel ctl (read-only, v0.7.0). Phase-1 of the long-tracked operability item: a read-only HTTP API on a unix socket (~/.rusnel/admin.sock, mode 0600) exposing live clients, tunnels, conns, per-tunnel byte counters, and a bounded disconnect-history ring buffer. New rusnel ctl subcommand wraps the API for the shell. Three-layer client / tunnel / conn data model in ServerState + DTOs.
  2. 6c80f95 — Replace per-stream RemoteRequest with session hello + OpenConn (v0.8.0). Wire-protocol overhaul that follows chisel's session-config model. The 0.7 protocol re-sent a full RemoteRequest on every data-plane bi-stream and used an announce_only band-aid for forward-tunnel admin visibility; both are gone. Clients now declare every tunnel up front in SessionHello, server validates the batch and assigns tunnel_ids, every subsequent bi-stream opens with a tiny OpenConn { tunnel_id, dynamic } frame. SOCKS5 dynamic targets ride along as OpenConn::dynamic under the parent SOCKS tunnel, removing the from_socks flag.

Breaking

  • 0.8 wire format is not backward compatible with 0.7.x — upgrade both ends together.
  • RemoteRequest::announce_only / from_socks and dynamic_tcp / dynamic_udp constructors removed.
  • ServerConfig gains an admin_socket: Option<PathBuf> field (0.7).
  • Tunnel handler signatures take a new tunnel_id: u64 argument (0.8) and a Counters argument (0.7).
  • Forward SOCKS5 with --allow-socks=false now fails the entire session at hello time instead of binding a local listener and rejecting per-CONNECT.

Highlights

  • New /api/v1/{server,clients,clients/:id,clients/:id/{tunnels,conns},tunnels,tunnels/:id,tunnels/:id/conns,conns,conns/:id,history} endpoints.
  • rusnel ctl {server,clients,client,client-conns,tunnels,tunnel,tunnel-conns,conns,history} with table or --json output.
  • --no-admin-socket flag to opt out; --admin-socket <path> to override.
  • Single declaration path replaces three different ones (forward per-conn, reverse control, 0.7 announce).
  • find_or_create_tunnel + tunnel_index dedup hack collapsed into a simple bulk register_tunnels — the protocol no longer needs runtime dedup.

Test plan

  • cargo build --release
  • cargo fmt --all -- --check
  • cargo clippy --all -- -D warnings
  • cargo test --all — full suite passes (admin, auth, combinations, concurrent, disconnect_cleanup, edge_cases, ipv6, large_transfer, proxy, reconnect, tunnels)
  • Manual smoke: rusnel server --admin-socket /tmp/r.sock + rusnel client --remote 1080:google.com:80 + rusnel ctl --socket /tmp/r.sock tunnels shows the tunnel before and after pushing traffic

Made with Cursor

guyte149 and others added 2 commits May 4, 2026 17:28
Phase-1 of the long-tracked admin observability item: the server now
exposes a read-only HTTP API over a unix domain socket so operators can
list connected clients, the tunnels each client declared, the live conns
going through every tunnel, and per-tunnel / per-conn byte counters.
A new `rusnel ctl` subcommand wraps the API for the shell.

State model is three layers, each with one meaning: a *client* is one
connected `rusnel client` daemon; a *tunnel* is the remote declaration
that client established (deduped per client by spec, atomically via a
DashMap entry index); a *conn* is one proxied network connection going
through a tunnel (one accepted local TCP, one per-source UDP flow, one
SOCKS5 CONNECT, one SOCKS5 UDP target). Tunnels accumulate cumulative
counters as conns close; conns expose live counters plus a free-form
`peer` label.

Admin socket defaults to `~/.rusnel/admin.sock`, mode 0600. Override
with `--admin-socket <path>`; disable with `--no-admin-socket`. Routes:
`GET /api/v1/{server,clients,clients/:id,clients/:id/{tunnels,conns},
tunnels,tunnels/:id,tunnels/:id/conns,conns,conns/:id,history}`.

`rusnel ctl` subcommands mirror the routes: `server`, `clients`,
`client <id>`, `client-conns <id>`, `tunnels`, `tunnel <id>`,
`tunnel-conns <id>`, `conns`, `history`. Tab-aligned tables by default,
`--json` for the raw payload, `--socket <path>` to override.

Implementation:
- `src/server/state.rs` — `ServerState` (DashMaps of clients/tunnels/
  conns, ring-buffered disconnect history), `TunnelHandle` for reverse
  handlers, `ConnGuard` RAII for per-conn registration, DTOs.
- `src/server/admin.rs` — axum router served over hyper http1 on a
  `tokio::net::UnixListener`; bind tightens permissions to 0600.
- `src/common/counted.rs` — `TunnelCounters` (atomic in/out) and
  `CountedReader` to bump bytes_in zero-copy on the TCP path; UDP /
  SOCKS5 paths bump per datagram.
- `src/ctl/mod.rs` — minimal HTTP/1 client + table renderer (no
  dependency on a full HTTP client).
- Reverse handlers (`tunnel_tcp_client`, `tunnel_udp_client`,
  `tunnel_socks_client`) take `TunnelHandleOpt` and register a conn
  per accept / per-source / per-CONNECT.
- Forward handlers stay session-shaped (one bi-stream = one conn).

Phase-2 follow-ups (kick client, kill conn, Prometheus `/metrics`,
embedded web UI) are tracked in the README TODO section.

Tests: `tests/admin.rs` covers socket perms (0600), tunnel/conn dedup
under concurrency, and the cumulative-vs-active counter split. Existing
tests updated for the new tunnel-handler signatures.

Co-authored-by: Cursor <cursoragent@cursor.com>
The 0.7 wire protocol re-sent a full `RemoteRequest` on every
data-plane bi-stream and used an `announce_only` band-aid to make
forward tunnels visible in admin state before traffic flowed. Replace
both with chisel's session-config model: one `SessionHello` carrying
every declared remote at connect time, the server validates the whole
batch against `--allow-reverse` / `--allow-socks` and assigns one
`tunnel_id` per declaration, then every subsequent bi-stream opens
with a tiny `OpenConn { tunnel_id, dynamic }` frame. SOCKS5 dynamic
targets ride along as `OpenConn::dynamic` under their parent SOCKS
tunnel — no more `from_socks` flag.

This is a breaking wire change (0.8 client cannot talk to a 0.7
server). Forward SOCKS5 with `--allow-socks=false` is now rejected at
hello time rather than per-CONNECT, so the client's local SOCKS
listener no longer binds in that configuration; test updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
@guyte149
guyte149 merged commit fc3278e into master May 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant