141 lines
8.0 KiB
Markdown
141 lines
8.0 KiB
Markdown
# 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`.
|