Skip to content

Instantly share code, notes, and snippets.

@trevormjaymes
Created February 16, 2026 21:01
Show Gist options
  • Select an option

  • Save trevormjaymes/e7eb50cadb9ec96d66987d97733a43ed to your computer and use it in GitHub Desktop.

Select an option

Save trevormjaymes/e7eb50cadb9ec96d66987d97733a43ed to your computer and use it in GitHub Desktop.

SSH Module

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.

Architecture

  ┌───────────────────────────────────────────────┐
  │  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)     │
  └──────────────┘          └──────────────┘

Why a Dedicated Thread

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.

Files

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

Configuration

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).

Authentication

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.

PTY Forwarding Pipeline

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)
  1. spawn_blocking thread reads from the PTY master fd in a loop
  2. Chunks are sent through a bounded mpsc channel (backpressure at 64 buffers)
  3. 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.

Host Key

Ed25519 key stored at /data/orbicd/ssh_host_key. Generated automatically on first start if missing. File permissions: 0600.

Dynamic Config Reload

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment