dragoon-server 0.1.0

Public-relay server for the dragoon remote-executor: axum + rusqlite + ed25519 task signing + per-user message inbox.
Documentation

dragoon

crates.io: dragoon-proto License: MIT OR Apache-2.0

Disconnected Rapid Guidance Once-deployed Operational Network — a public-relay command executor for long-running experiments, named after the Gundam SEED 浮游炮 system since the architecture is isomorphic: a controller pilots remote autonomous nodes that execute tasks under cryptographically authenticated commands.

The model is ctl → server → worker(s) over HTTPS: every component connects out, so no NAT/firewall holes are needed on the worker boxes.

Three binaries, one Cargo workspace:

Binary Role
dragoon-server Public-relay HTTP server (axum + rusqlite). The only host that needs an inbound port.
dragoon-worker Execution agent. Polls the server, runs shell payloads, streams logs/artifacts back.
dragoon-ctl Controller CLI. Submits commands, tails logs, drains the message inbox.

The original Python implementation lives in git history (last commit: 8cce0de deletes it). Wire format, SQLite schema, canonical strings and fingerprints are byte-for-byte unchanged — fixture parity tests in crates/dragoon-proto/tests/ lock that down.

See docs/plans/2026-04-27-remote-executor-design.md for the full design (auth model, protocol, state machine).

Build

cargo build --release --workspace
# produces target/release/{dragoon-server, dragoon-worker, dragoon-ctl}

Tests: cargo test --workspace (~120 unit + integration tests, including 4 byte-for-byte parity fixtures and the auth_routes axum integration tests).

Lint: cargo clippy --workspace --all-targets -- -D warnings is clean.

Quick start (loopback, no TLS)

DATA=/tmp/re-demo
mkdir -p "$DATA" && ssh-keygen -t ed25519 -f "$DATA/id" -N "" -q
cat > "$DATA/server.toml" <<EOF
[server]
data_dir = "$DATA/srv"
bind_host = "127.0.0.1"
bind_port = 8765
public_url = "http://127.0.0.1:8765"
EOF

# admin pipeline
echo hunter2 | ./target/release/dragoon-server user create --name alice --data-dir "$DATA/srv"
TOTP_SECRET=...                                 # printed by the previous step
./target/release/dragoon-server user pubkey add --user alice --pub "$DATA/id.pub" --data-dir "$DATA/srv"
CODE=$(./target/release/dragoon-server token issue --worker lab-w1 --data-dir "$DATA/srv" | awk '{print $NF}')

# server up
./target/release/dragoon-server run --config "$DATA/server.toml" &

# worker register + run (on the same or a different box)
./target/release/dragoon-worker init --server http://127.0.0.1:8765 --code "$CODE" \
    --name lab-w1 --workdir "$DATA/wd" --state-file "$DATA/state.json"
./target/release/dragoon-worker run --name lab-w1 --state-file "$DATA/state.json" \
    --workdir "$DATA/wd" --server http://127.0.0.1:8765 &

# ctl: log in, submit, watch
TOTP=$(oathtool --base32 --totp "$TOTP_SECRET")
printf 'hunter2\n%s\n' "$TOTP" | ./target/release/dragoon-ctl login \
    --server http://127.0.0.1:8765 --username alice --ssh-key "$DATA/id"
./target/release/dragoon-ctl worker list
TID=$(./target/release/dragoon-ctl submit lab-w1 -c "echo hello-from-rust && hostname")
./target/release/dragoon-ctl tail "$TID"
./target/release/dragoon-ctl events

Cross-compile a worker for another arch

The xtask cargo subcommand detects the target's arch over SSH and builds a static musl binary against the right Rust target triple, then scps it over and installs.

cargo xtask deploy-worker --host me@some-arm-box
# detects Linux aarch64 -> aarch64-unknown-linux-musl
# cargo build --release -p dragoon-worker --target aarch64-unknown-linux-musl
# scp -> /tmp/dragoon-worker.new.<pid> -> install -m 0755 to /usr/local/bin/dragoon-worker

Build all supported triples for an offline distribution:

cargo xtask release-all-triples
ls dist/   # dragoon-worker-x86_64-unknown-linux-musl
           # dragoon-worker-aarch64-unknown-linux-musl
           # dragoon-worker-armv7-unknown-linux-musleabihf

If a target's musl linker isn't on PATH, cargo xtask doctor --triple T tells you what to install (apt install musl-tools, aarch64-linux-musl-cross, etc.) — or fall back to cargo install cross and use cross build directly.

macOS targets (x86_64-apple-darwin, aarch64-apple-darwin) only build on a mac host — Linux→Mac cross-compilation is intentionally out of scope.

Workspace layout

crates/
    dragoon-proto/   shared protocol primitives (constants, wire format,
                canonical strings, signers, models). Pure-Rust, no
                tokio/reqwest/sqlite. The byte-parity fixture tests
                live here.
    dragoon-server/  axum app + rusqlite (bundled) + argon2id + totp-rs +
                ed25519/RSA key handling. Routes:
                  /v1/auth/{challenge,login,logout}
                  /v1/workers, /v1/workers/{name}/fetch
                  /v1/tasks, /v1/tasks/{id}/{cancel,log,artifact}
                  /v1/messages, /v1/messages/ack
                  /v1/worker/{init,poll,log,finish,blob}
    dragoon-worker/  reqwest + nix (setsid + killpg) + globset + walkdir.
                Spawns bash -c in its own process group; threaded
                stdout/stderr readers; SIGTERM→grace→SIGKILL on cancel.
    dragoon-ctl/     reqwest signed-request client (KeyFileSigner or
                AgentSigner over SSH_AUTH_SOCK) + clap CLI.
    dragoon-testkit/ dev-only fixture loaders + harness.
xtask/          cross-compile orchestrator (see above).
fixtures/       JSON fixtures committed to repo; the source of truth
                for canonical_request, canonical_task, fingerprints
                and ed25519 signatures.
.claude/skills/ Project-level Claude Code skills (deploy-server,
                deploy-worker, send-command).

Authentication summary

ctl → server

Every business request must carry all of:

  • Authorization: Bearer <session_token>
  • X-RE-Timestamp (±60 s window)
  • X-RE-Nonce (5-min one-shot)
  • X-RE-Key-Fingerprint (must match the session-bound fp)
  • X-RE-Signature (SSH-wire ed25519 / rsa-sha2-512 signature over the RE-V1 canonical request bytes)

KeyFileSigner (loaded private key) and AgentSigner (talks the SSH agent protocol over SSH_AUTH_SOCK) both produce the wire format the server's verify_ssh_wire_signature accepts.

server → worker

Every assigned task is signed by the per-server ed25519 task-signing key. Worker pins the public key on init and rejects any out-of-signature, mismatched-name, or out-of-order-seq task with error="bad_task_signature: <reason>" before spawning a subprocess.

License

Dual-licensed under either of:

at your option.