-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
etr uses SSH only for the initial bootstrap — to start the server and exchange credentials securely. All subsequent terminal I/O flows over a persistent QUIC connection that survives network interruptions.
1. etr SSHes to the remote host and starts etrs
2. etrs generates an ephemeral self-signed TLS certificate,
binds a random QUIC port, prints "PORT <n> CERT <fingerprint>",
and forks into the background
3. etr reads the port and certificate fingerprint from SSH stdout; SSH closes
4. etr opens a QUIC connection to the server, pinning the received certificate
5. etr sends SessionOpen (session ID + passkey) on the control stream;
etrs responds with SessionAccept
6. Terminal I/O flows on the PTY stream; port-forwards on additional streams
7. On clean disconnect, etrs exits immediately
QUIC runs over UDP, so like raw UDP it isn't tied to a specific network path — etr can reconnect from a new IP or port without the OS discarding connection state. Unlike raw UDP, QUIC provides:
- TLS 1.3 built in — all data is encrypted with no custom crypto needed
- Reliable ordered delivery per stream — no dropped or reordered bytes reach the application
- Multiplexed streams — PTY and each port-forward run independently; a stalled forward can't block the terminal
- Congestion control — built in via QUIC's loss detection
mosh is the inspiration for the bootstrap model. etr differs in a few ways:
- etr uses a proper ordered stream with replay and acknowledgement, not mosh's state-sync model
- etr uses QUIC, which gives reliable delivery, TLS 1.3, and multiplexing for free
- etr supports port forwarding (
-L)
etr runs:
ssh -p <port> <host> etrs
and writes SESSION_ID/PASSKEY/TERM to the SSH process's stdin. etrs generates an ephemeral self-signed TLS certificate, binds a QUIC port, and prints PORT <n> CERT <cert_der_hex> to stdout, then forks:
- Parent process: exits immediately → SSH sees the command return and closes cleanly
-
Child process: calls
setsid(), redirects stdio to/dev/null, writes logs to~/.local/state/etr/etrs.log, and runs the session
etr reads the port and certificate from SSH stdout, then opens a QUIC connection pinning exactly that certificate. No CA or PKI is involved — this is analogous to SSH host-key trust.
The passkey (a random 32-character string generated fresh each session) is exchanged over the SSH-encrypted bootstrap channel. Only a holder of the correct passkey can authenticate a SessionOpen message. The TLS certificate, also transmitted over SSH, prevents man-in-the-middle attacks on the QUIC connection. Together these provide the same security properties as SSH host-key pinning, without requiring the server to be pre-configured.
The client sends a heartbeat every 5 seconds. If 15 seconds pass without any packet:
-
etr's per-connection tasks see QUIC errors and return -
etropens a new QUIC connection to the same server address -
etrsends a newSessionOpenwith the samesession_idandpasskey, plus its currentlast_received_seqwatermarks - The server matches on
session_id+passkey, sendsSessionAcceptwith its own watermarks - Both sides replay any unacknowledged PTY data (seq > peer's watermark)
The server holds session state (shell process, PTY, stream history) for 30 minutes after the last packet. The reconnecting client may come from a different IP or port.
Port-forward streams are not replayed on reconnect — they are re-opened fresh by the client.
Every QUIC stream opened by the client begins with a 1-byte tag:
| Tag | Stream | Content |
|---|---|---|
0x01 |
Control |
SessionOpen / SessionAccept, then Heartbeat / TerminalResize / Disconnect
|
0x02 |
PTY | Sequence-numbered raw chunks (terminal output server→client, stdin client→server) |
0x03 |
Forward |
StreamOpen header then raw bytes (TCP) or UdpDatagram envelopes (UDP) |
See PROTOCOL.md for the full wire format.
Each -L or -R UDP spec uses one shared QUIC stream for all datagrams, with every UdpDatagram envelope carrying the originating peer_addr and peer_port. Multiple concurrent UDP senders on the same forwarded port are fully supported:
-
-L(local → remote): the server-side forwarding function maintains one ephemeral UDP socket per unique source address. Each socket has a distinct OS-assigned port, so replies from the remote target return to the correct socket and are routed back to the right local sender. -
-R(remote → local): the client-side handler applies the same per-sender socket model toward the local target. Reply envelopes carry the original external sender's address so the server routes them to the correct remote client.
Sender sockets are evicted after 30 seconds of inactivity. Suitable for DNS, STUN, and game-protocol use cases where multiple clients share a single forwarded port.
On Linux, etrs registers the session with the system login database via libutempter:
-
On connect: a
USER_PROCESSentry is written to utmp/wtmp, so the session appears inwhoandlastwith the client IP address andvia etr [PID]. -
On clean disconnect (shell exits or client sends
Disconnect): aDEAD_PROCESSentry is written to mark the session as ended. -
On signal (SIGTERM or SIGHUP):
etrswrites theDEAD_PROCESSentry before exiting, sowho/lastentries are cleaned up even if the session is killed rather than ended normally.
On non-Linux platforms (e.g. macOS) the utmp calls are no-ops.
| Process | Log location |
|---|---|
etr (client) |
~/.local/state/etr/etr.log (when running interactively with -v) |
etrs (server) |
~/.local/state/etr/etrs.log (on the remote host) |