forked from mincomk/rsh
feat: rshc: add sshserve
This commit is contained in:
140
CLAUDE.md
Normal file
140
CLAUDE.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
`rsh` is a self-hosted reverse-shell / remote-access system, similar in spirit to a lightweight
|
||||
teleport/ssh-reverse-tunnel setup. A central **backend** exposes a public HTTP/WebSocket endpoint.
|
||||
Target machines run a tiny **stub** binary that dials out to the backend and registers under a
|
||||
named **session**. An **operator** uses the `rshc` CLI, authenticated with an SSH keypair, to list
|
||||
sessions/connections and attach an interactive shell to any connected stub.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
Cargo workspace with 4 crates (`Cargo.toml` at repo root):
|
||||
|
||||
- `crates/rsh-types` — shared wire protocol types only (`StubMsg`, `BackendStubMsg`, `OpReq`,
|
||||
`OpResp`, `OpEvent`, `OpMsg`, `BackendOpMsg`, ...), all serde-tagged enums (`#[serde(tag = "t", content = "c")]`).
|
||||
This is the contract between all three binaries; changing a variant here touches backend, stub,
|
||||
and client.
|
||||
- `crates/rsh-backend` — the server (`axum`). Binary: `rsh-backend`.
|
||||
- `crates/rsh` — the stub agent that runs on a target machine. Binary: `rsh`. Statically linked
|
||||
(musl) so it runs on arbitrary Linux hosts with no dependencies.
|
||||
- `crates/rshc` — the operator's CLI client. Binary: `rshc`.
|
||||
|
||||
## Build & dev commands
|
||||
|
||||
There are no automated tests in this repo currently.
|
||||
|
||||
```sh
|
||||
cargo build # build everything (debug)
|
||||
cargo build --release # release build
|
||||
cargo check # fast typecheck
|
||||
cargo clippy # lint
|
||||
cargo fmt # format
|
||||
```
|
||||
|
||||
Backend development requires the musl stub binaries to be built first and embedded via env vars
|
||||
(see "Stub embedding" below) — use the `justfile` targets rather than raw cargo:
|
||||
|
||||
```sh
|
||||
just stubs # cross-compiles the `rsh` stub for x86_64 and aarch64 musl targets
|
||||
just backend # builds stubs, then builds rsh-backend with them embedded (release)
|
||||
just dev # builds stubs, then `cargo run -p rsh-backend` bound to 127.0.0.1:7777,
|
||||
# data dir /tmp/rsh-dev
|
||||
```
|
||||
|
||||
`rshc` and `rsh` (stub) can be built/run directly with plain `cargo build -p rshc` /
|
||||
`cargo run -p rsh -- <subcommand>` — they don't need the stub-embedding dance.
|
||||
|
||||
Container image (per user's global config, use `podman`/`podman compose` in place of any
|
||||
`docker`/`docker compose` commands referenced by the justfile or docs):
|
||||
|
||||
```sh
|
||||
just docker-build # docker build -t rsh-backend:local .
|
||||
just docker-run # run the built image, mounts a data volume, exposes 7777
|
||||
just docker-publish # tag + push to registry.walruslab.org/pub/rsh-backend
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Protocol split: two independent WebSocket channels
|
||||
|
||||
- `/ws/stub` (`rsh-backend/src/ws_stub.rs`) — target-machine stubs connect here. Messages:
|
||||
`StubMsg` (stub → backend: `Hello`, `Stdout`/`Stderr`, `Exited`, `ShellReady`/`ShellStdout`/
|
||||
`ShellStderr`/`ShellExited`, `Pong`) and `BackendStubMsg` (backend → stub: `Accepted`/`Rejected`,
|
||||
`Stdin`, `Resize`, `Kill`, `Ping`, `SpawnShell`/`ShellStdin`/`ShellResize`/`ShellKill`).
|
||||
- `/ws/op` (`rsh-backend/src/ws_op.rs`) — the `rshc` operator client connects here. Messages:
|
||||
`OpMsg`/`BackendOpMsg` wrapping request/response bodies `OpReq`/`OpResp` and pushed `OpEvent`s
|
||||
(new/closed connections, new/deleted sessions).
|
||||
|
||||
The backend is the hub connecting the two: `AppState` (`rsh-backend/src/state.rs`) holds live
|
||||
stub connections (`connections: DashMap<(session_id, conn_id), Arc<ConnHandle>>`) and forwards
|
||||
data between an operator's `AttachSink` and a stub's channel. `AttachIO` frames from an operator
|
||||
become `Stdin`/`Resize`/`Kill` messages sent down `ConnHandle::to_stub`; stdout/stderr coming back
|
||||
from the stub is forwarded to whichever operator is currently attached.
|
||||
|
||||
### Sessions, connections, shells
|
||||
|
||||
- A **session** is a named, optionally password-protected registration point (`SessionRecord`,
|
||||
persisted to `sessions.json` via `rsh-backend/src/persist.rs`). Created/deleted/updated by
|
||||
operators (`rshc session ...`).
|
||||
- A **connection** is one stub process currently connected under a session (a session can have
|
||||
multiple simultaneous stub connections, e.g. multiple machines using the same session name).
|
||||
Connection IDs are allocated per-session (`AppState::alloc_conn_id`).
|
||||
- Each connection has one primary attach point (`ConnHandle::attach`) plus any number of
|
||||
**extra shells** (`ConnHandle::extra_shells`, keyed by `shell_id`), spawned on demand via
|
||||
`OpReq::SpawnShell` → `BackendStubMsg::SpawnShell`. The backend tracks pending spawns in
|
||||
`AppState::spawn_shell_pending` and waits (with a 10s timeout) for the stub's `ShellReady`
|
||||
before replying to the operator.
|
||||
- Only one operator can be attached to a given connection/shell at a time; attaching replaces
|
||||
whatever `AttachSink` was previously stored.
|
||||
|
||||
### Auth model (two different mechanisms for two different actors)
|
||||
|
||||
- **Stub auth** (`rsh-backend/src/auth.rs::verify_password`): sessions may have an argon2 password
|
||||
hash. The stub either checks it up front via `GET /check-auth?s=<session>&pw=<pw>` (used by
|
||||
`rsh auth` to cache a working password locally in `~/.config/rsh.yaml`) and/or presents it in the
|
||||
`Hello` message on `/ws/stub`.
|
||||
- **Operator auth** (`rsh-backend/src/ws_op.rs::auth_handshake`): SSH public-key challenge/response.
|
||||
Client sends `AuthInit{pubkey_openssh}`; backend checks it against `authorized_keys` (file on
|
||||
disk, reloadable via `rshc keys ...`, plus keys baked in via `RSH_AUTHORIZED_KEYS` env var —
|
||||
`rsh-backend/src/keys.rs`), sends a random 32-byte `Challenge`, and expects a signature over that
|
||||
nonce in SSH-signature (`SshSig`) format using namespace `"rsh-auth"`. `rshc` signs with the
|
||||
private key at `ssh_key_file` from its config (prompting for a passphrase if encrypted).
|
||||
|
||||
### Stub self-install / bootstrap (`rsh-backend/src/dist.rs`)
|
||||
|
||||
`GET /` or `/run.sh?s=<session>` serves a POSIX shell script (`SCRIPT_TEMPLATE`) that detects
|
||||
arch, downloads the matching stub binary from `/rsh/:arch`, runs `<stub> auth --url <ws> <session>`
|
||||
to cache the session password, then launches `<stub> stub --url .../ws/stub --session <session>`
|
||||
as a backgrounded, disowned process. This is the one-liner meant to be curled onto a target host.
|
||||
|
||||
### Stub embedding (why `just backend` needs `just stubs` first)
|
||||
|
||||
`rsh-backend/build.rs` embeds two prebuilt `rsh` binaries into the `rsh-backend` binary itself via
|
||||
`include_bytes!` (see `dist.rs`'s `STUB_X86_64`/`STUB_AARCH64` statics), reading their paths from
|
||||
the `RSH_STUB_X86_64`/`RSH_STUB_AARCH64` env vars at build time. If those env vars aren't set (or
|
||||
point to a missing file), the build succeeds anyway but embeds an empty placeholder, and `/rsh/:arch`
|
||||
serves `503` for that arch instead of the binary — this lets `cargo build -p rsh-backend` work
|
||||
standalone for iterating on non-stub-serving code, at the cost of a backend that can't actually
|
||||
bootstrap new targets. The `Dockerfile` does the equivalent two-phase build (musl cross-compile
|
||||
stages `stub-amd64`/`stub-arm64`, then `cargo chef` for the backend) so the shipped image always
|
||||
has both stubs embedded.
|
||||
|
||||
### `rshc` CLI structure
|
||||
|
||||
`crates/rshc/src/cmd/` has one module per subcommand family (`session`, `connection`, `connect`,
|
||||
`shell`, `keys`, `watch`); `main.rs` just parses `clap` args and dispatches. `AuthedClient`
|
||||
(`crates/rshc/src/auth.rs`) owns the `/ws/op` connection after the handshake, multiplexing
|
||||
request/response pairs by an incrementing `id` (oneshot for single responses, `mpsc` for streaming
|
||||
ones like `Watch`/`Attach`) over one shared writer task. Config lives at `~/.config/rshc.yaml`
|
||||
(`backend_url`, `ssh_key_file`) — `rshc` writes a stub file and errors out on first run if missing.
|
||||
|
||||
### Deployment
|
||||
|
||||
`Dockerfile` + `docker-compose.yml` + Helm chart at `deploy/helm/rsh-backend/` (standard
|
||||
Deployment/Service/Ingress/PVC/Secret/ServiceAccount). Backend runtime config is all env vars
|
||||
(`rsh-backend/src/config.rs`): `RSH_DATA` (default `/var/lib/rsh`), `RSH_BIND` (default
|
||||
`0.0.0.0:7777`), `RSH_LOG`, `RSH_AUTHORIZED_KEYS`.
|
||||
Reference in New Issue
Block a user