Files
dosh/SPEC.md
T
Codex 12beb04d4d
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
Support SSH aliases in Dosh client installs
2026-06-11 08:46:24 -04:00

18 KiB

dosh - Dormant Shell Spec

Status: Implemented Rust build with local and Docker SSH verification
Default language: Rust, unless benchmarks prove the stack is the bottleneck
Binaries: dosh-server, dosh-client, dosh-auth, dosh-bench
Helper mode: dosh-server auth or dosh-auth, invoked by SSH with -T


1. Product Shape

dosh is a fast-attach remote terminal. It borrows the useful shape of mosh - UDP transport, roaming, and latency-tolerant terminal rendering - but optimizes a different first-order problem: getting the user back into an already-running terminal as quickly as possible.

The daemon is resident. Sessions are named. A session owns one PTY and one authoritative terminal screen. Clients attach to that session over encrypted UDP. SSH is used for first trust establishment and as fallback when cached credentials are missing, expired, or rejected.


2. Design Goals

  • Connection speed first:
    • UDP resume: one encrypted UDP request and one encrypted UDP reply.
    • UDP attach ticket: one encrypted UDP request and one encrypted UDP reply.
    • Warm SSH bootstrap: existing-ControlMaster SSH command latency plus one UDP RTT.
    • Cold SSH bootstrap: cold ssh host terminal-ready time plus at most one UDP RTT.
  • No daemon spawn, PTY spawn, shell startup, rc file execution, or MOTD on attach to an existing session.
  • Prewarm configured sessions at daemon startup, including default by default.
  • Encrypted UDP terminal data.
  • Single configurable UDP port.
  • Multiple clients attached to one session.
  • Optional read-only clients.
  • Named persistent sessions.
  • Reuse existing SSH key infrastructure.
  • Instrument connection timing from the first implementation.

3. Non-Goals

  • Replacing SSH as the first public-key trust mechanism.
  • Multi-user authorization or ACLs.
  • Windows support in v0.
  • Full mosh protocol compatibility.
  • Perfect local echo/prediction in the first MVP.
  • QUIC in v0. QUIC can be revisited if measurements show custom UDP is not enough.

4. Connection Speed Contract

Measure terminal-ready time: elapsed time from launching dosh ... to first usable terminal render.

Benchmarks must use the same host, network, key, DNS path, and SSH config.

Path Acceptance gate
UDP resume <= one measured UDP RTT + local render time
UDP attach ticket <= one measured UDP RTT + local render time
Warm SSH bootstrap <= ssh host true over existing ControlMaster + one measured UDP RTT
Cold SSH bootstrap <= cold ssh host terminal-ready time + one measured UDP RTT
New session Report separately; PTY/shell creation is expected

Required timing evidence:

  • Client stderr spans for credential lookup, SSH bootstrap, UDP resume, UDP ticket attach, and terminal-ready time.
  • dosh-bench samples for SSH true, Dosh attach, and optional ControlMaster-backed SSH true.
  • make bench-docker-ssh gates both cold SSH bootstrap and ControlMaster-backed SSH bootstrap against containerized OpenSSH plus resident dosh-server.

5. Fast Path Order

The client always tries the cheapest path that is valid for the requested host/user/session/mode:

  1. UDP resume

    • Requires cached ClientId, session key, server identity, and unexpired resume metadata.
    • Sends ResumeRequest.
    • Receives ResumeOk with a snapshot or diff.
  2. UDP attach ticket

    • Requires cached attach ticket scoped to server identity, SSH username, session, mode, and expiry.
    • Sends TicketAttachRequest.
    • Receives AttachOk with session key, ClientId, and snapshot.
  3. SSH bootstrap

    • Runs ssh -T user@host dosh-auth ....
    • Receives attach token, attach ticket, session key material, and server metadata.
    • Sends BootstrapAttachRequest.
    • Receives AttachOk with ClientId and snapshot.
  4. New session

    • Same as attach, but if the session does not exist and is not prewarmed, the server creates PTY and shell before first paint.

6. Architecture

dosh-server
  config loader
  secret manager
  UDP socket on one port
  session table: HashMap<SessionName, Session>
  optional prewarm of configured sessions
  auth helper mode for SSH bootstrap
  metrics/timing logger

Session
  PTY master
  child process/shell
  terminal parser
  authoritative screen model
  scrollback ring
  monotonic output sequence
  client table: HashMap<ClientId, ClientState>

ClientState
  ClientId
  UDP endpoint
  mode: read-write | view-only
  session key id
  last acked sequence
  terminal size
  last seen timestamp

dosh-client
  config loader
  local credential cache
  terminal raw mode
  UDP protocol engine
  SSH bootstrap runner
  reconnect state machine
  renderer

Server hot-path ownership should avoid locks on every broadcast. A single event-loop owner per session is preferred. Cross-thread designs are allowed only if benchmarked.


7. Security Model

SSH is the first trust root. dosh does not implement a competing public-key login system in v0.

The UDP channel uses AEAD. Recommended default: ChaCha20-Poly1305 for portable speed, with AES-GCM allowed when hardware acceleration is known to be available. The negotiated algorithm is recorded in the bootstrap response.

All encrypted packets use:

  • Unique nonce per (session_key_id, direction).
  • Monotonic packet counter.
  • Associated data containing protocol version, packet type, session name hash, client id when known, and sequence numbers.
  • Replay rejection using the packet counter window.

Secrets:

  • server_secret: generated on first server start; stored mode 0600.
  • session_key: random 256-bit key per client attachment, rotated on SSH bootstrap or ticket attach.
  • attach_ticket_key: derived from server_secret and rotated by server key epoch.

No terminal bytes are sent outside AEAD after the attach handshake begins.


8. SSH Bootstrap Auth

Client command:

ssh -T user@host dosh-auth \
  --protocol 1 \
  --nonce <client_nonce> \
  --session <name> \
  --mode <read-write|view-only> \
  --size <cols>x<rows> \
  --client-version <version>

dosh-auth must:

  • Not allocate a PTY.
  • Not start a shell.
  • Not run user shell rc files.
  • Read server config and secret directly.
  • Return one compact binary or base64url response on stdout.
  • Exit immediately.

Bootstrap response fields:

  • protocol_version
  • server_id
  • server_key_epoch
  • issued_at
  • expires_at
  • user
  • session
  • mode
  • terminal_size
  • attach_token
  • attach_ticket
  • attach_ticket_psk
  • session_key
  • session_key_id
  • udp_host
  • udp_port
  • aead_algorithm

attach_token = HMAC-SHA256(server_secret, user || session || mode || terminal_size || client_nonce || issued_at || expires_at || session_key_id).

The token TTL defaults to 30 seconds. Attach tickets default to 1 hour and are server-configurable.


9. Attach Tickets

Attach tickets let a new client process attach without spawning SSH again.

Ticket properties:

  • Server-sealed and authenticated by attach_ticket_key.
  • Paired with a client-held random attach_ticket_psk returned during SSH bootstrap.
  • Scoped to server identity, SSH username, session, mode, and key epoch.
  • Short-lived by default.
  • Revoked implicitly when server secret/key epoch changes.
  • Stored client-side with mode 0600, along with attach_ticket_psk.

Ticket attach does not prove fresh possession of the SSH private key. It proves recent possession of a server-issued credential. This is acceptable for speed, configurable, and can be disabled with allow_attach_tickets = false.

Ticket attach flow:

  1. Client sends TicketAttachRequest containing the sealed ticket, client nonce, and requested terminal size.
  2. The request body is AEAD-encrypted with a key derived from HKDF(attach_ticket_psk, client_nonce || "ticket-attach-request").
  3. Server opens the sealed ticket, validates scope/expiry/key epoch, derives the same request key, and decrypts the request.
  4. Server creates a fresh session key and ClientId.
  5. AttachOk is AEAD-encrypted with HKDF(attach_ticket_psk, client_nonce || server_nonce || "ticket-attach-ok") and carries the fresh session key metadata plus first snapshot.
  6. Subsequent terminal packets use the fresh session key, not the ticket PSK.

10. UDP Protocol

UDP port defaults to 50000. One socket handles all sessions and clients.

Hot-path terminal packets use a fixed binary header:

magic      4 bytes   "DOSH"
version    1 byte    1
type       1 byte
flags      2 bytes
conn_id    16 bytes  zero before client id is assigned
seq        8 bytes   sender packet sequence
ack        8 bytes   latest received peer sequence
body_len   2 bytes
body       body_len bytes
tag        AEAD tag, length depends on algorithm

