DuProcess 8b1af51bc6 Persist sessions across server restarts via per-session holders
Make dosh terminal sessions survive a dosh-server restart
(crash/upgrade/systemctl restart): a reattaching client lands on the
SAME shell with screen state intact, instead of a fresh one.

Design: when persist_sessions is on (new config, default true) each
terminal session's shell runs in a small detached per-session holder
process (the dosh-server binary re-exec'd as `dosh-server hold`). The
holder double-forks + setsid into its own session, opens the PTY, spawns
the shell as ITS child, and serves the PTY master fd over a per-session
Unix socket via SCM_RIGHTS. The server adopts that fd to build a PtyHandle
that DETACHES (does not kill the shell) on drop, so a server exit leaves
holder + shell alive. On startup the server scans the runtime dir under
sessions_dir/run, reconnects to each live holder, receives the master fd
again, restores the persisted vt100 screen, and rebuilds the Session.

Screen/scrollback (server-memory only) is mirrored to disk atomically:
byte-throttled on the output hot path plus a 2s periodic flush, and
restored on re-adoption so reattach repaints. Truly-abandoned persistent
sessions are still reaped per the existing grace logic by asking the
holder to shut down. Anything in the persistent path failing degrades
gracefully to the original in-process shell.

PtyHandle gains an Owned (kill-on-drop, unchanged default) vs Adopted
(detach-on-drop) backing; resize on an adopted master uses TIOCSWINSZ.

No wire-format change, so protocol::VERSION stays 3. New deps: libc
(direct). New config: persist_sessions (default true); existing
integration tests pin it false to keep exercising the non-persistent
path unchanged.

Adds tests/integration_smoke.rs::session_survives_server_restart_same_
shell_and_screen: attaches, sets `cd /tmp; export MARK=...`, paints a
screen marker, KILLs the server, restarts it on the same config, reattaches
and asserts same shell pid + MARK + PWD + restored screen. Plus persist.rs
unit tests (name round-trip, screen save/load, dead-holder scan cleanup,
SCM_RIGHTS fd round-trip). cargo fmt --check and cargo test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:19:09 -04:00
2026-06-11 08:42:28 -04:00
2026-06-11 09:49:53 -04:00
2026-06-14 10:42:03 -04:00

dosh - Dormant Shell

dosh is a low-latency remote terminal designed around fast attach and fast reconnect. It is mosh-shaped, but not a mosh clone: the server is a resident daemon, terminal sessions stay hot, and repeat connects try encrypted UDP before starting SSH.

The core target is simple:

  • First secure trust establishment uses SSH.
  • Existing sessions attach in one encrypted UDP exchange whenever cached credentials allow it.
  • Reconnect after sleep, roaming, or network change resumes in one encrypted UDP exchange.
  • Cold SSH fallback stays competitive with plain ssh by doing less after auth.

Why not just mosh?

mosh is excellent at roaming and high-latency interactivity. Its startup path still has work dosh can avoid:

  1. SSH connects to the host.
  2. SSH starts mosh-server.
  3. The client receives connection material over SSH.
  4. SSH exits and the mosh UDP session begins.

dosh keeps dosh-server running before the client arrives. Named PTY sessions can also be prewarmed, so attaching to default does not need to spawn a daemon, create a PTY, or start a shell on the user's critical path.

This is not an encryption argument against mosh. dosh also encrypts its UDP data channel; the speed difference comes from keeping the server and session hot.

Fast Path Order

The client always tries the cheapest valid path first:

  1. UDP resume: existing ClientId and session key. No SSH. One encrypted UDP request, one encrypted UDP reply.
  2. UDP attach ticket: cached server-issued attach ticket for the same host/user/session/mode. No SSH. One encrypted UDP request, one encrypted UDP reply.
  3. SSH bootstrap: ssh -T user@host ~/.local/bin/dosh-auth ..., then one encrypted UDP attach.
  4. New session: same as attach, but the server must create the PTY/shell unless the session was prewarmed.

The fastest path is not a custom SSH replacement. SSH remains the first trust root; dosh removes SSH from repeat attaches when the server has already issued valid credentials.

Attach tickets are implemented because they are the way a fresh client process can skip SSH after a recent successful bootstrap.

Connection Speed Contract

dosh is measured by terminal-ready time: elapsed time from running dosh host to the first usable terminal screen.

  • UDP resume: <= one measured UDP RTT + local render time.
  • UDP attach ticket: <= one measured UDP RTT + local render time.
  • Warm attach with ControlMaster: <= ssh host true over the existing master + one measured UDP RTT.
  • Cold attach without ControlMaster: <= cold ssh host terminal-ready time + one measured UDP RTT.
  • New session: measured separately because it may need PTY and shell creation.

Server-side client resume state is kept for a day by default, so a sleeping laptop can resume the same encrypted client association without depending on a still-valid attach ticket. Attach tickets remain the fast path for fresh client processes.

The client emits timing spans for credential lookup, SSH bootstrap, UDP resume, UDP ticket attach, and terminal-ready time.

Architecture

dosh-server
  UDP socket on one configurable port
  session table keyed by name
  one PTY per named session
  optional prewarmed sessions, default ["default"]
  terminal parser/screen state per session
  client table per session
  encrypted UDP protocol
  tiny SSH-invoked dosh-auth helper mode
  persistent sessions (persist_sessions, default on): each shell runs in a
    detached per-session holder process; the server adopts its PTY master fd via
    SCM_RIGHTS and re-adopts it after a restart, so sessions survive crash/upgrade

dosh-client
  terminal raw mode
  local credential cache
  UDP resume/attach first
  SSH bootstrap fallback
  PTY input/output forwarding
  reconnect and roaming state machine

Install

Default UDP port: 50000. This is intentionally inside the common forwarded range 50000-52000/udp.

Install on each Linux server you want to attach to:

curl -fsSL https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh \
  | DOSH_REPO=https://git.palav.dev/Palav/dosh.git DOSH_PORT=50000 sh -s -- server

Install the client on macOS:

curl -fsSL https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh \
  | DOSH_REPO=https://git.palav.dev/Palav/dosh.git DOSH_SERVER=palav DOSH_HOST=git.palav.dev DOSH_PORT=50000 sh -s -- client

Update an installed client later:

dosh update

Install the client on Windows PowerShell:

$env:DOSH_REPO="https://git.palav.dev/Palav/dosh.git"; $env:DOSH_SERVER="palav"; $env:DOSH_HOST="git.palav.dev"; $env:DOSH_PORT="50000"; irm https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1 | iex

Attach:

dosh palav

Plain dosh palav opens a fresh terminal session. Use named sessions when you want to reattach to the same persistent terminal from multiple clients:

dosh --session work palav
dosh --session logs palav

Press Ctrl-] to detach the current client while leaving the server session alive. Typing exit in the remote shell closes that Dosh session and returns the client. If UDP packets stop arriving, Dosh keeps the terminal open, sends keepalive pings, and attempts ticket-based reconnect. While the link is silent it shows a single, non-destructive status line on the bottom screen row ([dosh] last contact Ns ago — reconnecting…), drawn with save/restore cursor so it never moves the app cursor or corrupts a full-screen TUI, and cleared the instant packets resume. (The old in-band overlay wrote over the active line and could corrupt TUIs; this draws only the last row.) Disable it with disconnect_status = false in client.toml or DOSH_DISCONNECT_STATUS=0.

If SSH and UDP use different public names, specify the UDP address:

dosh-client --dosh-host public.example.com --dosh-port 50000 user@host

Dosh already lets OpenSSH handle SSH aliases, users, keys, ports, known-hosts, ProxyJump, and other SSH config during bootstrap. It also runs ssh -G to infer the UDP host from an SSH alias when dosh_host is not configured. To make that explicit in Dosh's host config:

dosh import-ssh palav homelab

Develop

Build:

cargo build

Attach locally, using local bootstrap instead of SSH:

target/debug/dosh-client --local-auth --no-cache local

Benchmark local attach:

target/debug/dosh-bench --local-auth --server local --iterations 5

Benchmark a remote host over SSH bootstrap:

target/release/dosh-bench --server user@host --ssh-port 22 --iterations 3

Benchmark the ControlMaster-backed SSH bootstrap path:

target/release/dosh-bench --server user@host --controlmaster --iterations 3

Run the Docker OpenSSH benchmark gate used by CI. It checks both cold SSH bootstrap and ControlMaster-backed SSH bootstrap against a containerized sshd plus resident dosh-server:

make bench-docker-ssh

Run the same Docker comparison with Mosh installed in the benchmark container:

make bench-docker-mosh

That prints ssh_true_ms, dosh_attach_ms, and mosh_start_true_ms under the same container, key, DNS, and network path. It also prints dosh_cached_attach_ms, which is the real Dosh fast path after the first SSH-authenticated bootstrap has issued an attach ticket. See docs/PUBLIC_READINESS.md before using the numbers publicly; Dosh's current strongest claim is fast attach/reconnect, not full Mosh feature parity yet.

