Files
alkssh/docs/architecture/client.md
T
glm-5.3-flash 263f91fdb7 docs(architecture): review-pass cleanup — inconsistencies, stale refs, scaffolding refresh
- OQ-02: rewritten as a clean resolved statement; contradictory
  deferred-table row removed
- OQ-05: 'old-code anti-pattern' framing corrected — ephemeral-fleet
  accept-and-continue is a legitimate case-2 HostKeyPolicy; the crate
  rule is 'no hard-coded trust model', not 'accept-all forbidden';
  dangling 'expected ADR-007' pointer replaced with the real contract
  locations (auth.md, client.md)
- README: OQ counts corrected (6 resolved / 3 open / 4 deferred);
  sibling references normalized to real ADR IDs (alkcall 010 is alkhttp
  014's analog; 'ADR-016-analog' was a wrong number)
- client/auth: OQ-07 status corrected to resolved-at-adapter-level;
  TOFO typo; TOFU framing
- ADR-004: dangling 'ADR-008' -> serve-config defaults
- ADR-006: SOCKS5 typo
- mapping: alktty typo; '(OQ-… note below)' placeholder -> the real
  POC-3 note reference
- research: cross-repo numbers qualified (alktty OQ-44, alkvault
  ADR-021); reverse-runner lesson 5 reframed per correction
- scaffolding: Cargo.toml description/comments and src/lib.rs doc
  comment updated against ADR-002/003/006 (dynamic-forward retired,
  git exec-only, russh pin decided); CHANGELOG logs POC completion and
  ADRs 001-006

Verification: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check, cargo check — all pass
2026-10-27 10:00:11 +00:00

7.5 KiB

status, last_updated
status last_updated
draft 2026-10-27

Client — client direction: driving vanilla SSH servers as family ops

The door framing

The client direction is a door to vanilla servers: family in, vanilla out. The motivating consumer is the reverse-runner shape (docs/research/reverse-runner.md, alkminer extraction): a family hub dials vanilla sshd on rented/near-ephemeral hosts — remote boxes expose plain sshd and hold nothing of ours; every op the hub drives maps family-side onto the same protocols the server direction serves. The asymmetry is honest (vision principle 2): the server direction serves family seam entries; the client direction consumes family types. Same features gate both (ADR-003 symmetry rule).

Type symmetry at the seam is the client contract: a family app drives alkssh in family-protocol types; alkssh translates to vanilla wire ops (russh client surface, ADR-002). No vanilla-concept leaks up into the family surface.

The dial surface

Every dial needs: address, key source (ADR-005 — vault-backed blob, decrypt-at-dial, fail-closed on locked/absent — never env vars, never plaintext at rest), host-key policy (OQ-05 resolution — assembly-provided; crate default fail-closed reject-unknown, hard decline on mismatch; the OpenSSH "add to known_hosts?" moment is an assembly policy, e.g. learn-and-record / prompt / provider-metadata match — the crate is policy-blind), and the bounded-resource config (keepalive_interval/keepalive_max — 15 s / 3 default posture per the reverse-runner evidence; inactivity_timeout; window/packet knobs per overview.md).

The identity the dial authenticates as is a registered fingerprint at the remote end — for family-run peers (alkforge runner) that's the alkcall IdentityProvider; for vanilla fleets it's whatever the vanilla server's authorized_keys holds — either way the key is ours from the vault and the host's key is checked by the policy.

Op rows (family op → vanilla translation)

Family-facing op Family protocol/types Vanilla-wire translation Feature
run a command / own a shell on the runner alktty session types (pipe vs PTY; exit semantics) channel_open_session + exec/pty-req/shell-req; data pump; drain wait() per the POC-3 §D.3 mechanics (Channel::exec() — no request_exec; Success/Failure arrive on the channel queue, not as Err from the request call) session
open a tunnel target alktunnels TunnelSession/target types channel_open_direct_tcpip per connection — the typed open resolves to Err(ChannelOpenFailure) on server reject (POC-1 §6); no wait() before into_stream tunnel
the -D behavior (local SOCKS door) alksocks' local door composed at the assembly, fed a DialFn backed by row 2's open the wire sees only direct-tcpip; alkssh does not depend on alksocks on this path (ADR-003 §1a) tunnel
listen + ingest (-R leg) alktunnels consumer/ForwardedHandler shape tcpip_forward(address, port) -> bound global request (no mpsc sink param) + accept the server-initiated forwarded-tcpip opens via server_channel_open_forwarded_tcpip, routing each to the family handler tunnel
upstream git consumption alkgit client/service types the same exec grammar the server side parses (git-(upload,receive)-pack '/repo' exec), pointed at an upstream sshd git
artifact/deploy transfer alkfs client half (future) subsystem "sftp" + the alkfs protocol sftp (OQ-13)

The acceptor: fail-closed client-side

russh's client handler receives server-initiated channel opens; its default posture for a forwarded open is accept-and-drop — a half-capability, which the family forbids (ADR-003 symmetry + reverse-runner.md lesson 1). The client contract:

  • Server-initiated opens are served only when the assembly registered an acceptor for that type and under its feature (forwarded-tcpip under tunnel is the real case — the -R data path).
  • Unregistered/unfeatured accepted opens are declined cleanly, never silently dropped after accept.
  • Acceptor handlers may be installed across a live connection's lifetime (the alkminer install_forward_sink late-install dynamic — consumers turn capabilities on/off per connection; the shape is an assembly-mutable acceptor registry).

Lifecycle primitives (OQ-12 resolution)

  • Forward listeners return the actually-bound port (russh tcpip_forward -> u32); every listener/handle is explicitly cancellable; disconnects are explicit (Disconnect::ByApplication).
  • No reconnect or restore policy is owned by the crate. Reconnect-and-restore (rebinding persisted tunnel ports — the alkminer flow) is assembly policy; the crate provides the primitives (bound ports, handles, cancellation, clean close) to make that possible.

What the crate does not include (client direction)

  • Job supervision — background-job lifecycle (the alkminer start/stop/logs job model, pid files, host-side ledgers) is assembly work composed over the exec rows; the crate gives the channels and the exit semantics (including the pipe-mode pid-only signal caveat — mapping.md / OQ-10).
  • Credential storage policy — assembly (the vault handle is injected; ADR-005 / OQ-06).
  • Trust decisions — the host-key policy is assembly-provided (OQ-05; the crate default rejects; a permissive case-2 policy for ephemeral fleets is the assembly's to choose — the crate carries no trust model of its own).
  • sftp — until alkfs (OQ-13); the sftp feature exists as the tested decline on both directions.

Correctness facts to carry (POC-recorded)

  • Channel::exec() (no request_exec at this pin); pty/subsystem request failures surface as ChannelMsg::Failure via wait(), not Err from the call — the caller must drain until the expected message, continue past the rest.
  • window_change/signal are fire-and-forget and must be sent before into_stream() (the Channel is consumed by conversion); server-side they carry no SSH reply.
  • Do not consume the channel queue before into_stream() — a wait() on a fresh channel breaks it (POC-1 §6 caveat), both sides.
  • Keepalives on cloud fleets: 15 s / 3 (OpenSSH ServerAliveInterval=15/CountMax=3 parity) — with russh's default (none), dead NAT'd connections sit unnoticed until an exec health check fails (reverse-runner.md lesson 3).
  • A rejected channel open is an Err from the typed open call (not a wait()-match); the connection stays usable after declines.

Open Questions

  • OQ-05 (resolved): the host-key policy seam — the contract above; pin storage is assembly detail.
  • OQ-12 (resolved): lifecycle primitives only — recorded above.
  • OQ-13 (deferred, alkfs): the sftp client half.
  • OQ-07 (resolved at the adapter level): the vault decrypt plain-String gap — the adapter-side Zeroizing wrap is the contract (ADR-005 decision 3); the vault-side improvement is an upstream alkvault ask, not alkssh scope.

References

  • ADR-002 (the client russh surface + config knobs), ADR-003 (§1a the alksocks composition; the symmetry rule), ADR-004 (fingerprint identity), ADR-005 (key sources), ADR-006 (route-1 bridge, both legs)
  • docs/research/reverse-runner.md (the consumer pattern + lessons), poc-3-mapping-bakeoff.md §D.3 (client mechanics), §C.4 (the fabric recipe), poc-1-auth-bridge.md §6/§7 (open-call Err semantics, plumbing), poc-2-vault-keys.md (dial lifecycle)
  • mapping.md (the server-direction mirror rows)