Packet types:

Type Direction Encrypted Purpose
BootstrapAttachRequest client -> server token-authenticated Attach after SSH bootstrap
TicketAttachRequest client -> server ticket PSK Attach with cached ticket
AttachOk server -> client yes Assign client id and send first snapshot
AttachReject server -> client no terminal bytes Reject and require SSH
ResumeRequest client -> server yes Resume known client
ResumeOk server -> client yes Endpoint updated; diff/snapshot follows
Input client -> server yes PTY input bytes
Resize client -> server yes Terminal size update
Frame server -> client yes Screen diff or PTY byte frame
Ack both yes Ack without payload
Ping / Pong both yes Keepalive and RTT
Detach client -> server yes Remove client, keep session

MTU target:

  • Default payload target: 1200 bytes.
  • Larger datagrams may be enabled only after path MTU discovery.
  • Snapshots larger than the target are chunked.

Reliability:

  • Input packets are reliable and ordered per client.
  • Output frames are sequenced; clients ack rendered sequence.
  • Server retransmits unacked frames within a bounded window.
  • If a client falls too far behind, server sends a fresh snapshot instead of replaying unlimited diffs.

11. Sessions and PTYs

Named sessions:

dosh                       # attach default
dosh attach                # attach default
dosh attach work
dosh attach work --view-only
dosh new work
dosh list
dosh list-clients [session]
dosh kill work

Session behavior:

  • One PTY per session.
  • Sessions persist until killed or server exits.
  • If a session has zero clients, the PTY keeps running.
  • Configured sessions are prewarmed at daemon startup.
  • If a requested session does not exist:
    • attach creates it only when create_on_attach = true.
    • new always creates it and fails if it already exists.

Resize policy:

  • One PTY means one size.
  • Read-write clients may resize.
  • View-only clients never resize.
  • Default policy: latest read-write resize wins.

12. Screen State

Server maintains the authoritative terminal model:

  • Visible grid.
  • Cursor position and style.
  • Alternate screen.
  • Text attributes and colors.
  • Scrollback ring.
  • Monotonic output sequence.

Initial attach:

  • Server sends a snapshot in the first UDP reply if it fits the packet budget.
  • If not, server sends a minimal first frame immediately and follows with chunks.

Diffs:

  • Diffs are computed per client from that client's last acked rendered sequence.
  • Lagging clients may receive larger diffs or a full snapshot.
  • Diffs are preferred over raw PTY bytes for reconnect correctness.

Encoding:

  • Hot terminal frames use fixed binary headers and compact binary payloads.
  • MessagePack is allowed only for non-hot control/list/config responses.
  • JSON is not used on the protocol path.

13. Multi-Client Model

Default mode is shared input. Any read-write client can write to the session PTY. All clients see the same resulting screen.

View-only mode:

  • Client suppresses local input.
  • Server rejects Input from view-only clients even if a malformed client sends it.
  • Promotion/demotion requires reconnect.

Client timeout:

  • Clients are removed after client_timeout_secs without ack/ping.
  • Removing a client never kills the session.

14. Local Echo

MVP local echo is conservative:

  • Printable keystrokes may be rendered optimistically only when the client is in a simple shell line-editing state.
  • Server output is always authoritative.
  • On mismatch, client replaces local prediction with server state.

Full mosh-style predictive display is a later feature. It must not delay the first implementation of fast attach/resume.


15. Reconnect and Roaming

Client detects possible disconnect when no server packet arrives for reconnect_timeout_secs.

Reconnect order:

  1. Send encrypted ResumeRequest to the configured host/port.
  2. If accepted, update endpoint server-side and receive diff/snapshot.
  3. If rejected, try attach ticket.
  4. If ticket attach is rejected, run SSH bootstrap.

The server matches resume by ClientId and session key id, not by source address. Successful resume updates the client's UDP endpoint.


16. Configuration

Server config: ~/.config/dosh/server.toml

port = 50000
bind = "0.0.0.0"
scrollback = 5000
auth_ttl_secs = 30
attach_ticket_ttl_secs = 3600
allow_attach_tickets = true
client_timeout_secs = 30
retransmit_window = 256
default_input_mode = "read-write"
prewarm_sessions = ["default"]
create_on_attach = true
shell = "/bin/sh"
sessions_dir = "~/.local/share/dosh/sessions"
secret_path = "~/.config/dosh/secret"

