SSH server for authenticated remote shell access to the device, using the russh crate. Supports password and public key authentication with PTY allocation for interactive shells and exec requests for one-shot commands.
┌───────────────────────────────────────────────┐
│ Dedicated OS thread ("ssh-server") │
│ Own tokio current_thread runtime │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ run() — outer loop (config changes) │ │
│ │ ┌──────────────────────────────────────┐│ │
│ │ │ accept loop — TcpListener ││ │
│ │ │ ┌────────────────────────────┐ ││ │
│ │ │ │ SshSession (per client) │ ││ │
│ │ │ │ ├─ auth (password/pubkey) │ ││ │
│ │ │ │ ├─ pty_request → PTY+shell│ ││ │
│ │ │ │ ├─ exec_request → cmd │ ││ │
│ │ │ │ └─ data → PTY write │ ││ │
│ │ │ └────────────────────────────┘ ││ │
│ │ └──────────────────────────────────────┘│ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
▲ │
│ Event::SshConfigChanged │ PTY I/O
│ Event::Shutdown ▼
┌──────────────┐ ┌──────────────┐
│ orbicd main │ │ /bin/sh │
│ (LocalSet) │ │ (child) │
└──────────────┘ └──────────────┘
russh uses tokio::spawn internally, which requires a multi-threaded or current-thread runtime. orbicd's main loop runs inside a tokio::task::LocalSet, where tokio::spawn futures don't get polled. The SSH server therefore gets its own OS thread with a separate current_thread runtime.
| File | Purpose |
|---|---|
mod.rs |
Server lifecycle: spawn_threaded(), run() loop, host key management, russh config |
config.rs |
SshConfig — JSON persistence at /data/orbicd/ssh.json, password hashing |
session.rs |
SshSession — per-client handler: auth, PTY, shell, exec, data forwarding |
Stored at /data/orbicd/ssh.json (created with defaults on first use):
{
"enabled": true,
"port": 22,
"shell": "/bin/sh",
"username": "root",
"password_hash": "<sha256 hex>"
}Default password: orbic. Changed via the web API (PUT /api/ssh/config).
File permissions: 0600 (contains password hash).
Two methods (both enabled by default):
Password: Checked against SshConfig.username + SHA-256 hash of the password. Config is re-read from disk on each auth attempt, so changes take effect immediately.
Public key: Keys loaded from /data/orbicd/authorized_keys (OpenSSH format, one key per line). File is re-read on each auth attempt.
Interactive shell sessions use a two-stage pipeline to bridge blocking PTY I/O with async SSH:
PTY master fd ──blocking read──▶ mpsc(64) ──async recv──▶ SSH channel
(spawn_blocking) (tokio::spawn)
spawn_blockingthread reads from the PTY master fd in a loop- Chunks are sent through a bounded
mpscchannel (backpressure at 64 buffers) - An async task receives chunks and writes them to the SSH channel
Client input flows the other direction: data() handler writes directly to the PTY master fd via libc::write.
Ed25519 key stored at /data/orbicd/ssh_host_key. Generated automatically on first start if missing. File permissions: 0600.
The outer run() loop listens for Event::SshConfigChanged on the broadcast channel. On config change, the accept loop breaks, the listener is dropped, and the server re-binds with new settings (port, enabled/disabled). Active sessions continue on their spawned tasks.
Event::Shutdown causes the server to exit cleanly.