The CI workflow includes an optional remote benchmark job. It runs when DOSH_BENCH_HOST, DOSH_BENCH_USER, and DOSH_BENCH_SSH_KEY repository secrets are configured.

Install release binaries and the user systemd service:

make install

Performance Rules

The stack is performance-driven, not fixed by taste. Rust is the default because the likely bottlenecks are network RTT, SSH startup/auth, PTY/shell creation, packet size, and terminal rendering. Change language or runtime only if measurements show they are the bottleneck.

Hot-path rules:

  • Custom UDP protocol with AEAD for v0; no QUIC handshake on attach.
  • Fixed binary packet headers for terminal traffic; no JSON on the protocol path.
  • Preallocated buffers; avoid per-packet heap churn.
  • Single-thread event loop is preferred for the hot path.
  • No PTY allocation, shell spawn, shell rc files, or MOTD on attach to an existing session.
  • Initial snapshot should be sent in the first UDP reply when it fits under the packet budget.

Goals

  • Connection speed as specified above.
  • UDP roaming and reconnect.
  • Encrypted terminal data.
  • Reuse SSH pubkeys for first trust establishment.
  • Named persistent sessions.
  • Multiple clients attached to one session.
  • Optional view-only clients.
  • Single server port, not one port per session.
  • Static server and client binaries where practical.

Non-Goals

  • Replacing SSH as the first public-key trust mechanism.
  • Multi-user access control.
  • Windows support in v0.
  • Full mosh compatibility.
  • Perfect predictive local echo in the first MVP.

Status

Rust implementation is present in this repository. It contains dosh-server, dosh-client, dosh-auth, dosh-bench, shared auth/crypto/protocol modules, a resident PTY server, encrypted UDP bootstrap attach, UDP resume, sealed UDP attach tickets, client ACKs, server retransmit bookkeeping, sliding replay protection, server-side vt100 screen snapshots/diffs, a hardened user systemd unit, an install script, Docker SSH benchmark gates, CI, and protocol/integration tests.

Native v1

Beyond the SSH-bootstrap core, native v1 (docs/NATIVE_V1_SPEC.md) is substantially implemented and aims to replace the day-to-day ssh host workflow on Dosh-installed servers:

  • Native UDP auth with X25519 key exchange, transcript-bound Ed25519 user auth via ssh-agent or an encrypted OpenSSH key, ChaCha20-Poly1305 transport, and authorized_keys policy enforcement (from=, no-port-forwarding, permitopen=; unsupported options fail closed).
  • Dosh host-key trust: pinned known_hosts, dosh trust [--remove|--replace], TOFU only when explicitly enabled, and hard-fail on host-key mismatch.
  • TCP forwarding: local -L, remote -R (loopback bind by default), dynamic SOCKS5 -D, forward-only -N, and background -f, with per-stream flow control so bulk transfers do not lag the terminal.
  • Agent forwarding (opt-in, security-gated): -A / forward_agent exports your local SSH_AUTH_SOCK to a per-session proxy socket on the server (private dir, mode 0600) and tunnels each connection back to your agent over a Dosh stream. Only active on explicit client opt-in and server allow_agent_forwarding = true; it applies to freshly spawned shells (not an already-running attached/prewarmed session, the same constraint ssh/mosh have).
  • Diagnostics: dosh doctor host for config/auth/UDP/forwarding-policy checks.

Native auth is opt-in alongside SSH fallback (auth_preference = "native,ssh"): the native authenticated path is tried first and falls back to SSH bootstrap explicitly when native auth is disabled, unavailable, or rejected. It never silently degrades to an unauthenticated mode.

Native v1 is not yet fully verified. Per-IP token-bucket rate limiting, protocol VERSION negotiation hardening, fuzzing in CI, ECDSA/RSA user keys, and an external security review are still open. See docs/THREAT_MODEL.md for the published threat model and accepted residual risks, and the "Native v1 verification checklist status" table in docs/PUBLIC_READINESS.md for the item-by-item state. Dosh does not yet claim a fully verified SSH replacement; its defensible claim remains fast encrypted native attach/reconnect with SSH-equivalent transport security and SSH fallback.

S
Description
Dosh dormant shell: SSH-speed Rust remote terminal attach over encrypted UDP
Readme 13 MiB
v0.1.17 Latest
2026-07-03 20:35:22 +00:00
Languages
Rust 94.5%
Shell 3.7%
PowerShell 1.1%
JavaScript 0.4%
Makefile 0.2%