Client config: ~/.config/dosh/client.toml

server = "user@example.com"
# ssh_port = 22
dosh_port = 50000
default_session = "default"
reconnect_timeout_secs = 5
view_only = false
cache_attach_tickets = true
credential_cache = "~/.local/share/dosh/credentials"

17. Performance-First Stack

Default implementation:

# server/client shared
bytes
chacha20poly1305
aes-gcm optional
hmac
hkdf
sha2
rand
serde
toml

# server
mio or tokio    # benchmark; single-thread hot path either way
rustix          # PTY/process/syscall wrappers where possible
vt100           # authoritative terminal parser/model

# client
mio or tokio
crossterm       # raw terminal mode
vt100 optional  # only if client-side model is needed for prediction

Rules:

  • Benchmark mio vs single-thread tokio before committing to runtime.
  • Avoid locks on per-packet session broadcast.
  • Preallocate packet buffers.
  • Avoid serde on terminal frames.
  • Keep dosh-auth tiny and static where practical.
  • Optimize startup path before throughput.

18. MVP Scope

MVP must include:

  • dosh-server daemon.
  • dosh-auth SSH helper mode.
  • dosh-client.
  • One UDP port.
  • Prewarmed default session.
  • SSH bootstrap attach.
  • Attach-ticket UDP attach.
  • Encrypted UDP channel.
  • UDP resume.
  • Raw terminal input/output.
  • Basic resize.
  • Timing instrumentation.

MVP may defer:

  • Sophisticated predictive local echo.
  • Per-cell minimal diffs; raw frame plus snapshot fallback is acceptable initially if reconnect correctness is preserved.
  • Multi-session management commands beyond attach, new, list, and kill.

19. Verification Checklist

A build is not done until these are demonstrated:

  • Cold attach timing compared against cold ssh host.
  • Warm attach timing compared against ssh host true with ControlMaster.
  • UDP resume completes without spawning SSH.
  • Existing session attach does not spawn PTY or shell.
  • Prewarmed default exists before first client.
  • Terminal data is encrypted on UDP.
  • Replay counters reject duplicate encrypted packets.
  • View-only clients cannot write to PTY.
  • Multiple clients see the same screen.
  • Client survives source port/IP change by resume.
  • Snapshot fallback repairs a lagging client.
  • README.md and SPEC.md remain consistent with implemented behavior.

20. Status

Spec complete. The Rust implementation is present in this repository.

Implemented:

  • Rust workspace and binaries: dosh-server, dosh-client, dosh-auth.
  • Server config and secret creation.
  • SSH/local bootstrap response generation.
  • HMAC bootstrap verification.
  • ChaCha20-Poly1305 encrypted UDP packets.
  • Fixed DOSH packet header.
  • Resident server with prewarmed named PTY sessions.
  • Authoritative server-side vt100 terminal parser.
  • Full attach/resume snapshots from terminal screen state.
  • Per-client screen-state diffs for broadcast frames.
  • Raw terminal client attach.
  • UDP resume from cached client credentials.
  • Sealed attach-ticket UDP attach after server restart or unknown-client resume.
  • Client ACKs and server-side bounded pending retransmit window.
  • Sliding replay window for encrypted client packet counters.
  • View-only server-side input rejection.
  • Basic resize handling.
  • Timing output for bootstrap and terminal-ready.
  • dosh-bench benchmark harness for attach timing, SSH key/known-host options, and ControlMaster-backed SSH measurement.
  • Hardened user systemd unit.
  • Release install script.
  • Docker OpenSSH benchmark gate covering cold SSH bootstrap and ControlMaster-backed SSH bootstrap.
  • GitHub Actions CI for format, tests, release build, and Docker SSH benchmark gate.
  • Optional GitHub Actions remote benchmark job gated by repository secrets.
  • Auth/protocol tests.
  • Integration smoke tests for local attach, ticket attach after server restart, and view-only input rejection.
  • Integration tests for retransmit, resize, multi-client shared screen, and UDP endpoint roaming.

Optional deployment evidence:

  • Configure DOSH_BENCH_HOST, DOSH_BENCH_USER, and DOSH_BENCH_SSH_KEY repository secrets to run the same benchmark against a real remote host in addition to the Docker OpenSSH gate.