The native-v1 work changed the UDP wire format (HEADER_LEN 42->58) without a
version bump, so a current client silently timed out against the older
still-deployed server. The server just needed redeploying; alongside that,
fix the robustness bugs surfaced while restoring the connection:
- Server: clamp terminal size to >=1 at every PTY/parser resize site. A client
reporting a 0x0 terminal made vt100::Parser::set_size panic ("attempt to
subtract with overflow") and took down the whole single-threaded daemon.
- Client: treat size() == Ok((0,0)) like a missing size and fall back to 80x24
instead of sending 0x0 to the server.
- Reap abandoned sessions and their shells. PtyHandle now retains the child and
kills it on drop, and the cleanup task removes clientless, non-prewarmed
sessions after the grace period. Previously the shell leaked forever (one
zsh per abandoned session); prewarmed sessions still stay hot.
- Evict half-finished native handshakes (pending_native) after 30s so an
unauthenticated ClientHello flood can't grow the map without bound.
- Drop the per-client, per-packet vt100 Screen clone on the output hot path and
remove the write-only last_screen field.
- Guard load_or_create_server_secret against a <32-byte secret file (was an
index-out-of-range panic at startup).
- Reconcile stream flow-control credit (client and server) when StreamData hits
a missing writer, so a closed/unknown stream can't wedge the peer's window.
Adds a unit test for session reaping. cargo fmt + cargo test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
sshby 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:
- SSH connects to the host.
- SSH starts
mosh-server. - The client receives connection material over SSH.
- 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:
- UDP resume: existing
ClientIdand session key. No SSH. One encrypted UDP request, one encrypted UDP reply. - 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.
- SSH bootstrap:
ssh -T user@host ~/.local/bin/dosh-auth ..., then one encrypted UDP attach. - 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 trueover the existing master + one measured UDP RTT. - Cold attach without ControlMaster: <= cold
ssh hostterminal-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
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. The old in-band status overlay was removed
because writing directly over the terminal could corrupt TUIs; a non-destructive
status surface is tracked as a public-readiness item.
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.