--- id: w5-e2e-client name: "Wave 5: real-tool e2e, client direction (the door against a real sshd)" status: completed depends_on: [review-wave-4] scope: moderate risk: medium impact: phase level: implementation --- ## Description The client-direction real-tool e2e, which takes the reverse-runner shape re-landed against a **real `sshd`** (`docs/plans/implementation.md` §Wave 5, the real-tool e2e bullet's client half; the wave-4 client rows ran against the in-process fake `sshd` double, and the wave-4 gate named real-`sshd` e2e wave-5 territory explicitly). A dedicated new binary (decide + record the placement, for which `tests/e2e_client.rs` reusing `tests/common` is the recommended posture, disjoint from the matrix task's binaries), under the check-gated **skipped-with-reason** posture: - **The sshd fixture**: a spawned real OpenSSH server the dial targets: host key generated (ed25519), a listen port picked, the `HostKeyPolicy` accepting the fixture's known fingerprint (the case-1 pin posture, with the policy seam consumed against a real presented key), a generated ed25519 user key authorized in the fixture's `authorized_keys` (the decrypt-at-dial path over the in-memory vault double). The fixture's exact shape (spawned per-test vs a shared per-binary instance; the sshd_config knobs, publickey-only, no password) decided + recorded. - **The op rows against real sshd**: exec (exit-status parity through a real server's reaping), the direct-tcpip dial (a real TCP connection through the server's forwarding, `-L`-shaped), **the `-R` leg**: `tcpip_forward` with the port-0 mint against a real server's actually-bound reply, the server-initiated `forwarded-tcpip` open served through the acceptor registry (the reverse-runner consumer pattern end to end), the cancel form tearing the real listen down. - **The decline rows against a real server**, the sftp client-side placeholder decline (`subsystem` sftp → the typed decline), the unfeatured direct-tcpip decline (the connection stays usable, which is the typed-open decline mirror's real-server row), the host-key policy mismatch hard-decline (a wrong-fingerprint fixture → no dial, the case-1 posture against a real presented key). - **Every skip reasoned** (no `sshd` in the environment / fixture constraints), every finding fixed in-task or booked for the gate, never absorbed silently. ## Acceptance Criteria - [x] The sshd fixture lands (spawned real server, ed25519 host + user keys, the policy pin posture); its shape decided + recorded - [x] Exec, direct-tcpip, and the `-R` leg (port-0 mint + accepted arrival + cancel) green against real sshd (or reasoned-skipped) - [x] The decline rows green: sftp placeholder, unfeatured direct-tcpip (connection stays usable), host-key mismatch hard-decline - [x] Every finding fixed in-task or booked for the gate (recorded) - [x] `cargo test` (+ the builds the new binary rides), clippy ×2 (`-D warnings`), fmt, `cargo doc --no-deps` green ## References - docs/plans/implementation.md §Wave 5 (the real-tool e2e bullet), §Wave 4 (the fake-`sshd` posture this task's real server replaces for e2e) - docs/architecture/client.md (the dial surface, op rows, the acceptor), docs/research/reverse-runner.md (the consumer pattern the client direction serves) - docs/research/poc-2*.md / the reverse-runner extraction (the real-`sshd` client-direction evidence this re-lands against the shipped crate) - tests/client_dial.rs, tests/client_ops.rs, tests/client_reverse_listen.rs (the double-driven rows this task mirrors against the real server), tests/common/sshd_double.rs (the double's semantics, which the real fixture must match) ## Notes **The sshd fixture decided**: `tests/e2e_client.rs` (the new binary), reusing `tests/common`, and the wave-4 client binaries stay single-writer per the decomposition's file map; this task edited only its own binary (`tests/e2e_client.rs`, an in-file `support` module). **Fixture shape: per-row spawned real `sshd`**, since each row's config knobs differ (`AllowTcpForwarding` yes/no) and each row dials its own server, so a shared per-binary instance buys nothing; per-row isolation (own workdir, own minted ed25519 host key via `ssh-keygen`, own probe-verified port from the e2e-server's monotonic allocator posture) beats the spawn cost. Knob set: publickey-only, `StrictModes no` (the throwaway workdir carries none of the real home-dir hygiene), `UsePAM no`, `UseDNS no`, `PidFile none`, `PermitRootLogin prohibit-password`, host key + authorized_keys (fixture `alpha`'s public blob) at absolute paths, per-row `AllowTcpForwarding`. Readiness is a bounded TCP probe; a server that never comes up is the row's **reasoned skip** with the server's own `-e` log tail as the reason (worked as non-root in this environment, verified against OpenSSH 9.6p1). The dial rides the decrypt-at-dial path over the in-memory vault double (`VaultKeySource` over `TestVault`, `alpha` sealed) and the case-1 pin posture (`MemHostKeyPolicy::pin`) accepts only the minted host fingerprint. **Row inventory** (all check-gated, `#[cfg(unix)]` + feature-gated per row group; deadline-bounded end to end (the POC-1 §5 posture); `Contract stamp:` markers cite `client.md` / `reverse-runner.md`): | Row | Build(s) | What it pins | |---|---|---| | `exec_row::real_sshd_exec_pumps_streams_and_reports_the_exit_status` | session (+all) | the wave-4 double-driven exec row re-landed: stdout/stderr separated through a real server, the **nonzero** exit (3) reported in-band last, the real server's reaping, parity not a canned 0; the case-1 pin accept decision on record against the really-presented key | | `direct_tcpip_row::real_sshd_direct_tcpip_round_trips_a_live_tcp_connection` | tunnel (+all) | the `-L`-shaped dial through a real server's forwarding into a live loopback echo server, bytes end to end | | `reverse_rows::real_sshd_port_zero_mint_serves_the_accepted_arrival_and_the_data_leg` | tunnel (+all) | the port-0 mint surfaces the actually-bound port (verified by dialing it), the server-initiated `forwarded-tcpip` arrives into the installed acceptor, the external dialer's bytes round trip, giving the reverse-runner consumer pattern end to end | | `reverse_rows::real_sshd_cancel_tears_the_listen_down_and_unknown_cancels_refuse` | tunnel (+all) | explicit-port listen actually bound; the listener handle's `cancel()` frees the **real** bound port (later dials refused); a stateless cancel of the unknown listener is `ListenRefused`; connection stays usable | | `declines::real_connection_sftp_placeholder_declines_with_the_connection_intact` | **every build** | the OQ-13 placeholder against a real authenticated connection: typed decline before any wire op, connection intact | | `declines::real_presented_key_mismatch_hard_declines_and_the_right_pin_dials` | **every build** | the wrong-pinned policy hard-declines the dial (`DialError::HostKeyRejected`), the policy log records the **real** fingerprint as rejected, and the correctly-pinned dial to the same server succeeds, and the decline is the policy's decision, not the server's | | `unfeatured_direct_decline::real_server_refused_direct_tcpip_declines_and_the_connection_stays_usable` | tunnel+session (all-features) | a real server with `AllowTcpForwarding no` refuses the typed open → `OpenDeclined(Prohibited)`, and a following exec is served, the connection-stays-usable fact end to end | **Decisions recorded** (for the wave-5 gate): - The "connection stays usable" decline row is gated `tunnel` **and** `session` (rides `--all-features` only): the post-decline exec is the stays-usable proof's vehicle, and exec is a `session`-gated op. Under tunnel-only builds the row is absent; the fake-double mirror (`tests/client_ops.rs`) carries the tunnel-only stays-usable rows. - The sftp and host-key-mismatch rows run in **every** build (feature-free ops on `SshConnection`), so even the no-feature matrix build exercises two real-server rows. - The fixture skips are honest: no `sshd` in the environment / no `ssh-keygen` / spawn refusal / never-listening (the server's `-e` log tail carried in the reason), never silently green. **Findings**: no src/ changes needed, as every observed issue was e2e-graph local (compile/fmt-level only). No bookings for the gate beyond the decisions above. ## Summary Completed 2026-10-30. The client-direction real-tool e2e landed as `tests/e2e_client.rs` (7 rows, fixture shape above): exec (exit-status parity, nonzero through real reaping), the direct-tcpip dial, the `-R` leg (port-0 mint + accepted arrival + data leg + cancel tearing the real listen down + unknown-cancel `ListenRefused`), and the three decline rows (sftp placeholder, host-key mismatch hard-decline with the real presented fingerprint on record, server-refused direct-tcpip with the connection staying usable). Verification: full `scripts/matrix.sh` run green: `cargo test` per build (none / git / session / tunnel / sftp / --all-features; the new binary compiles in every build and runs only its rows' builds: 2 / 2 / 3 / 5 / 2 / 7 rows), both clippy runs `-D warnings` clean, fmt clean, `cargo doc --no-deps` clean; plus 10 consecutive all-features e2e_client runs green (no flakes; per-row workdirs + the e2e-server's port allocator carried over). Skips are reasoned prints only; on this host sshd 9.6p1 is present, so every row ran.