Mosh-like per-session model, PQC on by default, real README - #1
Merged
Conversation
Replace the TCP/bincode stack with a UDP-based protocol: - 26-byte unencrypted PacketHeader (version, flags, session_id, packet_seq) for server-side routing before decryption - Protobuf envelope (prost derive macros, no protoc required) with stable field tags for forward/backward compatibility - Stream multiplexing: stream 0 = terminal PTY, streams 1+ = port forwards, each with independent sequence/replay state - 1-RTT handshake: ClientHello (plaintext) → ServerHello (hello-key encrypted); both sides derive the session key in one round trip - Cipher suite negotiation with preference list; suites in priority order: ML-KEM-1024+AES-GCM+SHA3, ML-KEM-768+AES-GCM+SHA256, X25519+AES-GCM+SHA256, X25519+ChaCha20-Poly1305+SHA256 - PQC suites (ML-KEM) gated behind the `pqc` cargo feature - Ephemeral KEM adds perfect forward secrecy on top of the SSH- bootstrapped passkey; passkey also encrypts ServerHello for implicit server authentication - SessionState now owns per-stream StreamState ring buffers; reconnect replay is per-stream Assisted-By: Claude Sonnet 4.6
The previous design had handle_client_hello spawning its own recv_packet loop, racing with the main loop on the same socket. Data packets received by the main loop were silently dropped. Replace with a proper demux architecture: - run_daemon owns the socket exclusively as the sole reader - Handshake packets spawn a handle_client_hello task as before - Data packets are routed by session_id to a per-session mpsc::Sender<ReceivedPacket> (inbound_tx), replacing the competing recv_packet call in handle_client_hello - handle_client_hello creates the (inbound_tx, inbound_rx) pair, registers inbound_tx in ActiveSession, and reads from inbound_rx - Replacing inbound_tx on reconnect closes the old Receiver, cleanly terminating the prior connection's reader task - outbound_tx replaces udp_tx: PTY reader and disconnect signals write Envelopes here; the writer task encrypts and sends them - server_last_received is now populated from actual session state rather than an empty HashMap Assisted-By: Claude Sonnet 4.6
stderr is unbuffered, so operational log messages (startup, session registration, PTY exit, timeouts) appear immediately when the daemon's stdout is redirected to a file. Error messages were already using eprintln!; this makes all messages consistent. Assisted-By: Claude Sonnet 4.6
Recipes: check-tools, build, install, test-local, log, clean. test-local covers happy path and reconnect via SIGSTOP/SIGCONT without requiring sudo or iptables. Assisted-By: Claude Sonnet 4.6
Add fmt, fmt-check, clippy, check, test, audit, build-release, and install-release recipes. Decouple build from check-tools; scope check-tools to test-local prerequisites only. Assisted-By: Claude Sonnet 4.6
Assisted-By: Claude Sonnet 4.6
Socket: $XDG_RUNTIME_DIR/etr/etrs.sock Log: $XDG_STATE_HOME/etr/etrs.log Use dirs crate for XDG lookup in etrs; create parent directory before binding the socket. Justfile derives both paths via shell expansion with XDG fallbacks. Assisted-By: Claude Sonnet 4.6
-v connection lifecycle (connect, reconnect, timeout) -vv cipher suite negotiation, session IDs, peer address -vvv per-packet trace (type, seq, size, direction) CipherSuiteId gains Display and a name() method. process_server_hello now returns the negotiated suite. etrs uses a OnceLock<u8> global so spawned tasks log without extra parameter threading. Assisted-By: Claude Sonnet 4.6
Docs: struct/method docs for Aes256GcmCipher, ChaCha20Cipher, AeadCipher, X25519KeyPair, MlKem768/1024KeyPair, StreamLifecycle variants, SessionState methods, HandshakeError variants. Tests added (+50 new tests): - crypto/aead: AEAD auth-tag verification, wrong-key, mutated ciphertext, wrong-seq, empty plaintext, seq_to_nonce uniqueness - crypto/kdf: determinism, salt/ikm/info binding, output length, SHA-256 vs SHA3-256 divergence - crypto/x25519: invalid-length error paths, key uniqueness - crypto/kyber: ML-KEM-768/1024 round-trips, wrong-ciphertext implicit rejection - transport: decode_data_packet wrong key/mutation/seq errors, decode_plaintext_packet invalid protobuf, send/recv loopback (plaintext and encrypted), truncated-header returns None - session/stream: acknowledge_up_to edge cases (empty, past end, drain all), replay_from on empty history, initial seq values - session/mod: close/apply_acks on unknown stream, last_received semantics (nothing received → 0), collect_replays with empty peer map, open_stream idempotence - handshake: MalformedPacket, UnexpectedPacket, UnsupportedSuite, last_received_seq roundtrip in both directions Assisted-By: Claude Sonnet 4.6
- Box AeadCipher::Aes256Gcm to balance variant sizes - Suppress vec_init_then_push: cfg-gated pushes can't use vec![] - Fix doc_overindented_list_items in lib.rs and protocol/mod.rs - Collapse nested if-let chains in parse_target (etr.rs) - Use is_multiple_of(2) in hex_decode (etrs.rs) - assert! instead of assert_eq!(x, true) in session tests Assisted-By: Claude Sonnet 4.6
- Replace Unix domain socket with TCP loopback (127.0.0.1:udp+1) for session registration; eliminates XDG_RUNTIME_DIR path mismatch between interactive daemon and non-interactive SSH sessions - Pass reg_port through stdin bootstrap channel instead of CLI flags, avoiding clap global-arg-after-subcommand parsing issues - Resolve hostnames via tokio::net::lookup_host instead of SocketAddr::parse to support non-IP targets like 'localhost' - Bind client UDP socket to [::]:0 for IPv6 targets, 0.0.0.0:0 for IPv4 - Default daemon bind address to [::] for dual-stack IPv4+IPv6 support - Route etr verbose logs to ~/.local/state/etr/etr.log in interactive mode to avoid corrupting raw-mode terminal display - Fix stale test asserting removed --socket CLI field Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix vlog: show on stderr before raw mode, log file only during session - Set IN_RAW_MODE flag around enable/disable_raw_mode calls - AGENTS.md: require NOTES.md read on startup, update on commit/push - NOTES.md: add product vision (Mode 1 mosh-like, Mode 2 port forwarding), update known gaps, fix verbose logging description Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
etr now SSHes to the target and starts etrs on the fly — no pre-running daemon required. etrs binds a random UDP port, prints it to stdout, forks, and the child runs the session. On clean disconnect the child exits immediately. - etrs: complete rewrite as per-session binary (fork + detach) - etr: bootstrap_ssh reads PORT from SSH stdout; no reg port - ml-kem: update kyber.rs for ml-kem 0.3.2 API - handshake: generate keypair for most-preferred suite (fixes PQC) - PQC on by default; opt out with --no-default-features - justfile: test-local updated for new architecture - README: replace placeholder with real documentation - NOTES.md: reflect new architecture throughout Assisted-By: Claude Sonnet 4.6
ci.yml: trigger on feature/**, fix/**, chore/** push branches release.yml: Linux-only build (macOS/Windows untested), fix completions flag (--completions SHELL, not subcommand), simplify to single job matrix Assisted-By: Claude Sonnet 4.6
l1a
added a commit
that referenced
this pull request
Jul 21, 2026
* Fix Windows input path and terminal restore Two independent native-Windows parity fixes in the etr client. 1. Special characters no longer "eaten" (issue #54): the stdin reader used std::io::stdin().read(), which on Windows goes through Rust std's ReadConsoleW shim (UTF-16->UTF-8 + line cooking). Even in raw mode it batches input and drops non-UTF-8 bytes, which made special characters vanish (zellij keybindings needing ^g) and caused the first-line-not- echoed bug. It now reads the console input handle directly with ReadFile (read_stdin); with ENABLE_VIRTUAL_TERMINAL_INPUT on this returns the same unbatched, per-keystroke VT byte stream a Unix terminal emits. enable_vt_console also sets the console input codepage to UTF-8 (65001), saved/restored on exit, so typed multi-byte input reaches the remote as UTF-8. Adds windows-sys feature Win32_Storage_FileSystem for ReadFile. 2. Local terminal restored on exit: a remote full-screen app leaves the local terminal in alternate-screen/mouse/paste/hidden-cursor modes that disable_raw_mode does not undo, so after a hard drop or ~. the mouse wheel spewed escapes and the terminal was unusable. restore_terminal() now emits VT resets on every final-exit path: a cursor-safe part (TERM_RESET_MODES) on every exit and a screen-restoring part (TERM_RESET_SCREEN, which homes the cursor) only on unclean exits. Avoids a full RIS so scrollback is kept. Version 0.6.4 -> 0.6.5. Test count 110 -> 112 (reset-sequence regressions). Assisted-By: Claude Opus 4.8 * docs: record live Windows->WSL verification of v0.6.5 Verified end-to-end against a real Unix etrs (WSL Fedora 44): remote prompt renders, PTY command round-trips, and the client emits the cursor-safe terminal-restore sequence on clean exit (fix #2 confirmed in the live byte stream). The console input-VT path (fix #1) needs interactive keystrokes and is flagged for manual confirmation. Also notes an adjacent pre-existing gap: redirected stdin ends a remote-command session on EOF before output arrives. Assisted-By: Claude Opus 4.8 * docs: verify v0.6.5 fixes via synthesized console keystrokes Drove the live etr client with real console key events (WriteConsoleInputW) against a WSL Fedora 44 etrs. Confirms fix #1 end-to-end: Ctrl+G->0x07, arrow keys, rapid bursts and Unicode all survive the raw+VT-input ReadFile path un-eaten, and injected keystrokes reach the remote per-keystroke (remote zsh-syntax-highlighting recolours char-by-char). Confirms fix #2: cursor-safe terminal-restore sequence emitted on clean exit. Updates NOTES accordingly. Assisted-By: Claude Opus 4.8 * docs: add stdin-EOF remote-command gap to Known gaps Promote the redirected-stdin truncation note from the v0.6.5 verification footnote into the Known gaps / next steps list so it is discoverable as tracked open work, with a sketch of the ssh-parity fix (half-close stdin, keep draining PTY output). Assisted-By: Claude Opus 4.8 * docs: note just recipes fail on native Windows shells `just install` (and other bash-shebang recipes) fail from PowerShell/nushell with "could not find cygpath": just tries to translate the shebang interpreter path via cygpath, absent without Git Bash on PATH. Recorded in Known gaps with the cargo-install workaround and a sketch of a cross-shell fix. Assisted-By: Claude Opus 4.8 * Fix Windows first-line echo (#54) via reader gate The single stdin reader thread is spawned before the QUIC connect, but raw + VT-input mode is only enabled after connect. On Windows a ReadFile issued while the console is still in cooked/line mode stays line-buffered for that whole read, so the first line was held client-side until Enter ("no echo until first Enter"). The v0.6.5 ReadFile change did not fix this — it is a timing problem, not a read-mechanism one. Gate the Windows reader on a one-shot signal fired right after the first enable_raw_mode + enable_vt_console, so its first read happens in raw + VT mode and is per-keystroke. Unix is unaffected (ungated, never had the bug). Verified with an A/B console-keystroke harness that snapshots the client's stdout before Enter: pre-fix the typed first line is absent (line-buffered); with the gate it appears, echoed back per-keystroke. NOTES corrected (the earlier claim that ReadFile alone fixed #54 was wrong). Assisted-By: Claude Opus 4.8 * Fix local shell Enter broken after etr exits (Windows) enable_vt_console sets ENABLE_VIRTUAL_TERMINAL_INPUT, but crossterm's disable_raw_mode only ORs the line/echo/processed-input bits back — it never clears the VT-input flag. So after etr exited, the console was left with VT-input enabled and the local shell echoed typed characters but would not accept Enter (the VT-translated Enter wasn't seen as line submission). Capture the console's exact original input/output modes + input codepage once (capture_console_originals, before raw mode is first enabled) and restore them verbatim on every exit path (restore_console_state), which clears the leftover VT-input flag. Verified with a harness: input mode restored byte-identical (0x01f7 -> 0x01f7), VT_INPUT not left set. Pre-existing since v0.6.4; no-op on Unix. NOTES also records a related server-side gap (clean shell `exit` sometimes reconnects instead of quitting because the Disconnect races the connection close). Assisted-By: Claude Opus 4.8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
etrnow SSHes to the target and startsetrson the fly — no pre-running daemon required.etrsbinds a random UDP port, prints it to stdout, forks into the background, and exits on clean disconnect. Mirrors the mosh model.--no-default-features.etrwritesSESSION_ID_HEX/PASSKEY/TERMtoetrsstdin via SSH;etrsresponds withPORT <n>on stdout, then forks. No TCP registration port, no daemon.kyber.rsfor the new API (DecapsulationKey<P>,EncapsulationKey<P>,Kemtrait,as_slice()instead ofAsRef).feature/**,fix/**,chore/**branches.--completionsflag syntax.Test plan
just check— fmt + clippy cleancargo test— 94 tests passetr localhostconnects and shows a shell (requires passwordless SSH to localhost)🤖 Generated with Claude Code