docs(comments): sweep em dashes from source comments, test prose, and manifest

Same treatment for the prose the compiler carries: doc and line comments
across src/ and tests/, test assertion and error-message strings (prose
surfaces: the tunnel-map registration/establish errors, the key-source
decode payload, assertion messages), scripts/matrix.sh comment lines,
the fuzz sidecar's comments and README (corpus seeds untouched), and the
Cargo.toml comment headers plus the crates.io description. The AGENTS.md
no-em-dash rule (convention 14) now holds for the whole tracked tree.

Verification: repo-wide git grep is clean (single exempt hit: the
verification command's own search pattern), cargo test (18 suites, zero
failures, default and per-feature builds via scripts/matrix.sh), clippy
--all-targets -D warnings and clippy all-features, fmt --check, doc
--no-deps, and the fuzz corpus gate (fuzz/shared, 11 tests) all green.
This commit is contained in:
glm-5.3-flash committed 2026-11-04 20:28:05 +00:00
1 parent f4b5f896b4
commit 1e583ab55b
55 files changed
+1756 -1751

No files matched your search

+21 -21
View File
@@ -1,6 +1,6 @@
# Single-crate protocol/door crate (the alktty template). No workspace: # Single-crate protocol/door crate (the alktty template). No workspace:
# the composition story is deferred — alkssh's substance is the mappings # the composition story is deferred: alkssh's substance is the mappings
# (family ADR-003). The `fuzz/` sidecar is its own workspace (excluded — # (family ADR-003). The `fuzz/` sidecar is its own workspace (excluded:
# it pins its own nightly toolchain and never rides the root's). # it pins its own nightly toolchain and never rides the root's).
[workspace] [workspace]
members = ["."] members = ["."]
@@ -12,14 +12,14 @@ version = "0.2.0"
edition = "2024" edition = "2024"
rust-version = "1.89" rust-version = "1.89"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels — session/pty, tunnels, git exec — with every capability mapping a fail-closed cargo feature (no features: no capabilities)" description = "SSH adapter for the alk family: russh-wrapped vanilla-ssh termination bridged to/from alkcall channels (session/pty, tunnels, git exec), with every capability mapping a fail-closed cargo feature (no features: no capabilities)"
repository = "https://git.alk.dev/alkdev/alkssh" repository = "https://git.alk.dev/alkdev/alkssh"
keywords = ["ssh", "russh", "protocol", "alkcall", "network"] keywords = ["ssh", "russh", "protocol", "alkcall", "network"]
categories = ["network-programming", "asynchronous"] categories = ["network-programming", "asynchronous"]
# The package policy (the sibling crates' convention): the published crate # The package policy (the sibling crates' convention): the published crate
# ships the architecture docs (the authoritative contract) but not the # ships the architecture docs (the authoritative contract) but not the
# process surface — reviews, research, plans, task files, the working # process surface: reviews, research, plans, task files, the working
# agents' config, AGENTS.md — and not the `fuzz/` sidecar (its own # agents' config, AGENTS.md; and not the `fuzz/` sidecar (its own
# workspace; never a root dep, never a shipped surface). # workspace; never a root dep, never a shipped surface).
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/research/", "docs/sdd_process.md", "tasks/", "fuzz/"] exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/research/", "docs/sdd_process.md", "tasks/", "fuzz/"]
@@ -27,12 +27,12 @@ exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/rese
name = "alkssh" name = "alkssh"
[features] [features]
# ADR-003: flat and independent — no feature implies another; `default = []` # ADR-003: flat and independent, no feature implies another; `default = []`
# by design (a default build has no capabilities; every unfeatured open # by design (a default build has no capabilities; every unfeatured open
# declines fail-closed). The wave-2 decline core is the first `cfg` # declines fail-closed). The wave-2 decline core is the first `cfg`
# consumer; the wave-3 seam-config handles are the second (each optional # consumer; the wave-3 seam-config handles are the second (each optional
# sibling dep is gated feature-to-dep so a git-build never compiles # sibling dep is gated feature-to-dep so a git-build never compiles
# alktty and vice versa — the wave-1 manifest rule: per-feature sibling # alktty and vice versa, per the wave-1 manifest rule: per-feature sibling
# deps land with their first consuming wave, which for alktty/alkgit is # deps land with their first consuming wave, which for alktty/alkgit is
# the wave-3 config fields and for alktunnels `w3-tunnel-direct`, the # the wave-3 config fields and for alktunnels `w3-tunnel-direct`, the
# route-1 bridge). # route-1 bridge).
@@ -44,28 +44,28 @@ sftp = []
[dependencies] [dependencies]
# async-trait (the alkcall precedent) makes the injected handles # async-trait (the alkcall precedent) makes the injected handles
# dyn-compatible (`Arc<dyn ...>`) — the HostKeyPolicy dial handle needs # dyn-compatible (`Arc<dyn ...>`); the HostKeyPolicy dial handle needs
# that, and native AFIT traits are not object-safe at this toolchain. # that, and native AFIT traits are not object-safe at this toolchain.
async-trait = "0.1" async-trait = "0.1"
# ADR-002: aws-lc-rs only; default features off — no flate2, no rsa # ADR-002: aws-lc-rs only; default features off, meaning no flate2 and
# (ed25519-only; the tree stays RC-free except the ssh-key pin riding the # no rsa (ed25519-only; the tree stays RC-free except the ssh-key pin riding the
# russh keys path). Never enable russh's `des`/`dsa` features. # russh keys path). Never enable russh's `des`/`dsa` features.
russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] } russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] }
alkcall = "0.10" alkcall = "0.10"
# alkvault 0.2 (breaking req bump, 0.1 -> 0.2, 2026-11-04): `decrypt` # alkvault 0.2 (breaking req bump, 0.1 -> 0.2, 2026-11-04): `decrypt`
# returns `Zeroizing<String>` (alkvault review 001's resolution — the # returns `Zeroizing<String>` (alkvault review 001's resolution; the
# pre-wrap plaintext window ADR-005 decision 3 recorded is closed # pre-wrap plaintext window ADR-005 decision 3 recorded is closed
# upstream); the adapter-side wrap is dropped. Pin re-verified against # upstream); the adapter-side wrap is dropped. Pin re-verified against
# crates.io: MIT OR Apache-2.0. # crates.io: MIT OR Apache-2.0.
alkvault = "0.2" alkvault = "0.2"
# Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`). # Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`).
# Pins verified against crates.io (2026-11-04): alktty 0.8.2 (0.1.0's # Pins verified against crates.io (2026-11-04): alktty 0.8.2 (0.1.0's
# 0.8.1 pin refreshed — 0.8.2 is the PTY exit-signal-fidelity fix, # 0.8.1 pin refreshed; 0.8.2 is the PTY exit-signal-fidelity fix,
# patch-level, API/wire unchanged; alkssh consumes negative exit # patch-level, API/wire unchanged; alkssh consumes negative exit
# codes verbatim so the derivation contract needed no change), alkgit # codes verbatim so the derivation contract needed no change), alkgit
# 0.1.0 — both MIT OR Apache-2.0, both rust_version ≤ our 1.89, # 0.1.0: both MIT OR Apache-2.0, both rust_version ≤ our 1.89,
# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` → # both over alkcall ^0.10 (shared with our `alkcall = "0.10"` →
# 0.10.1 resolution). alktty's `local` feature stays off — alkssh # 0.10.1 resolution). alktty's `local` feature stays off; alkssh
# never touches a real backend; the `local` shapes stay alktty-side. # never touches a real backend; the `local` shapes stay alktty-side.
alktty = { version = "0.8.2", default-features = false, optional = true } alktty = { version = "0.8.2", default-features = false, optional = true }
alkgit = { version = "0.1.0", default-features = false, optional = true } alkgit = { version = "0.1.0", default-features = false, optional = true }
@@ -73,13 +73,13 @@ alkgit = { version = "0.1.0", default-features = false, optional = true }
# open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`) + the # open_direct`/`take_halves`, `SubstrateAddr`/`Substrate`) + the
# forwarded serving surface's establisher shape # forwarded serving surface's establisher shape
# (`register_tunnel_forwarded_openable_with_establisher` + # (`register_tunnel_forwarded_openable_with_establisher` +
# `ForwardedGate` — the review-003 W2 adoption, ADR-008 Amendment 2's # `ForwardedGate`, the review-003 W2 adoption, ADR-008 Amendment 2's
# recommended shape). Pin re-verified against crates.io (2026-11-04): # recommended shape). Pin re-verified against crates.io (2026-11-04):
# 0.5.0 is newest_version, additive API only over 0.4.1 (the wire # 0.5.0 is newest_version, additive API only over 0.4.1 (the wire
# delta is empty, the base registration is byte-for-byte), MIT OR # delta is empty, the base registration is byte-for-byte), MIT OR
# Apache-2.0, rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned # Apache-2.0, rust_version 1.88 ≤ our 1.89, alkcall ^0.10.1 pinned
# exactly (single-versioned against our `alkcall = "0.10"`). The # exactly (single-versioned against our `alkcall = "0.10"`). The
# `local` feature stays off — the dial seam is injected (`DialFn`); # `local` feature stays off (the dial seam is injected `DialFn`);
# the real dial/backends belong to the assembly, and the tests # the real dial/backends belong to the assembly, and the tests
# hand-roll their own dial/dial-fail seams (`tests/tunnel_mapping.rs`). # hand-roll their own dial/dial-fail seams (`tests/tunnel_mapping.rs`).
alktunnels = { version = "0.5.0", default-features = false, optional = true } alktunnels = { version = "0.5.0", default-features = false, optional = true }
@@ -87,7 +87,7 @@ alktunnels = { version = "0.5.0", default-features = false, optional = true }
# and the exit-chunk / negotiation-error-frame JSON reads in `tty_map.rs`. # and the exit-chunk / negotiation-error-frame JSON reads in `tty_map.rs`.
# Same pin alktty carries (crate resolution stays single-versioned). # Same pin alktty carries (crate resolution stays single-versioned).
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# session-only: the alktty chunk plane is `bytes`-based (`Chunk.bytes`) — # session-only: the alktty chunk plane is `bytes`-based (`Chunk.bytes`),
# the bridge builds sentinel/ctrl chunks with it. Same pin russh/alktty # the bridge builds sentinel/ctrl chunks with it. Same pin russh/alktty
# carry (crate resolution stays single-versioned). # carry (crate resolution stays single-versioned).
bytes = { version = "1", optional = true } bytes = { version = "1", optional = true }
@@ -99,20 +99,20 @@ zeroize = "1.9"
[dev-dependencies] [dev-dependencies]
# The git-seam test doubles name `GitPackGen`'s `gix_hash::ObjectId` # The git-seam test doubles name `GitPackGen`'s `gix_hash::ObjectId`
# parameters and alkgit does not re-export them — test-graph only, # parameters and alkgit does not re-export them (test-graph only),
# version matched to alkgit's `^0.28` req (same crate in the lockfile). # version matched to alkgit's `^0.28` req (same crate in the lockfile).
gix-hash = { version = "0.28", features = ["sha1"] } gix-hash = { version = "0.28", features = ["sha1"] }
# The session shim's scripted stdout/stderr streams ride tokio's # The session shim's scripted stdout/stderr streams ride tokio's
# `ReceiverStream` — test-graph only (the crate proper hand-rolls no # `ReceiverStream`, test-graph only (the crate proper hand-rolls no
# streams). # streams).
tokio-stream = "0.1" tokio-stream = "0.1"
# The leg-2 fabric double's establisher/params serialization # The leg-2 fabric double's establisher/params serialization
# (`w3-tunnel-listen`) — test-graph only (the door's own lib code # (`w3-tunnel-listen`), test-graph only (the door's own lib code
# stays serde-free under the `tunnel` feature; the double's # stays serde-free under the `tunnel` feature; the double's
# dynamic-bind producer serializes params/reply fields). # dynamic-bind producer serializes params/reply fields).
serde_json = "1" serde_json = "1"
# The real-tool pty smoke (`w3-tty-session`): alktty's `local` backend # The real-tool pty smoke (`w3-tty-session`): alktty's `local` backend
# composition — the dev-dep `features = ["local"]` unification applies # composition: the dev-dep `features = ["local"]` unification applies
# to the TEST graph only (feature unification folds `local` into the # to the TEST graph only (feature unification folds `local` into the
# optional alktty dep in test builds; the published lib never enables # optional alktty dep in test builds; the published lib never enables
# it and `src/` never names a local shape). Same 0.8.2 pin. # it and `src/` never names a local shape). Same 0.8.2 pin.
+16 -16
View File
@@ -1,16 +1,16 @@
# alkssh fuzzing # alkssh fuzzing
libFuzzer targets for alkssh's ingress surfaces — the family standing libFuzzer targets for alkssh's ingress surfaces: the family standing
rule (anything worth fuzzing gets fuzzed), landed per the plan's wave-5 rule (anything worth fuzzing gets fuzzed), landed per the plan's wave-5
charter and the alkgit convention: the JSON schema gate itself is charter and the alkgit convention. The JSON schema gate itself is
alkcall's surface, family-fuzzed there; SSH framing, key exchange, and alkcall's surface, family-fuzzed there; SSH framing, key exchange, and
the channel multiplexer are russh's wire (family ADR-003) — the the channel multiplexer are russh's wire (family ADR-003); the
alkssh-owned post-framing ingress is what fuzzes here. alkssh-owned post-framing ingress is what fuzzes here.
## Layout ## Layout
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers. - `fuzz_targets/`: one binary per target; thin `fuzz_target!` wrappers.
- `shared/` — the invariant logic, as a plain library so normal - `shared/`: the invariant logic, as a plain library so normal
`cargo test` (stable toolchain) can replay the committed corpora `cargo test` (stable toolchain) can replay the committed corpora
through the same invariants (`#[cfg(test)]` modules; quinn's CI through the same invariants (`#[cfg(test)]` modules; quinn's CI
pattern). The fuzz binaries are nightly-only; the shared crate is pattern). The fuzz binaries are nightly-only; the shared crate is
@@ -18,34 +18,34 @@ alkssh-owned post-framing ingress is what fuzzes here.
`#[doc(hidden)]` fuzz-visible exposures: the exec-grammar parse path `#[doc(hidden)]` fuzz-visible exposures: the exec-grammar parse path
in `src/dispatch.rs`, the key-decode arm `decode_key_arm` in in `src/dispatch.rs`, the key-decode arm `decode_key_arm` in
`src/key_source.rs`, and the `terminal_params` conversion in `src/key_source.rs`, and the `terminal_params` conversion in
`src/tty_map.rs` — the recorded exposure decisions: the ingresses are `src/tty_map.rs`; the recorded exposure decisions: the ingresses are
production-private, so the exposures are the production surfaces production-private, so the exposures are the production surfaces
themselves, never forks; the fingerprint ingress is fully public API) themselves, never forks; the fingerprint ingress is fully public API)
and must never become a dependency of the root crate. and must never become a dependency of the root crate.
- `corpus/<target>/` — committed hand-made seeds (regenerate with - `corpus/<target>/`: committed hand-made seeds (regenerate with
`python3 fuzz/gen_fuzz_seeds.py` from the repo root — must be a no-op `python3 fuzz/gen_fuzz_seeds.py` from the repo root; the result must
diff on the committed set). Only `seed-*` files are tracked; grown be a no-op diff on the committed set). Only `seed-*` files are tracked; grown
corpora (SHA-named units), artifacts, and coverage output are corpora (SHA-named units), artifacts, and coverage output are
gitignored and are **not** merged back into the committed seeds. gitignored and are **not** merged back into the committed seeds.
- `run-detached.sh` — mandatory runner for agent sessions: wraps - `run-detached.sh`: the mandatory runner for agent sessions. It wraps
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM `cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
in a target can never take down the agent host. in a target can never take down the agent host.
- `gen_fuzz_seeds.py` — deterministic seed generator (quiche pattern). - `gen_fuzz_seeds.py`: deterministic seed generator (quiche pattern).
## Targets ## Targets
| Target | Drives | Invariants | | Target | Drives | Invariants |
|---|---|---| |---|---|---|
| `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs` — the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic | | `exec_parse` | the production exec-grammar parse (`parse_exec`, `src/dispatch.rs`, the handler's ingress shape: byte string → `from_utf8` fallback `""` → parse; the pre-parse `max_exec_command_len` cap and the git row's `invalid-repo-id` gate are the root contract rows' territory, the parse is everything behind them) | no-panic on any byte string (any length behind the cap); the typed routing classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) agree with a from-spec grammar oracle (the differential pair) and are stable per input; for `Git` routes the repo-id gate classification is deterministic |
| `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64` — ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) | | `fingerprint` | the crate's single fingerprint path ([`alkssh::fingerprint`] over `russh::keys::parse_public_key_base64`, ADR-004 decision 1) | no-panic on arbitrary key blobs (a non-blob is the parse's typed refusal); every accepted key's fingerprint is exactly the `SHA256:` + unpadded-standard-base64 contract shape (43-char body, canonical alphabet, decodes to a 32-byte digest, re-encodes canonically); determinism; the committed `ssh-keygen -lf` parity vectors still agree (the corpus-gate test row) |
| `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs` — the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (robustness drive — the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class — never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism | | `key_decode` | the decrypt-at-use chain's decode arm (`decode_key_arm`, `src/key_source.rs`, the production `decrypt_and_decode` conversion exposed `#[doc(hidden)]`) + the underlying `decode_secret_key` passphrase arm (a robustness drive; the production chain always passes `None`) | no-panic on arbitrary plaintext shapes (the vault's `String` shape, lossy from arbitrary bytes) and passphrase shapes; every failure on the production arm is exactly the typed `KeySourceError::Decode` class, never a catch-all, never a panic; the failure text never carries the plaintext; the `Zeroizing` posture rides the drive (the wrap is what the adapter seam applies); determinism |
| `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs` — OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed — reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input | | `terminal_params` | the `pty_request` args → `TerminalParamsWire` conversion (`terminal_params`, `src/tty_map.rs`, OQ-44's SSH modes → reserved-modes `{}` JSON) | no-panic on arbitrary term strings, u32 dimensions, and mode blobs (the modes are never parsed; they are reserved); the dimension clamp is exactly the recorded cap (`min(value, 65535)`, never a magnitude truncation); `term` preserved verbatim; the reserved-modes JSON is always exactly `{}`; the full serialized wire shape is deterministic per input |
## Commands ## Commands
`cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/` `cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/`
(or deeper) as the working directory so rustup selects the pinned (or deeper) as the working directory so rustup selects the pinned
nightly toolchain — the detached runner handles that itself. nightly toolchain; the detached runner handles that itself.
```sh ```sh
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/) # build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
+2 -2
View File
@@ -1,7 +1,7 @@
#![no_main] #![no_main]
//! The exec-grammar target (`mapping.md` §Session-channel dispatch — //! The exec-grammar target (`mapping.md` §Session-channel dispatch,
//! the git-parseable exec grammar + the dispatch parse path) — thin //! the git-parseable exec grammar + the dispatch parse path): a thin
//! wrapper over the shared invariant battery. //! wrapper over the shared invariant battery.
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
+2 -2
View File
@@ -1,7 +1,7 @@
#![no_main] #![no_main]
//! The fingerprint-format target (ADR-004 decision 1 — the `SHA256:` + //! The fingerprint-format target (ADR-004 decision 1, the `SHA256:` +
//! unpadded base64 path) — thin wrapper over the shared invariant //! unpadded base64 path): a thin wrapper over the shared invariant
//! battery. //! battery.
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
+2 -2
View File
@@ -1,7 +1,7 @@
#![no_main] #![no_main]
//! The key-decode target (ADR-005 decision 2 — the decrypt-at-use //! The key-decode target (ADR-005 decision 2, the decrypt-at-use
//! chain's `decode_secret_key` seam) — thin wrapper over the shared //! chain's `decode_secret_key` seam): a thin wrapper over the shared
//! invariant battery. //! invariant battery.
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
+2 -2
View File
@@ -1,8 +1,8 @@
#![no_main] #![no_main]
//! The terminal-parameters target (`tty_map.rs` — the `pty_request` //! The terminal-parameters target (`tty_map.rs`, the `pty_request`
//! args → `TerminalParamsWire` conversion, the SSH modes → reserved //! args → `TerminalParamsWire` conversion, the SSH modes → reserved
//! modes JSON) — thin wrapper over the shared invariant battery. //! modes JSON): a thin wrapper over the shared invariant battery.
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
+1 -1
View File
@@ -1,7 +1,7 @@
//! The corpus-replay plumbing: the committed seeds replay through the //! The corpus-replay plumbing: the committed seeds replay through the
//! invariant batteries on the stable toolchain (`cargo test //! invariant batteries on the stable toolchain (`cargo test
//! --manifest-path fuzz/shared/Cargo.toml` runs this in //! --manifest-path fuzz/shared/Cargo.toml` runs this in
//! `#[cfg(test)]` modules across the ingress units — the quinn CI //! `#[cfg(test)]` modules across the ingress units, the quinn CI
//! pattern), so the gate never needs a fuzzing session. //! pattern), so the gate never needs a fuzzing session.
pub fn read_corpus(target: &str) -> Vec<Vec<u8>> { pub fn read_corpus(target: &str) -> Vec<Vec<u8>> {
+7 -7
View File
@@ -1,10 +1,10 @@
//! The exec-grammar ingress (`mapping.md` §Session-channel dispatch, //! The exec-grammar ingress (`mapping.md` §Session-channel dispatch,
//! `dispatch.rs`): the production `parse_exec` path driven on arbitrary //! `dispatch.rs`): the production `parse_exec` path driven on arbitrary
//! command strings — no-panic, typed routing classes, and agreement //! command strings: no-panic, typed routing classes, and agreement
//! with a from-spec re-derivation of the grammar. The production //! with a from-spec re-derivation of the grammar. The production
//! ingress shape is the handler's: the per-length cap //! ingress shape is the handler's: the per-length cap
//! (`Limits::max_exec_command_len`) is enforced *before* the parse //! (`Limits::max_exec_command_len`) is enforced *before* the parse
//! (decline first, alloc never — the cap's pre-parse placement is //! (decline first, alloc never; the cap's pre-parse placement is
//! pinned by the root's contract rows in `tests/dispatch_core.rs`); //! pinned by the root's contract rows in `tests/dispatch_core.rs`);
//! everything behind the cap is this battery: a byte string → //! everything behind the cap is this battery: a byte string →
//! `std::str::from_utf8(..).unwrap_or("")` → the parse (invalid UTF-8 //! `std::str::from_utf8(..).unwrap_or("")` → the parse (invalid UTF-8
@@ -43,7 +43,7 @@ pub fn fuzz_exec_parse(data: &[u8]) {
/// The from-spec oracle: the grammar re-derived from the dispatch /// The from-spec oracle: the grammar re-derived from the dispatch
/// module's documented rules, sharing no code with the /// module's documented rules, sharing no code with the
/// implementation — the differential pair (the repo_id target's /// implementation, the differential pair (the repo_id target's
/// oracle pattern). /// oracle pattern).
fn spec_parse(command: &str) -> ExecRoute { fn spec_parse(command: &str) -> ExecRoute {
let (token, argument) = match command.split_once(' ') { let (token, argument) = match command.split_once(' ') {
@@ -64,7 +64,7 @@ fn spec_parse(command: &str) -> ExecRoute {
} }
/// The serving-service arm of the oracle: a serving token must carry /// The serving-service arm of the oracle: a serving token must carry
/// a non-empty, path-shaped argument (SSH exec must carry the repo — /// a non-empty, path-shaped argument (SSH exec must carry the repo;
/// service-only is the decline-with-reason class; anything else /// service-only is the decline-with-reason class; anything else
/// unparseable is the pipe row). /// unparseable is the pipe row).
fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute { fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
@@ -78,9 +78,9 @@ fn spec_serving(service: GitServiceRoute, argument: Option<&str>) -> ExecRoute {
} }
} }
/// The argument grammar re-derived: a path-shaped argument — fully /// The argument grammar re-derived: a path-shaped argument, either
/// single-quote-wrapped (a non-empty inner ending in a quote) or /// fully single-quote-wrapped (a non-empty inner ending in a quote) or
/// unquoted absolute — normalized by stripping the quotes, one /// unquoted absolute, normalized by stripping the quotes, one
/// leading `/`, and one trailing `.git` suffix (an empty-after-strip /// leading `/`, and one trailing `.git` suffix (an empty-after-strip
/// prefix keeps the suffix; the git row's validation declines the /// prefix keeps the suffix; the git row's validation declines the
/// empties, never a silent emptying). /// empties, never a silent emptying).
+4 -4
View File
@@ -1,9 +1,9 @@
//! The fingerprint format ingress (ADR-004 decision 1, //! The fingerprint format ingress (ADR-004 decision 1,
//! `fingerprint.rs`): arbitrary key blobs through the crate's single //! `fingerprint.rs`): arbitrary key blobs through the crate's single
//! fingerprint path — the composition of russh's public OpenSSH blob //! fingerprint path: the composition of russh's public OpenSSH blob
//! parse (`russh::keys::parse_public_key_base64`, the same re-export //! parse (`russh::keys::parse_public_key_base64`, the same re-export
//! the crate's surface rides) and the crate's public //! the crate's surface rides) and the crate's public
//! [`alkssh::fingerprint`]. The ingress is fully public API — the //! [`alkssh::fingerprint`]. The ingress is fully public API; the
//! recorded exposure decision needs no `#[doc(hidden)]` surface. //! recorded exposure decision needs no `#[doc(hidden)]` surface.
//! //!
//! Invariants: no-panic on any byte string (a non-blob is the parse's //! Invariants: no-panic on any byte string (a non-blob is the parse's
@@ -11,7 +11,7 @@
//! accepted key's fingerprint is exactly the `SHA256:` + unpadded //! accepted key's fingerprint is exactly the `SHA256:` + unpadded
//! standard base64 contract shape (one `:` separator, 43-char body, //! standard base64 contract shape (one `:` separator, 43-char body,
//! canonical alphabet, 32-byte digest decoded exactly); determinism //! canonical alphabet, 32-byte digest decoded exactly); determinism
//! (same key, same string — and the same string re-parses to the same //! (same key, same string; the same string re-parses to the same
//! verdict). The committed `ssh-keygen -lf` parity vectors //! verdict). The committed `ssh-keygen -lf` parity vectors
//! (`tests/vectors/`) are replayed by the corpus-gate tests below. //! (`tests/vectors/`) are replayed by the corpus-gate tests below.
@@ -98,7 +98,7 @@ fn decode_unpadded(body: &str) -> Option<Vec<u8>> {
Some(out) Some(out)
} }
/// A from-spec unpadded standard-base64 encoder — the re-encode half. /// A from-spec unpadded standard-base64 encoder: the re-encode half.
fn encode_unpadded(bytes: &[u8]) -> String { fn encode_unpadded(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = Vec::new(); let mut out = Vec::new();
+10 -10
View File
@@ -1,30 +1,30 @@
//! The key-decode ingress (ADR-005 decision 2, `key_source.rs`): the //! The key-decode ingress (ADR-005 decision 2, `key_source.rs`): the
//! decrypt-at-use chain's one-time `decode_secret_key` conversion — //! decrypt-at-use chain's one-time `decode_secret_key` conversion,
//! driven through the production arm itself //! driven through the production arm itself
//! ([`alkssh::decode_key_arm`], the `#[doc(hidden)]` exposure that is //! ([`alkssh::decode_key_arm`], the `#[doc(hidden)]` exposure that is
//! `decrypt_and_decode`'s exact decode stage, not a fork). //! `decrypt_and_decode`'s exact decode stage, not a fork).
//! //!
//! Invariants: no-panic on arbitrary plaintext shapes (the production //! Invariants: no-panic on arbitrary plaintext shapes (the production
//! plaintext is the vault's `Zeroizing<String>` — valid UTF-8 by //! plaintext is the vault's `Zeroizing<String>`, valid UTF-8 by
//! return type; the battery shapes arbitrary bytes into that form via //! return type; the battery shapes arbitrary bytes into that form via
//! the lossy conversion and drives it through the same zeroized //! the lossy conversion and drives it through the same zeroized
//! wrap the vault's `decrypt` return applies, alkssh 0.2.0 seam) and //! wrap the vault's `decrypt` return applies, alkssh 0.2.0 seam) and
//! arbitrary passphrase shapes (the //! arbitrary passphrase shapes (the
//! underlying decode's passphrase arm — the production chain always //! underlying decode's passphrase arm; the production chain always
//! passes `None`; the `Some` arm is robustness drive only); every //! passes `None`; the `Some` arm is robustness drive only); every
//! failure is the typed [`KeySourceError::Decode`] class — never a //! failure is the typed [`KeySourceError::Decode`] class, never a
//! catch-all, never another variant on this arm, never a panic; the //! catch-all, never another variant on this arm, never a panic; the
//! failure text never carries the plaintext (the assertable half of //! failure text never carries the plaintext (the assertable half of
//! the Zeroizing posture — the plaintext exists solely inside the //! the Zeroizing posture: the plaintext exists solely inside the
//! call, dropped zeroized; no retention, no static cache — and the //! call, dropped zeroized, with no retention and no static cache; the
//! typed error's only payload is the decode machinery's message //! typed error's only payload is the decode machinery's message
//! class, never the plaintext). //! class, never the plaintext).
//! //!
//! The no-echo invariant's assertable form is *the bounded-message //! The no-echo invariant's assertable form is *the bounded-message
//! pair* (reshaped in-task after the first campaign's finding): the //! pair* (reshaped in-task after the first campaign's finding): the
//! failure text is a bounded fixed message (≤ 256 bytes — the decode //! failure text is a bounded fixed message (≤ 256 bytes; the decode
//! machinery's vocabulary, never a growable echo), and no 256-byte //! machinery's vocabulary, never a growable echo), and no 256-byte
//! contiguous plaintext run appears in it — which, given the bound, //! contiguous plaintext run appears in it, which, given the bound,
//! cannot false-fire (a string shorter than 256 bytes cannot contain a //! cannot false-fire (a string shorter than 256 bytes cannot contain a
//! 256-byte run). A plaintext-`contains`-message check in the other //! 256-byte run). A plaintext-`contains`-message check in the other
//! direction was unsound: an attacker-chosen plaintext can be a //! direction was unsound: an attacker-chosen plaintext can be a
@@ -70,7 +70,7 @@ fn fingerprint(key: &russh::keys::PrivateKey) -> String {
} }
/// The typed-class invariant: `Ok` or exactly /// The typed-class invariant: `Ok` or exactly
/// [`KeySourceError::Decode`] — the decode arm's only failure shape — /// [`KeySourceError::Decode`] (the decode arm's only failure shape),
/// and the Decode text is the bounded fixed-message pair (see the /// and the Decode text is the bounded fixed-message pair (see the
/// module docs; the two assertions are jointly airtight). /// module docs; the two assertions are jointly airtight).
fn typed_decode_class(outcome: &Result<russh::keys::PrivateKey, KeySourceError>, plaintext: &str) { fn typed_decode_class(outcome: &Result<russh::keys::PrivateKey, KeySourceError>, plaintext: &str) {
@@ -123,7 +123,7 @@ mod invariants {
} }
/// A plaintext that is not OpenSSH-PEM key material is a /// A plaintext that is not OpenSSH-PEM key material is a
/// [`KeySourceError::Decode`] refusal (the inner-format contract — /// [`KeySourceError::Decode`] refusal (the inner-format contract:
/// decision 1, never a fallback). /// decision 1, never a fallback).
#[test] #[test]
fn non_key_plaintext_is_the_typed_decode_refusal() { fn non_key_plaintext_is_the_typed_decode_refusal() {
+5 -5
View File
@@ -1,22 +1,22 @@
//! The terminal-parameters ingress (`tty_map.rs`, OQ-44's reserved //! The terminal-parameters ingress (`tty_map.rs`, OQ-44's reserved
//! modes): the `pty_request` args → `TerminalParamsWire` conversion //! modes): the `pty_request` args → `TerminalParamsWire` conversion
//! driven through the production [terminal_params] surface (the //! driven through the production [terminal_params] surface (the
//! `#[doc(hidden)]` exposure — `handler.rs`'s `pty_request` row //! `#[doc(hidden)]` exposure: `handler.rs`'s `pty_request` row
//! constructs the stored params with exactly this function). //! constructs the stored params with exactly this function).
//! //!
//! The harness's framed feed (deterministic, seed-friendly): //! The harness's framed feed (deterministic, seed-friendly):
//! `term_len` (u32 LE, bounded to the remaining input) + the term //! `term_len` (u32 LE, bounded to the remaining input) + the term
//! bytes + four u32 LE dimensions (`cols`/`rows`/pixel width/height, //! bytes + four u32 LE dimensions (`cols`/`rows`/pixel width/height,
//! zero-defaulted when the input runs out) + an arbitrary trailing //! zero-defaulted when the input runs out) + an arbitrary trailing
//! mode blob. The mode blob is *never parsed* — the SSH ttymodes ride //! mode blob. The mode blob is *never parsed*; the SSH ttymodes ride
//! the reserved `{}` JSON (OQ-44, backends ignore modes in v1) — so //! the reserved `{}` JSON (OQ-44, backends ignore modes in v1), so
//! the battery's totality over arbitrary mode blobs is by //! the battery's totality over arbitrary mode blobs is by
//! construction; the framing demonstrates it (the conversion's //! construction; the framing demonstrates it (the conversion's
//! outcome never varies with the blob). //! outcome never varies with the blob).
//! //!
//! Invariants: no-panic on any term string (empty, unicode, control //! Invariants: no-panic on any term string (empty, unicode, control
//! characters) and any u32 dimensions; the dimension clamp is the //! characters) and any u32 dimensions; the dimension clamp is the
//! recorded cap (`min(value, 65535)` — a dimension beyond 65535 is //! recorded cap (`min(value, 65535)`; a dimension beyond 65535 is
//! nonsense on both papers) computed exactly, never a magnitude //! nonsense on both papers) computed exactly, never a magnitude
//! truncation; `term` is preserved verbatim; the reserved-modes JSON //! truncation; `term` is preserved verbatim; the reserved-modes JSON
//! is always exactly the empty object; the full wire shape is //! is always exactly the empty object; the full wire shape is
@@ -89,7 +89,7 @@ pub fn fuzz_terminal_params(data: &[u8]) {
} }
} }
/// The cursor's u32 (zero-defaulted when the framed field is absent — /// The cursor's u32 (zero-defaulted when the framed field is absent;
/// the feed is total over any input). /// the feed is total over any input).
fn take_u32(cursor: &mut &[u8]) -> u32 { fn take_u32(cursor: &mut &[u8]) -> u32 {
if cursor.len() >= 4 { if cursor.len() >= 4 {
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# The decline-matrix runner (tasks/implementation/w5-decline-matrix.md): # The decline-matrix runner (tasks/implementation/w5-decline-matrix.md):
# the per-build verification set as one command — `cargo test` per # the per-build verification set as one command: `cargo test` per
# feature build (none / git / session / tunnel / sftp-only / # feature build (none / git / session / tunnel / sftp-only /
# --all-features; the sftp-only run is the no-op-feature fact itself # --all-features; the sftp-only run is the no-op-feature fact itself
# verified), clippy ×2 (-D warnings, with and without # verified), clippy ×2 (-D warnings, with and without
+19 -18
View File
@@ -1,8 +1,8 @@
//! The server-direction mount point (ADR-001) — [`SshAdapter`], the //! The server-direction mount point (ADR-001): [`SshAdapter`], the
//! constructed russh server stack every mapping composes into. //! constructed russh server stack every mapping composes into.
//! //!
//! Construction ([`SshAdapter::new`], ADR-005 decision 6): the //! Construction ([`SshAdapter::new`], ADR-005 decision 6): the
//! injected [`HostKeySource`] loads **decrypt-at-construction** — a //! injected [`HostKeySource`] loads **decrypt-at-construction**; a
//! locked / absent / decryption-failed / unknown-version / undecodable //! locked / absent / decryption-failed / unknown-version / undecodable
//! load means **no server is constructed**: the error surfaces as //! load means **no server is constructed**: the error surfaces as
//! [`AdapterError::Construction`] with the original [`KeySourceError`] //! [`AdapterError::Construction`] with the original [`KeySourceError`]
@@ -11,34 +11,35 @@
//! host-key source contract's [`KeySourceError::Absent`] shape). //! host-key source contract's [`KeySourceError::Absent`] shape).
//! //!
//! The russh server `Config` (ADR-002 surface notes; ADR-004 decision //! The russh server `Config` (ADR-002 surface notes; ADR-004 decision
//! 4): `methods` is `{publickey}` **exactly** (honest advertisement — //! 4): `methods` is `{publickey}` **exactly** (honest advertisement:
//! `MethodSet::from(&[MethodKind::PublicKey][..])`, never russh's //! `MethodSet::from(&[MethodKind::PublicKey][..])`, never russh's
//! `server_supported()`), the loaded host keys ride `Config::keys`, //! `server_supported()`), the loaded host keys ride `Config::keys`,
//! and the [`Limits`] knobs map one-for-one: //! and the [`Limits`] knobs map one-for-one:
//! `window_size` / `maximum_packet_size` / `channel_buffer_size` / //! `window_size` / `maximum_packet_size` / `channel_buffer_size` /
//! `max_auth_attempts` verbatim, `inactivity_timeout` as **always //! `max_auth_attempts` verbatim, `inactivity_timeout` as **always
//! `Some`** — the bounded-resources posture (the crate never //! `Some`**, the bounded-resources posture (the crate never
//! constructs a session with no wall clock). Everything else stays at //! constructs a session with no wall clock). Everything else stays at
//! russh 0.65's own default posture (`Config::default()`, left //! russh 0.65's own default posture (`Config::default()`, left
//! un-touched deliberately): the rekey `Limits` stay russh-enforced at //! un-touched deliberately): the rekey `Limits` stay russh-enforced at
//! their RFC-4253 §9 defaults, `auth_rejection_time` stays russh's //! their RFC-4253 §9 defaults, `auth_rejection_time` stays russh's
//! constant-rejection knob, and the preferred-algorithm set stays //! constant-rejection knob, and the preferred-algorithm set stays
//! russh's — the ed25519-only key-algorithm posture holds by the //! russh's; the ed25519-only key-algorithm posture holds by the
//! loaded key set itself (ADR-002 decision 4) and by the auth bridge's //! loaded key set itself (ADR-002 decision 4) and by the auth bridge's
//! explicit non-ed25519 decline (`w2-auth-bridge`). //! explicit non-ed25519 decline (`w2-auth-bridge`).
//! //!
//! The mount ([`SshAdapter::handle`], ADR-001's contract): a generic //! The mount ([`SshAdapter::handle`], ADR-001's contract): a generic
//! byte-duplex stream in, the full russh server stack per stream via //! byte-duplex stream in, the full russh server stack per stream via
//! [`russh::server::run_stream`], returned **run to completion** — //! [`russh::server::run_stream`], returned **run to completion**;
//! the edge-termination shape only, no ALPN code, no relay leg (the //! the edge-termination shape only, no ALPN code, no relay leg (the
//! `alk/ssh` pin is a ledger entry; nothing here speaks it). The //! `alk/ssh` pin is a ledger entry; nothing here speaks it). The
//! in-flight framing is the alkhttp door precedent: `handle` is an //! in-flight framing is the alkhttp door precedent: `handle` is an
//! async per-stream call whose completion is the session's end, so the //! async per-stream call whose completion is the session's end, so the
//! assembly's accept loop mounts per stream via `tokio::spawn` — the //! assembly's accept loop mounts per stream via `tokio::spawn`, and
//! serving loop itself stays spawned inside russh (ADR-006 decision 4, //! the serving loop itself stays spawned inside russh (ADR-006
//! which `run_stream` already satisfies: the session's packet loop //! decision 4, which `run_stream` already satisfies: the session's
//! leaves on its own task), and the mount's return type stays //! packet loop leaves on its own task), and the mount's return type
//! `Result<(), AdapterError>` — no russh type rides the signature. //! stays `Result<(), AdapterError>` (no russh type rides the
//! signature).
use std::sync::Arc; use std::sync::Arc;
@@ -53,7 +54,7 @@ use crate::handler::SessionHandler;
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
use crate::tunnel_map; use crate::tunnel_map;
/// The constructed server stack — the substrate every channel-type /// The constructed server stack, the substrate every channel-type
/// mapping composes into (ADR-001's `SshAdapter`). /// mapping composes into (ADR-001's `SshAdapter`).
/// ///
/// Built by [`SshAdapter::new`] from the assembly-supplied /// Built by [`SshAdapter::new`] from the assembly-supplied
@@ -67,8 +68,8 @@ pub struct SshAdapter {
} }
impl SshAdapter { impl SshAdapter {
/// Constructs the adapter — decrypt-at-construction of the host /// Constructs the adapter: decrypt-at-construction of the host
/// key set (ADR-005 decision 6): a failed or empty load is no /// key set (ADR-005 decision 6); a failed or empty load is no
/// server, with the [`KeySourceError`] refused through /// server, with the [`KeySourceError`] refused through
/// [`AdapterError::Construction`]. /// [`AdapterError::Construction`].
pub async fn new(serve: ServeConfig) -> Result<Self, AdapterError> { pub async fn new(serve: ServeConfig) -> Result<Self, AdapterError> {
@@ -98,7 +99,7 @@ impl SshAdapter {
}) })
} }
/// The mounted russh server `Config` — in-crate inspection for the /// The mounted russh server `Config`, in-crate inspection for the
/// contract tests (advertisement and knob mapping); not public /// contract tests (advertisement and knob mapping); not public
/// surface: the mount's behavior is observable on the wire, never /// surface: the mount's behavior is observable on the wire, never
/// through a russh type leak. /// through a russh type leak.
@@ -108,7 +109,7 @@ impl SshAdapter {
} }
/// The mount (ADR-001): mounts the full russh server stack on one /// The mount (ADR-001): mounts the full russh server stack on one
/// generic duplex byte stream and runs it to completion — an /// generic duplex byte stream and runs it to completion; an
/// accepted vanilla-SSH transport is authenticated and served on /// accepted vanilla-SSH transport is authenticated and served on
/// this call's task; the assembly's accept loop mounts per stream /// this call's task; the assembly's accept loop mounts per stream
/// with `tokio::spawn(adapter.handle(stream))`. /// with `tokio::spawn(adapter.handle(stream))`.
@@ -126,7 +127,7 @@ impl SshAdapter {
} }
} }
/// The adapter's own `Config` construction from the serve-config — the /// The adapter's own `Config` construction from the serve-config: the
/// documented subset mapped one-for-one, everything else at russh's /// documented subset mapped one-for-one, everything else at russh's
/// default posture (see the module docs; ADR-002 surface notes). /// default posture (see the module docs; ADR-002 surface notes).
fn build_server_config(serve: &ServeConfig, keys: Vec<PrivateKey>) -> russh::server::Config { fn build_server_config(serve: &ServeConfig, keys: Vec<PrivateKey>) -> russh::server::Config {
@@ -160,7 +161,7 @@ mod tests {
const NOOP: alkcall::core::auth::NoopIdentityProvider = const NOOP: alkcall::core::auth::NoopIdentityProvider =
alkcall::core::auth::NoopIdentityProvider; alkcall::core::auth::NoopIdentityProvider;
/// The committed test-key fixtures — the wave-1 fixture set, read /// The committed test-key fixtures (the wave-1 fixture set), read
/// through `include_str!` per the wave-1 convention (the /// through `include_str!` per the wave-1 convention (the
/// `tests/common` double module is the tests/-side owner; src-unit /// `tests/common` double module is the tests/-side owner; src-unit
/// tests read the fixture files, never its exports). /// tests read the fixture files, never its exports).
+24 -24
View File
@@ -1,9 +1,9 @@
//! The in-crate concurrent-channel cap (`w2-bounded-resources`) — the //! The in-crate concurrent-channel cap (`w2-bounded-resources`): the
//! bound itself, carried by the per-connection handler //! bound itself, carried by the per-connection handler
//! ([`crate::handler`]) and enforced at every `channel_open_*` accept //! ([`crate::handler`]) and enforced at every `channel_open_*` accept
//! path (`overview.md` §Bounded-resources policy: "concurrent channels //! path (`overview.md` §Bounded-resources policy: "concurrent channels
//! per session — ours (in-crate) + per-identity policy when bridged"; //! per session (ours, in-crate) + per-identity policy when bridged";
//! AGENTS.md convention 9 — SSH servers are internet-facing; unbounded //! AGENTS.md convention 9: SSH servers are internet-facing; unbounded
//! loops and unbounded buffers are bugs). //! loops and unbounded buffers are bugs).
//! //!
//! ## Shape //! ## Shape
@@ -11,12 +11,12 @@
//! [`ChannelCap`] is a per-session count of active channels. Admission //! [`ChannelCap`] is a per-session count of active channels. Admission
//! ([`ChannelCap::admit`]) is the check-and-increment the accept paths //! ([`ChannelCap::admit`]) is the check-and-increment the accept paths
//! call: room under the cap admits (the count rises), otherwise the //! call: room under the cap admits (the count rises), otherwise the
//! open is refused — a clean decline, **never queued, never half-open** //! open is refused: a clean decline, **never queued, never half-open**
//! (the fail-closed posture; the refusal reason is //! (the fail-closed posture; the refusal reason is
//! [`crate::dispatch::cap_exceeded_open_decline`]). Release //! [`crate::dispatch::cap_exceeded_open_decline`]). Release
//! ([`ChannelCap::release`]) fires from the `channel_close` hook for //! ([`ChannelCap::release`]) fires from the `channel_close` hook for
//! established channels — and, since `w3-tunnel-direct`, from a //! established channels, and, since `w3-tunnel-direct`, from a
//! spawned route-1 bridge task on a fabric-open failure — and, since //! spawned route-1 bridge task on a fabric-open failure, and, since
//! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the //! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the
//! never-established paths (client-rejected open, dead session): see //! never-established paths (client-rejected open, dead session): see
//! the release-sites section. //! the release-sites section.
@@ -27,36 +27,36 @@
//! Every admission has exactly one release, and the paths are //! Every admission has exactly one release, and the paths are
//! disjoint by the wire shape: //! disjoint by the wire shape:
//! //!
//! 1. **The established-channel close** — the `channel_close` hook. //! 1. **The established-channel close**: the `channel_close` hook.
//! russh routes `CHANNEL_CLOSE` only for channels it considers //! russh routes `CHANNEL_CLOSE` only for channels it considers
//! established; a declined open never established one, and a close //! established; a declined open never established one, and a close
//! is consumed at most once (russh removes the channel before the //! is consumed at most once (russh removes the channel before the
//! hook). //! hook).
//! 2. **The admitted-but-rejected bridge** — the route-1 bridge's //! 2. **The admitted-but-rejected bridge**: the route-1 bridge's
//! failure path (`w3-tunnel-direct`). The open hook admits and //! failure path (`w3-tunnel-direct`). The open hook admits and
//! holds the count across the fabric open; on fabric failure the //! holds the count across the fabric open; on fabric failure the
//! open is rejected with the explicit `ConnectFailed` //! open is rejected with the explicit `ConnectFailed`
//! (ADR-006 decision 1: accept only after the fabric open //! (ADR-006 decision 1: accept only after the fabric open
//! succeeds), the channel never establishes, and *no //! succeeds), the channel never establishes, and *no
//! `channel_close` can ever fire* — so the count must be released //! `channel_close` can ever fire*, so the count must be released
//! from where the failure is known: inside the spawned bridge //! from where the failure is known: inside the spawned bridge
//! task, **before** the reject is answered (the very next open //! task, **before** the reject is answered (the very next open
//! then observes the recovered budget — pinned by wire test). A //! then observes the recovered budget; pinned by wire test). A
//! success never releases there (its close rides path 1). //! success never releases there (its close rides path 1).
//! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric //! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric
//! arrival is admitted into the owning route's count and opens the //! arrival is admitted into the owning route's count and opens the
//! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward //! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward
//! the vanilla client through the session handle. A rejected open — //! the vanilla client through the session handle. A rejected open
//! the client's own refusal, or a dead session's send failure — //! (the client's own refusal, or a dead session's send failure)
//! never establishes the channel, so the count releases inside the //! never establishes the channel, so the count releases inside the
//! arrival task exactly once (an established channel's release is //! arrival task exactly once (an established channel's release is
//! path 1's). These are the mirror of site 2 on the server-initiated //! path 1's). These are the mirror of site 2 on the server-initiated
//! open direction — the acceptor vocabulary's cap rows. //! open direction, the acceptor vocabulary's cap rows.
//! //!
//! ## Why the mutex (the single-writer reasoning, updated twice) //! ## Why the mutex (the single-writer reasoning, updated twice)
//! //!
//! The wave-2 shape was a bare `usize` field written only by the //! The wave-2 shape was a bare `usize` field written only by the
//! session's packet loop — sound because every hook runs inline on //! session's packet loop, sound because every hook runs inline on
//! that loop with exclusive `&mut self` access. The bridge release //! that loop with exclusive `&mut self` access. The bridge release
//! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`) //! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`)
//! both break the single-writer property: those tasks are alkcall //! both break the single-writer property: those tasks are alkcall
@@ -66,12 +66,12 @@
//! critical sections are single integer ops (no await inside the //! critical sections are single integer ops (no await inside the
//! lock), and all sites saturating. Channels never closed before //! lock), and all sites saturating. Channels never closed before
//! session teardown stay counted until the handler dies with the //! session teardown stay counted until the handler dies with the
//! session — per-session state, gone with the connection. //! session (per-session state, gone with the connection).
use parking_lot::Mutex; use parking_lot::Mutex;
/// The per-session active-channel count for the in-crate /// The per-session active-channel count for the in-crate
/// concurrent-channel cap — see the module docs for the shape, the /// concurrent-channel cap; see the module docs for the shape, the
/// release sites, and the writer/soundness reasoning. Shared with the /// release sites, and the writer/soundness reasoning. Shared with the
/// spawned route-1 bridge tasks ([`Arc<ChannelCap>`]) exactly so the /// spawned route-1 bridge tasks ([`Arc<ChannelCap>`]) exactly so the
/// fabric-failure release can fire from the task that knows it. /// fabric-failure release can fire from the task that knows it.
@@ -82,13 +82,13 @@ pub(crate) struct ChannelCap {
impl ChannelCap { impl ChannelCap {
/// The accept-path admission decision: room under `cap` increments /// The accept-path admission decision: room under `cap` increments
/// the count and admits; otherwise refuses (fail-closed — the /// the count and admits; otherwise refuses (fail-closed: the
/// caller answers the open with a clean decline). `cap == 0` /// caller answers the open with a clean decline). `cap == 0`
/// refuses every open. Call sites: the packet loop's open hooks /// refuses every open. Call sites: the packet loop's open hooks
/// `w2`-style, and — since `w3-tunnel-listen` — the `-R` arrival /// `w2`-style, and, since `w3-tunnel-listen`, the `-R` arrival
/// tasks (the door's forwarded-arrival admission: an arrival is /// tasks (the door's forwarded-arrival admission: an arrival is
/// admitted on the owning route's count inside the spawned /// admitted on the owning route's count inside the spawned
/// arrival pump — see the module doc's updated single-writer /// arrival pump; see the module doc's updated single-writer
/// reasoning). The mutex makes both sound. /// reasoning). The mutex makes both sound.
pub(crate) fn admit(&self, cap: usize) -> bool { pub(crate) fn admit(&self, cap: usize) -> bool {
let mut active = self.active.lock(); let mut active = self.active.lock();
@@ -110,7 +110,7 @@ impl ChannelCap {
*active = active.saturating_sub(1); *active = active.saturating_sub(1);
} }
/// The active count — in-crate inspection for the unit rows. /// The active count, in-crate inspection for the unit rows.
#[cfg(test)] #[cfg(test)]
pub(crate) fn active(&self) -> usize { pub(crate) fn active(&self) -> usize {
*self.active.lock() *self.active.lock()
@@ -136,7 +136,7 @@ mod tests {
} }
/// - *Contract stamp:* the task's cap mechanics (a close recovers /// - *Contract stamp:* the task's cap mechanics (a close recovers
/// the cap — the count decrements on close). /// the cap; the count decrements on close).
#[test] #[test]
fn release_recovers_the_cap() { fn release_recovers_the_cap() {
let cap = ChannelCap::default(); let cap = ChannelCap::default();
@@ -150,7 +150,7 @@ mod tests {
} }
/// - *Contract stamp:* the fail-closed cap posture (`cap == 0` is /// - *Contract stamp:* the fail-closed cap posture (`cap == 0` is
/// a valid, assembly-tunable refusal of every open — never a /// a valid, assembly-tunable refusal of every open, never a
/// bypass). /// bypass).
#[test] #[test]
fn zero_cap_refuses_every_open() { fn zero_cap_refuses_every_open() {
@@ -177,9 +177,9 @@ mod tests {
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded release /// - *Contract stamp:* `w3-tunnel-direct`'s recorded release
/// mechanism (the admitted-but-rejected bridge releases inside /// mechanism (the admitted-but-rejected bridge releases inside
/// the spawned task): the cap is shared safely — admission from /// the spawned task): the cap is shared safely (admission from
/// the packet-loop site, release from anywhere that holds the /// the packet-loop site, release from anywhere that holds the
/// `Arc` — and disjoint releases never double-count. /// `Arc`), and disjoint releases never double-count.
#[test] #[test]
fn the_cap_is_shareable_between_the_loop_and_bridge_tasks() { fn the_cap_is_shareable_between_the_loop_and_bridge_tasks() {
let cap = std::sync::Arc::new(ChannelCap::default()); let cap = std::sync::Arc::new(ChannelCap::default());
+38 -38
View File
@@ -1,4 +1,4 @@
//! The dial surface (`w4-dial`) — the door *outward* (`client.md` //! The dial surface (`w4-dial`), the door *outward* (`client.md`
//! §The dial surface), the client-direction mirror of //! §The dial surface), the client-direction mirror of
//! [`crate::adapter`]'s mount: the russh client stack every wave-4 op //! [`crate::adapter`]'s mount: the russh client stack every wave-4 op
//! row composes into. //! row composes into.
@@ -7,7 +7,7 @@
//! whole authenticated-dial sequence, **fail-closed at every step**: //! whole authenticated-dial sequence, **fail-closed at every step**:
//! //!
//! 1. **The decrypt-at-dial key load** (ADR-005 decision 2): the //! 1. **The decrypt-at-dial key load** (ADR-005 decision 2): the
//! injected [`KeySource::load`] runs *here and nowhere else* — //! injected [`KeySource::load`] runs *here and nowhere else*:
//! locked / absent / decryption / unknown-version / decode are all //! locked / absent / decryption / unknown-version / decode are all
//! **no-dial outcomes** ([`DialError::KeySource`], the variant //! **no-dial outcomes** ([`DialError::KeySource`], the variant
//! preserved). The decoded key lives for this dial; the vault is //! preserved). The decoded key lives for this dial; the vault is
@@ -15,18 +15,18 @@
//! established connections streaming, POC-2 §2). //! established connections streaming, POC-2 §2).
//! 2. **The bounded russh client `Config`** ([`russh_client_config`]) //! 2. **The bounded russh client `Config`** ([`russh_client_config`])
//! from the [`Limits`]: the keepalive 15 s / 3 posture //! from the [`Limits`]: the keepalive 15 s / 3 posture
//! (`reverse-runner.md` lesson 3 — OpenSSH //! (`reverse-runner.md` lesson 3, OpenSSH
//! `ServerAliveInterval=15`/`CountMax=3` parity), the wall clock //! `ServerAliveInterval=15`/`CountMax=3` parity), the wall clock
//! **always `Some`** (`inactivity_timeout` — the crate never //! **always `Some`** (`inactivity_timeout`, the crate never
//! constructs a session with no clock), and the window/packet/ //! constructs a session with no clock), and the window/packet/
//! channel-buffer knobs verbatim. Everything else stays russh's //! channel-buffer knobs verbatim. Everything else stays russh's
//! own default posture (the ADR-002 surface notes, mirrored from //! own default posture (the ADR-002 surface notes, mirrored from
//! [`crate::adapter`]'s server mapping: rekey `Limits` //! [`crate::adapter`]'s server mapping: rekey `Limits`
//! russh-enforced, preferred algorithms russh's — the ed25519-only //! russh-enforced, preferred algorithms russh's; the ed25519-only
//! posture holds by the loaded key set itself). No client-side //! posture holds by the loaded key set itself). No client-side
//! auth-attempt knob exists at this pin and none is needed: the //! auth-attempt knob exists at this pin and none is needed: the
//! family contract is exactly **one publickey attempt**. //! family contract is exactly **one publickey attempt**.
//! 3. **The policy call at `check_server_key`** (OQ-05) — the //! 3. **The policy call at `check_server_key`** (OQ-05), the
//! wave-1 [`HostKeyPolicy`](crate::HostKeyPolicy) seam consumed by //! wave-1 [`HostKeyPolicy`](crate::HostKeyPolicy) seam consumed by
//! the handler ([`crate::client_handler`]): the crate default //! the handler ([`crate::client_handler`]): the crate default
//! [`RejectAll`](crate::RejectAll) connects to nothing; a //! [`RejectAll`](crate::RejectAll) connects to nothing; a
@@ -34,23 +34,23 @@
//! Declines surface as [`DialError::HostKeyRejected`]. //! Declines surface as [`DialError::HostKeyRejected`].
//! 4. **Publickey-only authentication** (the family contract): //! 4. **Publickey-only authentication** (the family contract):
//! exactly one `authenticate_publickey` attempt from the loaded //! exactly one `authenticate_publickey` attempt from the loaded
//! key — no password, no keyboard-interactive, no cert, no retry, //! key (no password, no keyboard-interactive, no cert, no retry,
//! no fallback method (russh's handle methods for those are never //! no fallback method (russh's handle methods for those are never
//! exposed through [`SshConnection`]); a refusal is //! exposed through [`SshConnection`])); a refusal is
//! [`DialError::AuthRejected`] and the attempt is closed cleanly. //! [`DialError::AuthRejected`] and the attempt is closed cleanly.
//! //!
//! **The connection handle** ([`SshConnection`]) is the type //! **The connection handle** ([`SshConnection`]) is the type
//! everything else in the wave rides (the op rows open channels on //! everything else in the wave rides (the op rows open channels on
//! it; the `-R` leg installs the acceptor registry over it) — the //! it; the `-R` leg installs the acceptor registry over it), the
//! russh `client::Handle` wrapped with the crate's surface, the //! russh `client::Handle` wrapped with the crate's surface, the
//! policy, limits and endpoint labels carried. The connection handle's //! policy, limits and endpoint labels carried. The connection handle's
//! clean-close path is [`SshConnection::close`]; the listener handles' //! clean-close path is [`SshConnection::close`]; the listener handles'
//! surface is [`crate::client_accept`]'s. //! surface is [`crate::client_accept`]'s.
//! //!
//! ## Lifecycle posture (`w4-lifecycle` — the OQ-12 resolution in code) //! ## Lifecycle posture (`w4-lifecycle`, the OQ-12 resolution in code)
//! //!
//! The handle surface is **primitives, not policy** (reverse-runner //! The handle surface is **primitives, not policy** (reverse-runner
//! fact 4): reconnect-and-restore (rebinding persisted tunnel ports — //! fact 4): reconnect-and-restore (rebinding persisted tunnel ports,
//! the alkminer flow) is *assembly* composition over these //! the alkminer flow) is *assembly* composition over these
//! primitives; the crate provides bound ports, handles, cancellation, //! primitives; the crate provides bound ports, handles, cancellation,
//! clean close, and never owns a reconnect/restore policy. Two //! clean close, and never owns a reconnect/restore policy. Two
@@ -58,18 +58,18 @@
//! //!
//! - **Drop is never the clean path.** At this russh pin dropping the //! - **Drop is never the clean path.** At this russh pin dropping the
//! last wire-stack handle ends the runner and the transport goes //! last wire-stack handle ends the runner and the transport goes
//! down **abruptly** — the peer observes a broken session (its //! down **abruptly**: the peer observes a broken session (its
//! serving outcome is an error wire class), never the clean //! serving outcome is an error wire class), never the clean
//! disconnect message. So the surface never relies on drop timing //! disconnect message. So the surface never relies on drop timing
//! for clean state: only [`SshConnection::close`] sends //! for clean state: only [`SshConnection::close`] sends
//! `Disconnect::ByApplication` — the peer observes a clean //! `Disconnect::ByApplication`, the peer observes a clean
//! disconnect, no half-open channels (row-pinned through the //! disconnect, no half-open channels (row-pinned through the
//! server double, both directions of the boundary: abrupt-on-drop, //! server double, both directions of the boundary: abrupt-on-drop,
//! clean-on-close). //! clean-on-close).
//! And a cancellation is async — it cannot hide inside a sync //! And a cancellation is async; it cannot hide inside a sync
//! `Drop` hook, so no handle hides a `Drop` side effect: the //! `Drop` hook, so no handle hides a `Drop` side effect: the
//! listener handles' drop frees *nothing* on their wire state. //! listener handles' drop frees *nothing* on their wire state.
//! - **Every listener/handle is explicitly cancellable** — the `-R` //! - **Every listener/handle is explicitly cancellable**: the `-R`
//! listens return the [`ReverseListener`](crate::client_accept:: //! listens return the [`ReverseListener`](crate::client_accept::
//! ReverseListener) handles (the actually-bound endpoint carried; //! ReverseListener) handles (the actually-bound endpoint carried;
//! `cancel()` the wire-honest teardown), the connection handle //! `cancel()` the wire-honest teardown), the connection handle
@@ -84,7 +84,7 @@
//! [`russh_client_config`] (the bounded-knob mapping, asserted //! [`russh_client_config`] (the bounded-knob mapping, asserted
//! verbatim) and [`ClientHandler`](crate::client_handler::ClientHandler) //! verbatim) and [`ClientHandler`](crate::client_handler::ClientHandler)
//! (the handler skeleton; `client_handler.rs`). The wire vehicle is //! (the handler skeleton; `client_handler.rs`). The wire vehicle is
//! the in-process fake `sshd` in `tests/common/sshd_double.rs` — the //! the in-process fake `sshd` in `tests/common/sshd_double.rs`: the
//! client direction dials vanilla servers, so the loopback harness //! client direction dials vanilla servers, so the loopback harness
//! (the *server*-direction vehicle) only hosts the crate-to-crate //! (the *server*-direction vehicle) only hosts the crate-to-crate
//! smoke (see the task's smoke-posture record). //! smoke (see the task's smoke-posture record).
@@ -102,7 +102,7 @@ use crate::client_handler::ClientHandler;
use crate::config::{DialConfig, Limits}; use crate::config::{DialConfig, Limits};
use crate::error::DialError; use crate::error::DialError;
/// The authenticated dial — the client direction's entry point /// The authenticated dial, the client direction's entry point
/// (`client.md` §The dial surface): see the module docs for the /// (`client.md` §The dial surface): see the module docs for the
/// four-step sequence and their fail-closed outcomes. /// four-step sequence and their fail-closed outcomes.
pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> { pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> {
@@ -149,7 +149,7 @@ pub async fn dial(config: DialConfig) -> Result<SshConnection, DialError> {
}) })
} }
/// Parses the dial address into its endpoint labels — the /// Parses the dial address into its endpoint labels, the
/// [`PresentedHostKey`](crate::host_key::PresentedHostKey)'s /// [`PresentedHostKey`](crate::host_key::PresentedHostKey)'s
/// `host`/`port` decision context. Malformed targets refuse before /// `host`/`port` decision context. Malformed targets refuse before
/// any connection ([`DialError::Address`]): plain `host:port` and the /// any connection ([`DialError::Address`]): plain `host:port` and the
@@ -177,7 +177,7 @@ fn parse_dial_address(address: &str) -> Result<(String, u16), DialError> {
Ok((host.to_string(), port)) Ok((host.to_string(), port))
} }
/// The [`Limits`] → russh client `Config` mapping — the bounded knobs /// The [`Limits`] → russh client `Config` mapping: the bounded knobs
/// verbatim, the wall clock always on, everything else at russh's own /// verbatim, the wall clock always on, everything else at russh's own
/// default posture (see the module docs). The mapping is the dial /// default posture (see the module docs). The mapping is the dial
/// task's `bounded-config wiring observable`; /// task's `bounded-config wiring observable`;
@@ -199,15 +199,15 @@ pub fn russh_client_config(limits: &Limits) -> russh::client::Config {
build_client_config(limits) build_client_config(limits)
} }
/// The authenticated connection handle — the type every later /// The authenticated connection handle, the type every later
/// wave-4 op rides (`client.md`: "the authenticated connection /// wave-4 op rides (`client.md`: "the authenticated connection
/// handle"): the russh `client::Handle` wrapped with the crate's /// handle"): the russh `client::Handle` wrapped with the crate's
/// surface. The remote end is authenticated (publickey-only, single /// surface. The remote end is authenticated (publickey-only, single
/// attempt); the dial key is gone (its lifetime is the dial's — /// attempt); the dial key is gone (its lifetime is the dial's;
/// everything after rides the wire, never the key). /// everything after rides the wire, never the key).
/// ///
/// The russh handle, budgets and registry ride one shared inner /// The russh handle, budgets and registry ride one shared inner
/// (behind the `Arc`) — the sharing the `client_accept` module's /// (behind the `Arc`), the sharing the `client_accept` module's
/// listener handles need for their independent `cancel()` calls; the /// listener handles need for their independent `cancel()` calls; the
/// wire-visible teardowns stay explicit calls on top of that shared /// wire-visible teardowns stay explicit calls on top of that shared
/// stack (the module docs' §Lifecycle posture: drop is never a /// stack (the module docs' §Lifecycle posture: drop is never a
@@ -216,13 +216,13 @@ pub struct SshConnection {
shared: Arc<ConnectionShared>, shared: Arc<ConnectionShared>,
} }
/// The connection handle's internals — the one behind the /// The connection handle's internals, the one behind the
/// [`SshConnection`]'s `Arc`, every field private to the crate: the /// [`SshConnection`]'s `Arc`, every field private to the crate: the
/// wire surface is the wrapped methods, never the russh stack itself /// wire surface is the wrapped methods, never the russh stack itself
/// (`serve.rs`'s wrapped-surface pattern mirrored client-side). /// (`serve.rs`'s wrapped-surface pattern mirrored client-side).
/// ///
/// The arc's meaning for the lifecycle boundary: the russh stack /// The arc's meaning for the lifecycle boundary: the russh stack
/// (the runner) lives while *any* handle holds the arc — the /// (the runner) lives while *any* handle holds the arc; the
/// connection handle's drop is the abrupt teardown only when it is /// connection handle's drop is the abrupt teardown only when it is
/// the last holder (row-pinned), and a live `ReverseListener` (its /// the last holder (row-pinned), and a live `ReverseListener` (its
/// `cancel()` composes on this stack) keeps the wire serving until /// `cancel()` composes on this stack) keeps the wire serving until
@@ -231,18 +231,18 @@ pub struct SshConnection {
pub(crate) struct ConnectionShared { pub(crate) struct ConnectionShared {
/// The russh handle the connection and the listener handles /// The russh handle the connection and the listener handles
/// compose into (its own request methods take `&self` at this /// compose into (its own request methods take `&self` at this
/// pin — the shared arc is the concurrent-sharing shape). /// pin, the shared arc is the concurrent-sharing shape).
pub(crate) handle: russh::client::Handle<ClientHandler>, pub(crate) handle: russh::client::Handle<ClientHandler>,
pub(crate) limits: Limits, pub(crate) limits: Limits,
pub(crate) address: String, pub(crate) address: String,
pub(crate) username: String, pub(crate) username: String,
/// The `-R` leg's acceptor registry (the dial constructs it empty; /// The `-R` leg's acceptor registry (the dial constructs it empty;
/// the assembly installs/uninstalls across the live connection — /// the assembly installs/uninstalls across the live connection;
/// see [`SshConnection::acceptors`] and /// see [`SshConnection::acceptors`] and
/// [`crate::client_accept`]`'s recorded decisions. /// [`crate::client_accept`]`'s recorded decisions.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub(crate) acceptors: Arc<AcceptorRegistry>, pub(crate) acceptors: Arc<AcceptorRegistry>,
/// The reverse-listen count's dedicated bound — admitted at the /// The reverse-listen count's dedicated bound, admitted at the
/// listen rows, released at the confirmed cancels and the refused/ /// listen rows, released at the confirmed cancels and the refused/
/// wire-error paths (the cap relation recorded in /// wire-error paths (the cap relation recorded in
/// [`crate::client_accept`]; [`crate::channel_cap`]'s mutex makes /// [`crate::client_accept`]; [`crate::channel_cap`]'s mutex makes
@@ -271,7 +271,7 @@ impl SshConnection {
/// The `-R` leg's acceptor registry (the /// The `-R` leg's acceptor registry (the
/// `w4-reverse-listen` surface, `client_accept`'s module docs /// `w4-reverse-listen` surface, `client_accept`'s module docs
/// carry the recorded decisions) — install/uninstall the /// carry the recorded decisions); install/uninstall the
/// per-type acceptors across the live connection (the /// per-type acceptors across the live connection (the
/// late-install dynamic). The dial constructs it empty: nothing /// late-install dynamic). The dial constructs it empty: nothing
/// server-initiated is served until a slot holds, fail-closed at /// server-initiated is served until a slot holds, fail-closed at
@@ -281,14 +281,14 @@ impl SshConnection {
&self.shared.acceptors &self.shared.acceptors
} }
/// The reverse-listen count's shared handle — the op rows /// The reverse-listen count's shared handle, which the op rows
/// ([`crate::client_accept`]) read it; private to the crate. /// ([`crate::client_accept`]) read; private to the crate.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub(crate) fn listener_budget(&self) -> &ChannelCap { pub(crate) fn listener_budget(&self) -> &ChannelCap {
&self.shared.listener_budget &self.shared.listener_budget
} }
/// The shared inner — the listener handles' compose-into point /// The shared inner, the listener handles' compose-into point
/// (their cancels run against the same russh stack); private to /// (their cancels run against the same russh stack); private to
/// the crate. /// the crate.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -296,7 +296,7 @@ impl SshConnection {
&self.shared &self.shared
} }
/// The russh handle the op rows compose into (`w4-op-rows` — the /// The russh handle the op rows compose into (`w4-op-rows`; the
/// method bodies live in [`crate::client_ops`], a sibling module, /// method bodies live in [`crate::client_ops`], a sibling module,
/// so the field stays private to this module). /// so the field stays private to this module).
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))] #[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
@@ -306,7 +306,7 @@ impl SshConnection {
&self.shared.handle &self.shared.handle
} }
/// The dial's bounded-resource budget, verbatim — the op rows /// The dial's bounded-resource budget, verbatim; the op rows
/// read the knobs they consume (the exec-command length cap /// read the knobs they consume (the exec-command length cap
/// drives the command-carrying rows' client-mirror gate). /// drives the command-carrying rows' client-mirror gate).
#[cfg(any(feature = "session", feature = "git"))] #[cfg(any(feature = "session", feature = "git"))]
@@ -320,12 +320,12 @@ impl SshConnection {
} }
/// The explicit clean-close path (the lifecycle primitive, /// The explicit clean-close path (the lifecycle primitive,
/// [`russh::Disconnect::ByApplication`] — the honest close): the /// [`russh::Disconnect::ByApplication`], the honest close): the
/// peer observes a clean disconnect, no half-open channels — /// peer observes a clean disconnect, no half-open channels;
/// every open channel is torn down with the session (row-pinned /// every open channel is torn down with the session (row-pinned
/// through the server double). **Drop is not this path** (the /// through the server double). **Drop is not this path** (the
/// module docs' §Lifecycle posture): a dropped handle releases /// module docs' §Lifecycle posture): a dropped handle releases
/// nothing on the wire — the connection outlives the handle /// nothing on the wire; the connection outlives the handle
/// until the peer or the wall clock ends it. /// until the peer or the wall clock ends it.
pub async fn close(self) { pub async fn close(self) {
let _ = self let _ = self
@@ -373,7 +373,7 @@ mod tests {
} }
/// - *Contract stamp:* `client.md` §Correctness facts (the /// - *Contract stamp:* `client.md` §Correctness facts (the
/// keepalive posture) + `overview.md` §Bounded-resources table — /// keepalive posture) + `overview.md` §Bounded-resources table;
/// the defaults land in the russh mapping verbatim. /// the defaults land in the russh mapping verbatim.
#[test] #[test]
fn bounded_config_mapping_carries_the_default_postures() { fn bounded_config_mapping_carries_the_default_postures() {
@@ -385,7 +385,7 @@ mod tests {
assert_eq!( assert_eq!(
config.inactivity_timeout, config.inactivity_timeout,
Some(crate::config::DEFAULT_INACTIVITY_TIMEOUT), Some(crate::config::DEFAULT_INACTIVITY_TIMEOUT),
"the wall clock is always set — no unbounded session" "the wall clock is always set, never an unbounded session"
); );
assert_eq!( assert_eq!(
config.keepalive_interval, config.keepalive_interval,
+85 -85
View File
@@ -1,28 +1,28 @@
//! The `-R` leg's client half (`w4-reverse-listen` — `client.md` //! The `-R` leg's client half (`w4-reverse-listen`, `client.md`
//! §op rows row 4 + §The acceptor): the hub's reverse direction in //! §op rows row 4 + §The acceptor): the hub's reverse direction in
//! family types. Two surfaces, the acceptor registry first: //! family types. Two surfaces, the acceptor registry first:
//! //!
//! 1. **The forward request rows** — [`SshConnection::reverse_listen`] //! 1. **The forward request rows**: [`SshConnection::reverse_listen`]
//! / [`SshConnection::reverse_listen_cancel`] / the unix pair //! / [`SshConnection::reverse_listen_cancel`] / the unix pair
//! (`reverse_listen_unix` / `reverse_listen_unix_cancel`): the //! (`reverse_listen_unix` / `reverse_listen_unix_cancel`): the
//! vanilla-server listen requests, the lifecycle primitives //! vanilla-server listen requests, the lifecycle primitives
//! (`client.md` §Lifecycle). The listens return the //! (`client.md` §Lifecycle). The listens return the
//! **[`ReverseListener`] handles** (`w4-lifecycle` — the handles' //! **[`ReverseListener`] handles** (`w4-lifecycle`, the handles'
//! surface: the actually-bound endpoint carried, the explicit //! surface: the actually-bound endpoint carried, the explicit
//! `cancel()`); the stateless wire cancel forms ride beside them //! `cancel()`); the stateless wire cancel forms ride beside them
//! (sshd's global-request cancels mirrored 1:1). //! (sshd's global-request cancels mirrored 1:1).
//! 2. **The acceptor registry** ([`AcceptorRegistry`]) — the //! 2. **The acceptor registry** ([`AcceptorRegistry`]): the
//! assembly-mutable, per-channel-type registry the dial surface's //! assembly-mutable, per-channel-type registry the dial surface's
//! russh handler routes **server-initiated `forwarded-*` opens** //! russh handler routes **server-initiated `forwarded-*` opens**
//! through. Served only under a registered acceptor for that type //! through. Served only under a registered acceptor for that type
//! and under the `tunnel` feature; unregistered/unfeatured accepted //! and under the `tunnel` feature; unregistered/unfeatured accepted
//! opens **decline cleanly** — never russh's accept-and-drop //! opens **decline cleanly**, never russh's accept-and-drop
//! default (ADR-003 §4, the client-side fail-closed acceptor; the //! default (ADR-003 §4, the client-side fail-closed acceptor; the
//! reverse-runner fact 1 disposition is ours). //! reverse-runner fact 1 disposition is ours).
//! //!
//! ## The recorded decisions (the task's decide + record items) //! ## The recorded decisions (the task's decide + record items)
//! //!
//! - **Registry shape: per-type fixed slots, not a typed map** — the //! - **Registry shape: per-type fixed slots, not a typed map**: the
//! client direction's registry-servable type set is closed and tiny //! client direction's registry-servable type set is closed and tiny
//! (exactly the `-R` data leg's two: `forwarded-tcpip` / //! (exactly the `-R` data leg's two: `forwarded-tcpip` /
//! `forwarded-streamlocal`, both under `tunnel`); one trait per type //! `forwarded-streamlocal`, both under `tunnel`); one trait per type
@@ -35,70 +35,70 @@
//! `ForwardedAcceptor` vocabulary is grown, not forked: the payload //! `ForwardedAcceptor` vocabulary is grown, not forked: the payload
//! fields are the same shapes (the open's originator fields + the //! fields are the same shapes (the open's originator fields + the
//! channel duplex), the duplex now family-typed //! channel duplex), the duplex now family-typed
//! ([`TunnelChannel`] — the `-L` rows' byte //! ([`TunnelChannel`], the `-L` rows' byte
//! duplex, one type both legs ride). //! duplex, one type both legs ride).
//! - **Install is refuse-never-replace** ([`AcceptorRegistry:: //! - **Install is refuse-never-replace** ([`AcceptorRegistry::
//! install_tcpip`]'s held-slot error): the same posture recorded //! install_tcpip`]'s held-slot error): the same posture recorded
//! server-side on the `ListenRouter` at `w4-router-collision` — //! server-side on the `ListenRouter` at `w4-router-collision`:
//! a same-slot second install is an assembly program error, declined //! a same-slot second install is an assembly program error, declined
//! never a quiet displacement of a live acceptor. Uninstall is //! never a quiet displacement of a live acceptor. Uninstall is
//! explicit, 1:1 with the install. //! explicit, 1:1 with the install.
//! - **Late-install across a live connection** (the alkminer //! - **Late-install across a live connection** (the alkminer
//! `install_forward_sink` dynamic — reverse-runner fact 2): the //! `install_forward_sink` dynamic, reverse-runner fact 2): the
//! registry rides the connected [`SshConnection`] //! registry rides the connected [`SshConnection`]
//! ([`SshConnection::acceptors`]) — the dial constructs it empty, //! ([`SshConnection::acceptors`]): the dial constructs it empty,
//! the assembly installs/uninstalls whenever it decides. The open //! the assembly installs/uninstalls whenever it decides. The open
//! hooks read the slot **at arrival time**, fail-closed: an open //! hooks read the slot **at arrival time**, fail-closed: an open
//! arriving before its acceptor installs (or after an uninstall) //! arriving before its acceptor installs (or after an uninstall)
//! is **declined at arrival, never buffered** — no unclaimed-open //! is **declined at arrival, never buffered**; no unclaimed-open
//! queue exists anywhere in this module's posture (an unclaimed //! queue exists anywhere in this module's posture (an unclaimed
//! open's bytes would have to be buffered unboundedly to accept- //! open's bytes would have to be buffered unboundedly to accept-
//! then-route; the decline is the clean, bounded answer). //! then-route; the decline is the clean, bounded answer).
//! - **The listen rows gate on the registered acceptor** — the //! - **The listen rows gate on the registered acceptor**, the
//! *issuing*-side disposition: a listen whose data leg cannot serve //! *issuing*-side disposition: a listen whose data leg cannot serve
//! is a half-capability (the peer stays bound, every arrival //! is a half-capability (the peer stays bound, every arrival
//! declines), so a `reverse_listen` with no registered acceptor for //! declines), so a `reverse_listen` with no registered acceptor for
//! the type answers //! the type answers
//! [`OpError::NoAcceptor`](crate::OpError) **before any wire op** //! [`OpError::NoAcceptor`](crate::OpError) **before any wire op**
//! (install-first is the recorded order; the rows never buffer an //! (install-first is the recorded order; the rows never buffer an
//! outstanding listen against a later install — re-issuing is the //! outstanding listen against a later install; re-issuing is the
//! assembly's retry, the listen rows are stateless global requests). //! assembly's retry, the listen rows are stateless global requests).
//! - **Bounded bookkeeping (AGENTS.md convention 9) — the cap //! - **Bounded bookkeeping (AGENTS.md convention 9): the cap
//! relation** (the server-side listener-counter posture as the //! relation** (the server-side listener-counter posture as the
//! template): the registry is bounded by construction (two fixed //! template): the registry is bounded by construction (two fixed
//! slots). The per-connection **listener count** is a dedicated //! slots). The per-connection **listener count** is a dedicated
//! in-crate counter admitting at the listen rows and releasing at //! in-crate counter admitting at the listen rows and releasing at
//! the confirmed cancels and the failed/wire-error paths — bounded //! the confirmed cancels and the failed/wire-error paths, bounded
//! against the *same* `Limits::max_concurrent_channels` ceiling the //! against the *same* `Limits::max_concurrent_channels` ceiling the
//! server direction's listener counter uses (independent counts, one //! server direction's listener counter uses (independent counts, one
//! ceiling — never a map: the outstanding listens live at the peer; //! ceiling, never a map: the outstanding listens live at the peer;
//! the counter exists so a dial cannot mint an unbounded listener //! the counter exists so a dial cannot mint an unbounded listener
//! set the registry would have to serve). The per-connection //! set the registry would have to serve). The per-connection
//! **accepted-open handoff count** is the second dedicated counter //! **accepted-open handoff count** is the second dedicated counter
//! against the same ceiling (admitted in the dial handler's open //! against the same ceiling (admitted in the dial handler's open
//! hook, accepted-with-the-count-held, released when the handoff //! hook, accepted-with-the-count-held, released when the handoff
//! task ends — the acceptor's `accept` completing *or the task //! task ends: the acceptor's `accept` completing *or the task
//! unwinding* ([`HandoffCapGuard`]: a panicking assembly acceptor //! unwinding* ([`HandoffCapGuard`]: a panicking assembly acceptor
//! can never strand a held slot) — no accepted-open ledger anywhere //! can never strand a held slot); no accepted-open ledger anywhere
//! else: the pump is the acceptor's from the handoff on, the //! else: the pump is the acceptor's from the handoff on, the
//! channel's transport bounds are russh's windows, and the slot read //! channel's transport bounds are russh's windows, and the slot read
//! is the arrival admission). //! is the arrival admission).
//! - **The listener handles (`w4-lifecycle` — the handles' //! - **The listener handles (`w4-lifecycle`, the handles'
//! surface)**: the listen rows return **[`ReverseListener`]** //! surface)**: the listen rows return **[`ReverseListener`]**
//! handles — the endpoint (the actually-bound values, the cancel //! handles: the endpoint (the actually-bound values, the cancel
//! match keys) carried, the explicit `cancel()` the wire-honest //! match keys) carried, the explicit `cancel()` the wire-honest
//! teardown. Decisions recorded: //! teardown. Decisions recorded:
//! (a) **no `Drop` hides a wire op** — a cancellation is async and //! (a) **no `Drop` hides a wire op**: a cancellation is async and
//! cannot run in a sync `Drop`, so no `Drop` hook exists: the //! cannot run in a sync `Drop`, so no `Drop` hook exists: the
//! listen **outlives a dropped handle until an explicit cancel or //! listen **outlives a dropped handle until an explicit cancel or
//! the connection's end** (no drop-timing-only wire state — //! the connection's end** (no drop-timing-only wire state,
//! row-pinned); //! row-pinned);
//! (b) the handle **shares the connection's inner stack** (the //! (b) the handle **shares the connection's inner stack** (the
//! `SshConnection`'s `Arc`-held internals — the russh request //! `SshConnection`'s `Arc`-held internals; the russh request
//! methods take `&self` at this pin), so the handle's cancel is the //! methods take `&self` at this pin), so the handle's cancel is the
//! same wire op the stateless forms issue; //! same wire op the stateless forms issue;
//! (c) the **stateless cancel forms ride beside** the handles //! (c) the **stateless cancel forms ride beside** the handles
//! (sshd's global-request cancels are stateless — a cancel by the //! (sshd's global-request cancels are stateless: a cancel by the
//! raw strings works from anywhere); every form releases the //! raw strings works from anywhere); every form releases the
//! listener count exactly once on a confirmed cancel (the wire //! listener count exactly once on a confirmed cancel (the wire
//! state machine governs: the second cancel is the unknown-listener //! state machine governs: the second cancel is the unknown-listener
@@ -107,37 +107,37 @@
//! *listen* rows (`reverse_listen_unix` / `reverse_listen_unix_cancel`) //! *listen* rows (`reverse_listen_unix` / `reverse_listen_unix_cancel`)
//! compile everywhere and //! compile everywhere and
//! decline off-Unix before any wire op (the bind is the //! decline off-Unix before any wire op (the bind is the
//! platform-bound operation — the door puts nothing on the wire it //! platform-bound operation; the door puts nothing on the wire it
//! cannot track). The `forwarded-streamlocal` *open* hook is //! cannot track). The `forwarded-streamlocal` *open* hook is
//! universal: ingestion is platform-neutral byte pumping — the //! universal: ingestion is platform-neutral byte pumping; the
//! unix-socket leg's socket is the peer-side's. //! unix-socket leg's socket is the peer-side's.
//! - **`OpError::ListenRefused`** — the peer's `RequestDenied` on //! - **`OpError::ListenRefused`**: the peer's `RequestDenied` on
//! every form's decline (a denied listen, a cancel of an unknown //! every form's decline (a denied listen, a cancel of an unknown
//! listener) — channel-scoped honesty: the connection stays usable //! listener), channel-scoped honesty: the connection stays usable
//! (the `From` mapping's recorded producer, `error.rs`). //! (the `From` mapping's recorded producer, `error.rs`).
//! //!
//! ## The data leg //! ## The data leg
//! //!
//! Per accepted open: the hook accepts the channel and hands the //! Per accepted open: the hook accepts the channel and hands the
//! assembly the open — the [`TunnelChannel`] //! assembly the open, the [`TunnelChannel`]
//! duplex + the open's echo/originator fields (the payload types //! duplex + the open's echo/originator fields (the payload types
//! below) — on a spawned task (the spawn-and-return rule's client //! below), on a spawned task (the spawn-and-return rule's client
//! mirror: the hook runs in the packet loop and returns fast). The //! mirror: the hook runs in the packet loop and returns fast). The
//! pump is the acceptor's from the handoff — the family handler shape //! pump is the acceptor's from the handoff, the family handler shape
//! (alktunnels' `ForwardedHandler` recipe, POC-3 §C.4); dropping the //! (alktunnels' `ForwardedHandler` recipe, POC-3 §C.4); dropping the
//! duplex is wire-clean (russh closes the channel). //! duplex is wire-clean (russh closes the channel).
//! //!
//! ## Test surface //! ## Test surface
//! //!
//! The wire vehicle is the fake `sshd` double grown with the //! The wire vehicle is the fake `sshd` double grown with the
//! server-initiated-forward surfaces (`tests/common/sshd_double.rs`) — //! server-initiated-forward surfaces (`tests/common/sshd_double.rs`);
//! the double binds **real** sockets for its script accepts, opens the //! the double binds **real** sockets for its script accepts, opens the
//! forwarded channels toward the client per accepted connection, and //! forwarded channels toward the client per accepted connection, and
//! pumps them (a real `ssh -R`-shaped round trip in-process, the task's //! pumps them (a real `ssh -R`-shaped round trip in-process, the task's
//! recorded smoke posture). The full-circle alkssh-client ↔ //! recorded smoke posture). The full-circle alkssh-client ↔
//! alkssh-server-direction smoke (both `-R` legs through the alkcall //! alkssh-server-direction smoke (both `-R` legs through the alkcall
//! fabric, the composition the assembly makes) rides the fabric //! fabric, the composition the assembly makes) rides the fabric
//! doubles in `tests/tunnel_mapping.rs` — the same doubles the //! doubles in `tests/tunnel_mapping.rs`, the same doubles the
//! server-side `-R` rows use; real-`sshd` `-R` is wave-5 e2e //! server-side `-R` rows use; real-`sshd` `-R` is wave-5 e2e
//! territory, never silently dropped. //! territory, never silently dropped.
//! //!
@@ -163,24 +163,24 @@ use crate::client::SshConnection;
use crate::error::OpError; use crate::error::OpError;
use crate::observe; use crate::observe;
/// An accepted server-initiated `forwarded-tcpip` open — the acceptor /// An accepted server-initiated `forwarded-tcpip` open: the acceptor
/// registry's payload for the /// registry's payload for the
/// [`TcpipForwardedAcceptor::accept`] call: the open's echo fields /// [`TcpipForwardedAcceptor::accept`] call: the open's echo fields
/// (the listen endpoint the arrival belongs to — the assembly's /// (the listen endpoint the arrival belongs to, the assembly's
/// routing key; the OpenSSH matcher parity verbatim) + the originator /// routing key; the OpenSSH matcher parity verbatim) + the originator
/// (the arrival's peer, as the wire carried it) + the channel duplex. /// (the arrival's peer, as the wire carried it) + the channel duplex.
pub struct ForwardedTcpip { pub struct ForwardedTcpip {
/// The connected address the open echoed — the `reverse_listen` /// The connected address the open echoed, the `reverse_listen`
/// wire string (sshd parity). /// wire string (sshd parity).
pub connected_address: String, pub connected_address: String,
/// The actually-bound port of the minted listener — for a /// The actually-bound port of the minted listener; for a
/// `port = 0` mint this is the server-picked number, never `0`. /// `port = 0` mint this is the server-picked number, never `0`.
pub connected_port: u32, pub connected_port: u32,
/// The arrival's peer address (the wire originator). /// The arrival's peer address (the wire originator).
pub originator_address: String, pub originator_address: String,
/// The arrival's peer port (the wire originator). /// The arrival's peer port (the wire originator).
pub originator_port: u32, pub originator_port: u32,
/// The accepted channel's byte duplex — the acceptor owns the /// The accepted channel's byte duplex; the acceptor owns the
/// pump from the handoff on (dropping it is wire-clean). /// pump from the handoff on (dropping it is wire-clean).
pub stream: TunnelChannel, pub stream: TunnelChannel,
} }
@@ -196,14 +196,14 @@ impl std::fmt::Debug for ForwardedTcpip {
} }
} }
/// An accepted server-initiated `forwarded-streamlocal` open — the /// An accepted server-initiated `forwarded-streamlocal` open: the
/// streamlocal acceptor's payload (the echoed socket path + the /// streamlocal acceptor's payload (the echoed socket path + the
/// channel duplex). /// channel duplex).
pub struct ForwardedStreamlocal { pub struct ForwardedStreamlocal {
/// The socket path the open echoed — the `reverse_listen_unix` /// The socket path the open echoed, the `reverse_listen_unix`
/// wire string (sshd parity). /// wire string (sshd parity).
pub socket_path: String, pub socket_path: String,
/// The accepted channel's byte duplex — the acceptor owns the /// The accepted channel's byte duplex; the acceptor owns the
/// pump from the handoff on (dropping it is wire-clean). /// pump from the handoff on (dropping it is wire-clean).
pub stream: TunnelChannel, pub stream: TunnelChannel,
} }
@@ -216,34 +216,34 @@ impl std::fmt::Debug for ForwardedStreamlocal {
} }
} }
/// The `forwarded-tcpip` acceptor — the registry's per-type slot /// The `forwarded-tcpip` acceptor: the registry's per-type slot
/// trait. `accept` runs on the spawned handoff task (never the /// trait. `accept` runs on the spawned handoff task (never the
/// packet loop): the assembly's pump/home of the open lives inside /// packet loop): the assembly's pump/home of the open lives inside
/// it; the crate releases its handoff counter when the handoff task /// it; the crate releases its handoff counter when the handoff task
/// ends — the call returning *or the task unwinding* /// ends (the call returning *or the task unwinding*
/// (`HandoffCapGuard`) — so the family shape (hand off to a queue, /// `HandoffCapGuard`), so the family shape (hand off to a queue,
/// return) keeps the counter's meaning "open accepted, handoff in /// return) keeps the counter's meaning "open accepted, handoff in
/// flight". /// flight".
#[async_trait] #[async_trait]
pub trait TcpipForwardedAcceptor: Send + Sync { pub trait TcpipForwardedAcceptor: Send + Sync {
/// Serves one accepted open. Never invoked for an unregistered /// Serves one accepted open. Never invoked for an unregistered
/// type (the registry gate is first); the open is already accepted /// type (the registry gate is first); the open is already accepted
/// on the wire — refusing one from here is a *data-plane* choice /// on the wire; refusing one from here is a *data-plane* choice
/// (drop the duplex; the channel closes on it), never a wire /// (drop the duplex; the channel closes on it), never a wire
/// open-decline. /// open-decline.
async fn accept(&self, open: ForwardedTcpip); async fn accept(&self, open: ForwardedTcpip);
} }
/// The `forwarded-streamlocal` acceptor — the unix slot's trait /// The `forwarded-streamlocal` acceptor: the unix slot's trait
/// (see [`TcpipForwardedAcceptor`] for the call-shape contract). /// (see [`TcpipForwardedAcceptor`] for the call-shape contract).
#[async_trait] #[async_trait]
pub trait StreamlocalForwardedAcceptor: Send + Sync { pub trait StreamlocalForwardedAcceptor: Send + Sync {
/// Serves one accepted open (platform-neutral pumping — see the /// Serves one accepted open (platform-neutral pumping; see the
/// module docs' unix-gate record). /// module docs' unix-gate record).
async fn accept(&self, open: ForwardedStreamlocal); async fn accept(&self, open: ForwardedStreamlocal);
} }
/// The held-slot refusal (`AcceptorRegistry::install_*`) — the /// The held-slot refusal (`AcceptorRegistry::install_*`): the
/// refuse-never-replace posture (the server-side `ListenRouter`'s /// refuse-never-replace posture (the server-side `ListenRouter`'s
/// recorded sibling): the slot already holds a live acceptor; a second /// recorded sibling): the slot already holds a live acceptor; a second
/// install is the assembly's program error, declined, never a quiet /// install is the assembly's program error, declined, never a quiet
@@ -252,7 +252,7 @@ pub trait StreamlocalForwardedAcceptor: Send + Sync {
#[error("an acceptor is already registered for this channel type")] #[error("an acceptor is already registered for this channel type")]
pub struct AcceptorHeld; pub struct AcceptorHeld;
/// The assembly-mutable, per-channel-type acceptor registry — the /// The assembly-mutable, per-channel-type acceptor registry: the
/// `-R` data leg's fail-closed gate (see the module docs for the /// `-R` data leg's fail-closed gate (see the module docs for the
/// recorded decisions). Constructed empty at the dial /// recorded decisions). Constructed empty at the dial
/// ([`SshConnection::acceptors`], the live-connection accessor); /// ([`SshConnection::acceptors`], the live-connection accessor);
@@ -263,7 +263,7 @@ pub struct AcceptorRegistry {
} }
impl AcceptorRegistry { impl AcceptorRegistry {
/// The empty registry — the dial's construct (nothing is served /// The empty registry, the dial's construct (nothing is served
/// until an acceptor installs; ADR-003 §4's fail-closed default). /// until an acceptor installs; ADR-003 §4's fail-closed default).
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -273,7 +273,7 @@ impl AcceptorRegistry {
} }
/// Installs the `forwarded-tcpip` acceptor. Refuse-never-replace: /// Installs the `forwarded-tcpip` acceptor. Refuse-never-replace:
/// a held slot declines ([`AcceptorHeld`]) — uninstall first if a /// a held slot declines ([`AcceptorHeld`]); uninstall first if a
/// replacement is meant (the late-install toggle's other half). /// replacement is meant (the late-install toggle's other half).
pub fn install_tcpip( pub fn install_tcpip(
&self, &self,
@@ -287,7 +287,7 @@ impl AcceptorRegistry {
Ok(()) Ok(())
} }
/// Uninstalls the `forwarded-tcpip` acceptor — `true` when one was /// Uninstalls the `forwarded-tcpip` acceptor: `true` when one was
/// held (the toggle's honest report). In-flight opens already /// held (the toggle's honest report). In-flight opens already
/// accepted keep pumping (their handoff happened); arrivals after /// accepted keep pumping (their handoff happened); arrivals after
/// this decline at arrival time (fail-closed, no buffering). /// this decline at arrival time (fail-closed, no buffering).
@@ -315,7 +315,7 @@ impl AcceptorRegistry {
self.streamlocal.lock().take().is_some() self.streamlocal.lock().take().is_some()
} }
/// The arrival-time slot reads (the open hooks' gates) — cloned /// The arrival-time slot reads (the open hooks' gates), cloned
/// out so the spawned handoff task owns its acceptor handle. /// out so the spawned handoff task owns its acceptor handle.
pub(crate) fn tcpip_acceptor(&self) -> Option<Arc<dyn TcpipForwardedAcceptor>> { pub(crate) fn tcpip_acceptor(&self) -> Option<Arc<dyn TcpipForwardedAcceptor>> {
self.tcpip.lock().clone() self.tcpip.lock().clone()
@@ -345,7 +345,7 @@ impl std::fmt::Debug for AcceptorRegistry {
// The server-initiated opens' handoff (the dial handler's routed paths) // The server-initiated opens' handoff (the dial handler's routed paths)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// The handoff counter's panic-safe release — the accepted open's count /// The handoff counter's panic-safe release: the accepted open's count
/// releases exactly at the handoff task's end, on *completion or /// releases exactly at the handoff task's end, on *completion or
/// unwind*: a panicking assembly acceptor (or any panic in the handoff /// unwind*: a panicking assembly acceptor (or any panic in the handoff
/// body) can never strand a held slot past the task. Saturating under /// body) can never strand a held slot past the task. Saturating under
@@ -358,14 +358,14 @@ impl Drop for HandoffCapGuard {
} }
} }
/// The accepted `forwarded-tcpip` open's handoff task — the /// The accepted `forwarded-tcpip` open's handoff task: the
/// spawn-and-return rule's client mirror (ADR-006 decision 4; the /// spawn-and-return rule's client mirror (ADR-006 decision 4; the
/// hook runs in the packet loop and returns fast): accepts the open on /// hook runs in the packet loop and returns fast): accepts the open on
/// the wire, converts the channel to the family duplex, delivers the /// the wire, converts the channel to the family duplex, delivers the
/// payload to the assembly's acceptor, and releases the handoff /// payload to the assembly's acceptor, and releases the handoff
/// counter when the handoff task ends — the acceptor's `accept` /// counter when the handoff task ends: the acceptor's `accept`
/// completing *or the task unwinding* ([`HandoffCapGuard`]; the /// completing *or the task unwinding* ([`HandoffCapGuard`]; the
/// recorded release site — the count's meaning is "accepted, handoff /// recorded release site; the count's meaning is "accepted, handoff
/// in flight"). /// in flight").
#[cfg_attr(feature = "tunnel", allow(clippy::too_many_arguments))] #[cfg_attr(feature = "tunnel", allow(clippy::too_many_arguments))]
pub(crate) async fn handoff_forwarded_tcpip( pub(crate) async fn handoff_forwarded_tcpip(
@@ -396,7 +396,7 @@ pub(crate) async fn handoff_forwarded_tcpip(
observe::serving_path_ended("forwarded-tcpip", channel_number, None); observe::serving_path_ended("forwarded-tcpip", channel_number, None);
} }
/// The accepted `forwarded-streamlocal` open's handoff task — the /// The accepted `forwarded-streamlocal` open's handoff task: the
/// unix slot's mirror of [`handoff_forwarded_tcpip`]. /// unix slot's mirror of [`handoff_forwarded_tcpip`].
pub(crate) async fn handoff_forwarded_streamlocal( pub(crate) async fn handoff_forwarded_streamlocal(
acceptor: Arc<dyn StreamlocalForwardedAcceptor>, acceptor: Arc<dyn StreamlocalForwardedAcceptor>,
@@ -421,10 +421,10 @@ pub(crate) async fn handoff_forwarded_streamlocal(
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The listener handles (w4-lifecycle — the handles' surface) // The listener handles (w4-lifecycle, the handles' surface)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// The endpoint a reverse listener is bound to — what a /// The endpoint a reverse listener is bound to: what a
/// [`ReverseListener`] carries for its assembly-side use (the /// [`ReverseListener`] carries for its assembly-side use (the
/// alkminer restore flow: the *actually bound* values to persist and /// alkminer restore flow: the *actually bound* values to persist and
/// rebind) and the cancel forms' raw wire match key (sshd parity: a /// rebind) and the cancel forms' raw wire match key (sshd parity: a
@@ -439,8 +439,8 @@ pub enum ReverseEndpoint {
} }
impl ReverseEndpoint { impl ReverseEndpoint {
/// The actually-bound port — the tcp shape's; `None` for the unix /// The actually-bound port, the tcp shape's; `None` for the unix
/// shape (no port semantics — the cancel matches by the path). /// shape (no port semantics; the cancel matches by the path).
pub fn bound_port(&self) -> Option<u32> { pub fn bound_port(&self) -> Option<u32> {
match self { match self {
Self::Tcp { port, .. } => Some(*port), Self::Tcp { port, .. } => Some(*port),
@@ -449,14 +449,14 @@ impl ReverseEndpoint {
} }
} }
/// A returned forward listener — the `-R` leg's **listener handle** /// A returned forward listener: the `-R` leg's **listener handle**
/// (the lifecycle primitive: every listener explicitly cancellable): /// (the lifecycle primitive: every listener explicitly cancellable):
/// the bound endpoint carried (see [`ReverseEndpoint`]; the /// the bound endpoint carried (see [`ReverseEndpoint`]; the
/// assembly's own record for a restore/rebind policy — the crate /// assembly's own record for a restore/rebind policy; the crate
/// ships none, OQ-12), and the explicit `cancel()`. /// ships none, OQ-12), and the explicit `cancel()`.
/// ///
/// **Drop frees nothing on the wire** (the recorded decision in this /// **Drop frees nothing on the wire** (the recorded decision in this
/// module's docs): there is no `Drop` hook — a dropped handle leaves /// module's docs): there is no `Drop` hook, so a dropped handle leaves
/// the peer's listen serving until an explicit `cancel()` or the /// the peer's listen serving until an explicit `cancel()` or the
/// connection's end removes it. The handle does not borrow the /// connection's end removes it. The handle does not borrow the
/// connection: the cancel composes on the shared wire stack (the /// connection: the cancel composes on the shared wire stack (the
@@ -469,18 +469,18 @@ pub struct ReverseListener {
} }
impl ReverseListener { impl ReverseListener {
/// The bound endpoint — the actually-bound address/port or socket /// The bound endpoint, the actually-bound address/port or socket
/// path (never the requested `0` for a mint: the peer's answer is /// path (never the requested `0` for a mint: the peer's answer is
/// the surfaced number). /// the surfaced number).
pub fn endpoint(&self) -> &ReverseEndpoint { pub fn endpoint(&self) -> &ReverseEndpoint {
&self.endpoint &self.endpoint
} }
/// The explicit cancellation — the wire teardown: the peer /// The explicit cancellation, the wire teardown: the peer
/// removes the matching listen (the endpoint's raw wire strings, /// removes the matching listen (the endpoint's raw wire strings,
/// sshd parity; the accept loop ends, the bound port frees) and /// sshd parity; the accept loop ends, the bound port frees) and
/// the confirmed cancel releases the connection's listener count. /// the confirmed cancel releases the connection's listener count.
/// The peer's refusal (an unknown listener — e.g. the already- /// The peer's refusal (an unknown listener, e.g. the already-
/// cancelled, or a cancelled by the connection's end itself) is /// cancelled, or a cancelled by the connection's end itself) is
/// [`OpError::ListenRefused`](crate::OpError). The handle is /// [`OpError::ListenRefused`](crate::OpError). The handle is
/// consumed (one cancel through it); a later cancel by the same /// consumed (one cancel through it); a later cancel by the same
@@ -511,10 +511,10 @@ impl std::fmt::Debug for ReverseListener {
} }
} }
/// The tcp cancel's shared path — the stateless wire form and the /// The tcp cancel's shared path: the stateless wire form and the
/// listener handle's `cancel` compose into it: the wire op, then the /// listener handle's `cancel` compose into it: the wire op, then the
/// confirmed release (exactly once: a second cancel hits the wire's /// confirmed release (exactly once: a second cancel hits the wire's
/// unknown-listener refusal, no release — budget-safe by the wire's /// unknown-listener refusal, no release; budget-safe by the wire's
/// own state machine). /// own state machine).
async fn cancel_reverse_tcp( async fn cancel_reverse_tcp(
shared: &crate::client::ConnectionShared, shared: &crate::client::ConnectionShared,
@@ -531,7 +531,7 @@ async fn cancel_reverse_tcp(
} }
/// The unix cancel's shared path (the twin; the runtime-Unix gate /// The unix cancel's shared path (the twin; the runtime-Unix gate
/// answers before the wire — see the module docs' gate record). /// answers before the wire; see the module docs' gate record).
#[cfg(unix)] #[cfg(unix)]
async fn cancel_reverse_unix( async fn cancel_reverse_unix(
shared: &crate::client::ConnectionShared, shared: &crate::client::ConnectionShared,
@@ -552,28 +552,28 @@ async fn cancel_reverse_unix(
impl SshConnection { impl SshConnection {
/// The reverse-listen row (`-R`): asks the vanilla server to listen /// The reverse-listen row (`-R`): asks the vanilla server to listen
/// on (address, port) and returns the **actually-bound port** — /// on (address, port) and returns the **actually-bound port**;
/// for `port = 0` this is the peer-picked number (`client.md` /// for `port = 0` this is the peer-picked number (`client.md`
/// §Lifecycle: the bound-port primitive, the port-0 mint); for an /// §Lifecycle: the bound-port primitive, the port-0 mint); for an
/// explicit port the wire's success reply carries no number (russh/ /// explicit port the wire's success reply carries no number (russh/
/// sshd parity — the reply's data only exists for mints) and the /// sshd parity; the reply's data only exists for mints) and the
/// bound port *is* the requested one, which the row surfaces. /// bound port *is* the requested one, which the row surfaces.
/// ///
/// Gates, both **before any wire op**: the `forwarded-tcpip` /// Gates, both **before any wire op**: the `forwarded-tcpip`
/// acceptor must be registered /// acceptor must be registered
/// ([`OpError::NoAcceptor`](crate::OpError) — a listen whose data /// ([`OpError::NoAcceptor`](crate::OpError), a listen whose data
/// leg cannot serve is a half-capability) and the connection's /// leg cannot serve is a half-capability) and the connection's
/// listener count must have room /// listener count must have room
/// ([`OpError::ListenCapExceeded`](crate::OpError) — the /// ([`OpError::ListenCapExceeded`](crate::OpError), the
/// dedicated counter sharing /// dedicated counter sharing
/// [`Limits::max_concurrent_channels`](crate::Limits); see the /// [`Limits::max_concurrent_channels`](crate::Limits); see the
/// module docs' cap relation). The peer's refusal is /// module docs' cap relation). The peer's refusal is
/// [`OpError::ListenRefused`](crate::OpError) (the wire's /// [`OpError::ListenRefused`](crate::OpError) (the wire's
/// `RequestDenied`) and releases the count; the connection stays /// `RequestDenied`) and releases the count; the connection stays
/// usable after every refusal class. The listener is explicitly /// usable after every refusal class. The listener is explicitly
/// cancellable — the returned [`ReverseListener`] handle's /// cancellable, via the returned [`ReverseListener`] handle's
/// `cancel()` (the wire-honest teardown; a cancel is never /// `cancel()` (the wire-honest teardown; a cancel is never
/// triggered by a drop — the module docs' recorded decision). /// triggered by a drop; the module docs' recorded decision).
pub async fn reverse_listen( pub async fn reverse_listen(
&self, &self,
address: impl Into<String>, address: impl Into<String>,
@@ -608,10 +608,10 @@ impl SshConnection {
/// The reverse-listen cancel row (`-R` teardown): the explicit /// The reverse-listen cancel row (`-R` teardown): the explicit
/// cancellation of one bound listener by the raw wire strings /// cancellation of one bound listener by the raw wire strings
/// (sshd parity) — the **stateless** wire form beside the /// (sshd parity), the **stateless** wire form beside the
/// [`ReverseListener`] handles' `cancel()` (compose into the same /// [`ReverseListener`] handles' `cancel()` (compose into the same
/// shared path; a confirmed cancel releases the listener count — /// shared path; a confirmed cancel releases the listener count
/// never twice, the wire's unknown-listener refusal governs the /// (never twice; the wire's unknown-listener refusal governs the
/// repeats). The peer's refusal (unknown listener) is /// repeats). The peer's refusal (unknown listener) is
/// [`OpError::ListenRefused`](crate::OpError). /// [`OpError::ListenRefused`](crate::OpError).
pub async fn reverse_listen_cancel( pub async fn reverse_listen_cancel(
@@ -630,7 +630,7 @@ impl SshConnection {
/// [`SshConnection::reverse_listen`] for the /// [`SshConnection::reverse_listen`] for the
/// `forwarded-streamlocal` slot, plus the **runtime-Unix gate** /// `forwarded-streamlocal` slot, plus the **runtime-Unix gate**
/// (OQ-11: off-Unix, [`OpError::UnixUnavailable`](crate::OpError) /// (OQ-11: off-Unix, [`OpError::UnixUnavailable`](crate::OpError)
/// before any wire op — the bind is the platform-bound operation; /// before any wire op; the bind is the platform-bound operation,
/// the channel ingestion is platform-neutral). /// the channel ingestion is platform-neutral).
#[cfg_attr(not(unix), allow(unused_variables))] #[cfg_attr(not(unix), allow(unused_variables))]
pub async fn reverse_listen_unix( pub async fn reverse_listen_unix(
@@ -667,7 +667,7 @@ impl SshConnection {
} }
/// The streamlocal cancel row (matches by the socket path, sshd /// The streamlocal cancel row (matches by the socket path, sshd
/// parity — see [`SshConnection::reverse_listen_cancel`]; composes /// parity, see [`SshConnection::reverse_listen_cancel`]; composes
/// into the same shared path as the unix handles' `cancel()`). /// into the same shared path as the unix handles' `cancel()`).
#[cfg_attr(not(unix), allow(unused_variables))] #[cfg_attr(not(unix), allow(unused_variables))]
pub async fn reverse_listen_unix_cancel( pub async fn reverse_listen_unix_cancel(
@@ -708,7 +708,7 @@ mod tests {
async fn accept(&self, _open: ForwardedStreamlocal) {} async fn accept(&self, _open: ForwardedStreamlocal) {}
} }
/// - *Contract stamp:* the recorded refuse-never-replace posture — /// - *Contract stamp:* the recorded refuse-never-replace posture:
/// a held slot declines the second install (never a quiet /// a held slot declines the second install (never a quiet
/// displacement of a live acceptor); uninstall frees it. /// displacement of a live acceptor); uninstall frees it.
#[test] #[test]
@@ -736,7 +736,7 @@ mod tests {
} }
/// - *Contract stamp:* the two slots are independent (the /// - *Contract stamp:* the two slots are independent (the
/// per-type-registry decision) — the empty streamlocal slot /// per-type-registry decision); the empty streamlocal slot
/// stays unfeatured while the tcpip slot serves. /// stays unfeatured while the tcpip slot serves.
#[test] #[test]
fn the_slots_are_independent() { fn the_slots_are_independent() {
@@ -789,7 +789,7 @@ mod tests {
} }
/// - *Contract stamp:* the handoff counter's panic-safe release /// - *Contract stamp:* the handoff counter's panic-safe release
/// (the bounded-bookkeeping cap relation) — the /// (the bounded-bookkeeping cap relation); the
/// [`HandoffCapGuard`] releases on unwind, so a panicking /// [`HandoffCapGuard`] releases on unwind, so a panicking
/// assembly acceptor can never strand a held slot past the /// assembly acceptor can never strand a held slot past the
/// handoff task. /// handoff task.
+29 -29
View File
@@ -1,4 +1,4 @@
//! The per-connection **client** handler — the dial surface's russh //! The per-connection **client** handler, the dial surface's russh
//! `client::Handler` impl (`w4-dial`), the mirror of //! `client::Handler` impl (`w4-dial`), the mirror of
//! [`crate::handler`]'s server-direction handler. //! [`crate::handler`]'s server-direction handler.
//! //!
@@ -9,46 +9,46 @@
//! the crate's single fingerprint path ([`crate::fingerprint`]), //! the crate's single fingerprint path ([`crate::fingerprint`]),
//! labels it with the dial target (host + port, parsed from the //! labels it with the dial target (host + port, parsed from the
//! dial address), and hands the whole [`PresentedHostKey`] to the //! dial address), and hands the whole [`PresentedHostKey`] to the
//! injected [`HostKeyPolicy`] — the verdict rides straight through //! injected [`HostKeyPolicy`], and the verdict rides straight
//! (`Ok(policy.decide(..).await)`). The crate is policy-blind; the //! through (`Ok(policy.decide(..).await)`). The crate is policy-blind; the
//! built-in [`RejectAll`](crate::RejectAll) verdict makes every dial //! built-in [`RejectAll`](crate::RejectAll) verdict makes every dial
//! refuse; a `false` verdict is the decline disposition russh //! refuse; a `false` verdict is the decline disposition russh
//! surfaces as its `UnknownKey` wire class //! surfaces as its `UnknownKey` wire class
//! ([`DialError::HostKeyRejected`](crate::DialError) //! ([`DialError::HostKeyRejected`](crate::DialError)),
//! — never an accept, never a retry). //! never an accept, never a retry).
//! 2. **Server-initiated channel opens declined cleanly** (ADR-003 §4, //! 2. **Server-initiated channel opens declined cleanly** (ADR-003 §4,
//! the client-side symmetry rule): russh's default posture for every //! the client-side symmetry rule): russh's default posture for every
//! `server_channel_open_*` hook is accept-and-drop — a //! `server_channel_open_*` hook is accept-and-drop, a
//! half-capability the family forbids. Every hook is overridden //! half-capability the family forbids. Every hook is overridden
//! explicit-over-inherited. The `forwarded-tcpip` / //! explicit-over-inherited. The `forwarded-tcpip` /
//! `forwarded-streamlocal` hooks (the `-R` data leg's types) route //! `forwarded-streamlocal` hooks (the `-R` data leg's types) route
//! through the **acceptor registry** ([`crate::client_accept`] — //! through the **acceptor registry** ([`crate::client_accept`],
//! the `w4-reverse-listen` surface): served only under a registered //! the `w4-reverse-listen` surface): served only under a registered
//! acceptor for that type and under the `tunnel` feature (the //! acceptor for that type and under the `tunnel` feature (the
//! [`with_tunnel_surface`]'s tunnel surface //! [`with_tunnel_surface`]'s tunnel surface
//! wiring; a build without `tunnel` has no registry — every hook //! wiring; a build without `tunnel` has no registry, so every hook
//! declines); unregistered/unfeatured opens answer //! declines); unregistered/unfeatured opens answer
//! [`ChannelOpenFailure::AdministrativelyProhibited`] — a clean //! [`ChannelOpenFailure::AdministrativelyProhibited`], a clean
//! wire-visible refusal the peer observes as an //! wire-visible refusal the peer observes as an
//! unconfirmed open, never an accepted-then-dropped channel. The //! unconfirmed open, never an accepted-then-dropped channel. The
//! accepted open's handoff spawns (the spawn-and-return rule's //! accepted open's handoff spawns (the spawn-and-return rule's
//! client mirror): the hook accepts and returns fast, the duplex + //! client mirror): the hook accepts and returns fast, the duplex +
//! the open's fields ride the payload to the assembly's acceptor //! the open's fields ride the payload to the assembly's acceptor
//! call on its own task. Every other type has no registry slot — //! call on its own task. Every other type has no registry slot:
//! the unknown-type gate //! the unknown-type gate
//! ([`Handler::should_accept_unknown_server_channel`]) answers //! ([`Handler::should_accept_unknown_server_channel`]) answers
//! `false` (the hook's reject-before-open form; its `_unknown` //! `false` (the hook's reject-before-open form; its `_unknown`
//! sibling stays a defensive reject — wire-reachable only if that //! sibling stays a defensive reject, wire-reachable only if that
//! gate ever changed, also fail-closed), and the never-feature rows //! gate ever changed, also fail-closed), and the never-feature rows
//! (x11, agent) decline at any configuration. //! (x11, agent) decline at any configuration.
//! //!
//! The accepted-open handoff carries a **bounded bookkeeping** //! The accepted-open handoff carries a **bounded bookkeeping**
//! counter (`AGENTS.md` convention 9, the cap relation recorded in //! counter (`AGENTS.md` convention 9, the cap relation recorded in
//! [`crate::client_accept`]): a dedicated per-connection counter — //! [`crate::client_accept`]): a dedicated per-connection counter
//! sharing the dial's `Limits::max_concurrent_channels` ceiling the //! (sharing the dial's `Limits::max_concurrent_channels` ceiling the
//! server direction's per-session cap uses — admitted in the hook //! server direction's per-session cap uses), admitted in the hook
//! before the accept, released when the handoff task ends (the //! before the accept, released when the handoff task ends (the
//! acceptor's call completing *or the task unwinding* — //! acceptor's call completing *or the task unwinding*;
//! [`crate::client_accept`]'s `HandoffCapGuard` panic-safety); a full //! [`crate::client_accept`]'s `HandoffCapGuard` panic-safety); a full
//! counter declines the arrival cleanly //! counter declines the arrival cleanly
//! (`cap-exceeded`). //! (`cap-exceeded`).
@@ -79,7 +79,7 @@ use crate::{DialError, fingerprint};
/// the two forwarded hooks route through, plus the accepted-open /// the two forwarded hooks route through, plus the accepted-open
/// handoff counter and its ceiling (the dial's /// handoff counter and its ceiling (the dial's
/// `Limits::max_concurrent_channels`; released when the handoff task /// `Limits::max_concurrent_channels`; released when the handoff task
/// ends — completion or unwind, `HandoffCapGuard`'s panic-safety). /// ends, completion or unwind, `HandoffCapGuard`'s panic-safety).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub(crate) struct TunnelSurface { pub(crate) struct TunnelSurface {
acceptors: Arc<AcceptorRegistry>, acceptors: Arc<AcceptorRegistry>,
@@ -87,7 +87,7 @@ pub(crate) struct TunnelSurface {
open_cap: std::sync::Arc<ChannelCap>, open_cap: std::sync::Arc<ChannelCap>,
} }
/// The per-connection client handler the dial surface constructs — /// The per-connection client handler the dial surface constructs;
/// see the module docs. Test surface /// see the module docs. Test surface
/// (`#[doc(hidden)]` re-export from the crate root): the contract /// (`#[doc(hidden)]` re-export from the crate root): the contract
/// tests drive the bare hooks, exactly the /// tests drive the bare hooks, exactly the
@@ -96,7 +96,7 @@ pub struct ClientHandler {
policy: std::sync::Arc<dyn HostKeyPolicy>, policy: std::sync::Arc<dyn HostKeyPolicy>,
host: String, host: String,
port: u16, port: u16,
/// The `-R` surface under `tunnel` (`Some` — the dial wires it; /// The `-R` surface under `tunnel` (`Some`, the dial wires it;
/// the bare-hook tests construct without it to pin the decline-all /// the bare-hook tests construct without it to pin the decline-all
/// default posture in the featured builds too). /// default posture in the featured builds too).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -143,10 +143,10 @@ impl russh::client::Handler for ClientHandler {
/// The host-key policy's hook (OQ-05): the presented key or /// The host-key policy's hook (OQ-05): the presented key or
/// certificate's fingerprint is computed on the crate's single /// certificate's fingerprint is computed on the crate's single
/// fingerprint path (certificates fingerprint their inner key — /// fingerprint path (certificates fingerprint their inner key;
/// the trust decision is the policy's either way) and the whole /// the trust decision is the policy's either way) and the whole
/// [`PresentedHostKey`] rides the policy; the verdict is passed /// [`PresentedHostKey`] rides the policy; the verdict is passed
/// through untouched. `Ok(false)` — russh's decline — is the /// through untouched. `Ok(false)`, russh's decline, is the
/// disposition, never an error path; the policy's own record /// disposition, never an error path; the policy's own record
/// carries why. /// carries why.
async fn check_server_key( async fn check_server_key(
@@ -186,7 +186,7 @@ impl russh::client::Handler for ClientHandler {
} }
/// The `forwarded-tcpip` hook (the `-R` data leg's gate): the /// The `forwarded-tcpip` hook (the `-R` data leg's gate): the
/// registry's slot read at arrival time — registered + room → /// registry's slot read at arrival time (registered + room →
/// accept and hand the assembly the open on a spawned task; no /// accept and hand the assembly the open on a spawned task; no
/// acceptor / no room → the clean decline, never accept-and-drop. /// acceptor / no room → the clean decline, never accept-and-drop.
async fn server_channel_open_forwarded_tcpip( async fn server_channel_open_forwarded_tcpip(
@@ -247,10 +247,10 @@ impl russh::client::Handler for ClientHandler {
} }
} }
/// The `forwarded-streamlocal` hook — the unix-slot's mirror of /// The `forwarded-streamlocal` hook, the unix-slot's mirror of
/// the tcpip gate (the ingestion is platform-neutral: no runtime /// the tcpip gate (the ingestion is platform-neutral: no runtime
/// gate here; the listen rows carry the OQ-11 gate on the bind — /// gate here; the listen rows carry the OQ-11 gate on the bind,
/// the recorded asymmetry, see [`crate::client_accept`]). /// the recorded asymmetry (see [`crate::client_accept`]).
async fn server_channel_open_forwarded_streamlocal( async fn server_channel_open_forwarded_streamlocal(
&mut self, &mut self,
channel: Channel<Msg>, channel: Channel<Msg>,
@@ -350,8 +350,8 @@ impl russh::client::Handler for ClientHandler {
Ok(()) Ok(())
} }
/// A never-feature row (ADR-003 §5): x11 has no family seam — the /// A never-feature row (ADR-003 §5): x11 has no family seam, so
/// decline is not even registry territory. /// the decline is not even registry territory.
async fn server_channel_open_x11( async fn server_channel_open_x11(
&mut self, &mut self,
channel: Channel<Msg>, channel: Channel<Msg>,
@@ -368,7 +368,7 @@ impl russh::client::Handler for ClientHandler {
} }
/// A never-feature row (ADR-003 §5): agent forwarding is the /// A never-feature row (ADR-003 §5): agent forwarding is the
/// anti-pattern of the vault custody model — always declined. /// anti-pattern of the vault custody model, always declined.
async fn server_channel_open_agent_forward( async fn server_channel_open_agent_forward(
&mut self, &mut self,
channel: Channel<Msg>, channel: Channel<Msg>,
@@ -384,7 +384,7 @@ impl russh::client::Handler for ClientHandler {
/// Wire-reachable only if /// Wire-reachable only if
/// [`russh::client::Handler::should_accept_unknown_server_channel`] /// [`russh::client::Handler::should_accept_unknown_server_channel`]
/// ever answers `true` (it does not — the skeleton's default is /// ever answers `true` (it does not; the skeleton's default is
/// fail-closed); the defensive reject keeps the posture honest /// fail-closed); the defensive reject keeps the posture honest
/// even then. /// even then.
async fn server_channel_open_unknown( async fn server_channel_open_unknown(
@@ -415,7 +415,7 @@ mod tests {
/// - *Contract stamp:* ADR-004 decision 6 posture mirror (the /// - *Contract stamp:* ADR-004 decision 6 posture mirror (the
/// client-side: the policy is injected, the handler never /// client-side: the policy is injected, the handler never
/// decides trust), pinned at the dial wiring — here, that the /// decides trust), pinned at the dial wiring; here, that the
/// labels ride the construction so the policy sees /// labels ride the construction so the policy sees
/// `PresentedHostKey { host, port }` verbatim, and the /// `PresentedHostKey { host, port }` verbatim, and the
/// fingerprint rides the crate's single fingerprint path. /// fingerprint rides the crate's single fingerprint path.
+66 -66
View File
@@ -1,67 +1,67 @@
//! The client op rows (`w4-op-rows`) — `client.md` §op rows in code: //! The client op rows (`w4-op-rows`), `client.md` §op rows in code:
//! the family op → vanilla translation the reverse-runner shape //! the family op → vanilla translation the reverse-runner shape
//! drives (the hub dialed [`SshConnection`](crate::SshConnection) and //! drives (the hub dialed [`SshConnection`](crate::SshConnection) and
//! now opens every channel itself). **Type symmetry at the seam is //! now opens every channel itself). **Type symmetry at the seam is
//! the contract:** family-protocol types in, vanilla wire ops out, no //! the contract:** family-protocol types in, vanilla wire ops out, no
//! vanilla-concept leaks up — the rows' inputs are the mapped //! vanilla-concept leaks up; the rows' inputs are the mapped
//! sibling crates' types (the alktty session shapes for the exec/pty //! sibling crates' types (the alktty session shapes for the exec/pty
//! rows, alkgit's `Service` for the git row) and the surfaces are //! rows, alkgit's `Service` for the git row) and the surfaces are
//! crate-owned (`RemoteSession` / `TunnelChannel` / `GitPackSession`), //! crate-owned (`RemoteSession` / `TunnelChannel` / `GitPackSession`),
//! the failure taxonomy [`OpError`](crate::OpError). All rows share //! the failure taxonomy [`OpError`](crate::OpError). All rows share
//! the russh 0.65 mechanics POC-3 §D.3 recorded: //! the russh 0.65 mechanics POC-3 §D.3 recorded:
//! //!
//! - `Channel::exec()` — there is **no `request_exec` at this pin**. //! - `Channel::exec()`: there is **no `request_exec` at this pin**.
//! - A request's Success/Failure replies arrive **on the channel //! - A request's Success/Failure replies arrive **on the channel
//! queue via `wait()`**, not as `Err` from the request call — every //! queue via `wait()`**, not as `Err` from the request call, so every
//! request-carrying row drains until the expected message, //! request-carrying row drains until the expected message,
//! `continue`-ing past the interleaved rest, and maps a `Failure` //! `continue`-ing past the interleaved rest, and maps a `Failure`
//! to [`OpError::RequestRefused`](crate::OpError). The drain is //! to [`OpError::RequestRefused`](crate::OpError). The drain is
//! message-capped (bounded resources: a peer that never answers //! message-capped (bounded resources: a peer that never answers
//! cannot loop it unboundedly; the *connection's* wall clock — the //! cannot loop it unboundedly; the *connection's* wall clock, the
//! dial's always-set [`Limits::inactivity_timeout`](crate::Limits) — //! dial's always-set [`Limits::inactivity_timeout`](crate::Limits),
//! bounds the idle waits). //! bounds the idle waits).
//! - **The typed opens resolve `Err` directly on a server reject** //! - **The typed opens resolve `Err` directly on a server reject**
//! (POC-1 §6) — [`OpError::OpenDeclined`](crate::OpError), family-mirrored //! (POC-1 §6) to [`OpError::OpenDeclined`](crate::OpError), family-mirrored
//! [`OpenDeclineReason`](crate::OpenDeclineReason), **and the //! [`OpenDeclineReason`](crate::OpenDeclineReason), **and the
//! connection stays usable after the decline** (row-pinned per //! connection stays usable after the decline** (row-pinned per
//! decline class). //! decline class).
//! - **No channel-queue consumption where a `into_stream()` conversion //! - **No channel-queue consumption where a `into_stream()` conversion
//! follows** (POC-1 §6's fresh-`wait()` caveat) — the tunnel rows //! follows** (POC-1 §6's fresh-`wait()` caveat): the tunnel rows
//! (the only `into_stream` consumers) go typed-open → straight //! (the only `into_stream` consumers) go typed-open → straight
//! conversion, nothing between. The exit-**consuming** rows //! conversion, nothing between. The exit-**consuming** rows
//! (exec/pty, git) cannot ride `into_stream` at all — the //! (exec/pty, git) cannot ride `into_stream` at all, since the
//! conversion's reader side swallows every non-`Data` message, //! conversion's reader side swallows every non-`Data` message,
//! which is exactly where `ExitStatus`/`ExitSignal` live — so they //! which is exactly where `ExitStatus`/`ExitSignal` live, so they
//! run the **split-halves wait-pump** (`Channel::split()` + a //! run the **split-halves wait-pump** (`Channel::split()` + a
//! spawned task draining `wait()`), the shape the server //! spawned task draining `wait()`), the shape the server
//! direction's git row landed for the same reason //! direction's git row landed for the same reason
//! (`w4-git-exit-close`). The exit-ride is the family plane's (§the //! (`w4-git-exit-close`). The exit-ride is the family plane's (§the
//! session rows) or the consumed-report oneshot's (§the git row). //! session rows) or the consumed-report oneshot's (§the git row).
//! - `window_change`/`signal` are fire-and-forget on the wire (no SSH //! - `window_change`/`signal` are fire-and-forget on the wire (no SSH
//! reply exists) — the pumps send and drop the outcome, never //! reply exists); the pumps send and drop the outcome, never
//! awaiting one. //! awaiting one.
//! //!
//! ## The family plane (the session rows) //! ## The family plane (the session rows)
//! //!
//! The exec/pty session's data plane is the **alktty chunk framing in //! The exec/pty session's data plane is the **alktty chunk framing in
//! both directions** — the exact plane the server direction's session //! both directions**, the exact plane the server direction's session
//! bridge speaks ([`crate::tty_map`]), consumed not re-derived: //! bridge speaks ([`crate::tty_map`]), consumed not re-derived:
//! stdin chunks (`STREAM_STDIN`) ride the wire `data` (the //! stdin chunks (`STREAM_STDIN`) ride the wire `data` (the
//! zero-length sentinel = wire EOF, the bridge's decided translation — //! zero-length sentinel = wire EOF, the bridge's decided translation,
//! the session keeps pumping after it); `CTRL_IN` chunks translate to //! the session keeps pumping after it); `CTRL_IN` chunks translate to
//! the wire request rows (`Resize` → `window_change` (the session //! the wire request rows (`Resize` → `window_change` (the session
//! dimension clamp is not needed — the family plane is already //! dimension clamp is not needed; the family plane is already
//! `u16`), `Signal{name}` → `signal` through the shared table's //! `u16`), `Signal{name}` → `signal` through the shared table's
//! name→`Sig` map, `Eof` → wire EOF); wire `data` → stdout chunks, //! name→`Sig` map, `Eof` → wire EOF); wire `data` → stdout chunks,
//! wire `extended_data(1)` → stderr chunks; and the exit rides //! wire `extended_data(1)` → stderr chunks; and the exit rides
//! **in-band as the family's exit chunk** (`STREAM_CTRL_OUT`, //! **in-band as the family's exit chunk** (`STREAM_CTRL_OUT`,
//! exit-chunk-is-last — alktty's ADR-004, mapping.md's contract): //! exit-chunk-is-last, alktty's ADR-004, mapping.md's contract):
//! wire `ExitStatus(N)` → code `N`, wire `ExitSignal(name)` → //! wire `ExitStatus(N)` → code `N`, wire `ExitSignal(name)` →
//! `−signum` through the shared table's inverse read //! `−signum` through the shared table's inverse read
//! ([`crate::exit_code`] — the derivation contract's client arm; a //! ([`crate::exit_code`], the derivation contract's client arm; a
//! name with no table entry rides the `−1` sentinel, the mapping //! name with no table entry rides the `−1` sentinel, the mapping
//! never invents). A session that ends with **no exit report** //! never invents). A session that ends with **no exit report**
//! delivers no exit chunk — the output half just ends (mapping.md's //! delivers no exit chunk; the output half just ends (mapping.md's
//! no-exit-chunk close family: "reports nothing"). The session's //! no-exit-chunk close family: "reports nothing"). The session's
//! ttymodes never ride the wire pty request (the OQ-44 //! ttymodes never ride the wire pty request (the OQ-44
//! reserved-JSON posture mirrored: the door never invents wire //! reserved-JSON posture mirrored: the door never invents wire
@@ -78,30 +78,30 @@
//! | tunnel target (`-L`; the alksocks `DialFn`) | `tunnel` | `channel_open_direct_tcpip` | [`SshConnection::tunnel_direct`] | //! | tunnel target (`-L`; the alksocks `DialFn`) | `tunnel` | `channel_open_direct_tcpip` | [`SshConnection::tunnel_direct`] |
//! | the streamlocal twin | `tunnel` | `channel_open_direct_streamlocal`, runtime-Unix-gated | [`SshConnection::tunnel_direct_unix`] | //! | the streamlocal twin | `tunnel` | `channel_open_direct_streamlocal`, runtime-Unix-gated | [`SshConnection::tunnel_direct_unix`] |
//! | upstream git service | `git` | the shared grammar's command + `exec`; the pack duplex | [`SshConnection::git_service`] | //! | upstream git service | `git` | the shared grammar's command + `exec`; the pack duplex | [`SshConnection::git_service`] |
//! | sftp | *every build* | **none** — the OQ-13 decline placeholder | [`SshConnection::sftp`] | //! | sftp | *every build* | **none**, the OQ-13 decline placeholder | [`SshConnection::sftp`] |
//! //!
//! The exec-command **length budget** applies identically to the //! The exec-command **length budget** applies identically to the
//! command-carrying rows (the server direction's pre-parse cap //! command-carrying rows (the server direction's pre-parse cap
//! mirrored at the command-construction gate — bounded resources; //! mirrored at the command-construction gate; bounded resources,
//! an unbounded command buffer can never ride a dial): //! an unbounded command buffer can never ride a dial):
//! [`OpError::CommandTooLong`](crate::OpError) before any wire op. //! [`OpError::CommandTooLong`](crate::OpError) before any wire op.
//! //!
//! No client-side in-crate channel cap *admits* ops (the server //! No client-side in-crate channel cap *admits* ops (the server
//! direction's cap is a wire-battery defense; the client rows are //! direction's cap is a wire-battery defense; the client rows are
//! hub-driven — the family app chose the dial): bounded resources on //! hub-driven (the family app chose the dial): bounded resources on
//! this direction are the config's always-on wall clock/keepalives, //! this direction are the config's always-on wall clock/keepalives,
//! the russh windows, the bounded duplexes/mpscs the pumps ride, and //! the russh windows, the bounded duplexes/mpscs the pumps ride, and
//! the drain cap — every spawn bounded by its own task's end //! the drain cap; every spawn bounded by its own task's end
//! conditions. //! conditions.
//! //!
//! ## Spawn posture //! ## Spawn posture
//! //!
//! The op methods await their launch (typed open, requests, the //! The op methods await their launch (typed open, requests, the
//! reply drains) on the *caller's* task — never the russh packet //! reply drains) on the *caller's* task, never the russh packet
//! loop's — so the server direction's spawn-and-return deadlock class //! loop's, so the server direction's spawn-and-return deadlock class
//! does not exist here; the long-lived pumps spawn at launch and end //! does not exist here; the long-lived pumps spawn at launch and end
//! on their terminal conditions (the exit chunk, the channel's end, //! on their terminal conditions (the exit chunk, the channel's end,
//! or the hub dropping the surface — a drop is wire-clean: the input //! or the hub dropping the surface; a drop is wire-clean: the input
//! pump's terminal path closes the channel, the pump pair reaping as //! pump's terminal path closes the channel, the pump pair reaping as
//! their duplex halves drop). //! their duplex halves drop).
@@ -117,13 +117,13 @@ use crate::client::SshConnection;
use crate::client_handler::ClientHandler; use crate::client_handler::ClientHandler;
use crate::error::OpError; use crate::error::OpError;
/// The op-row pump duplex capacity — the crate's bridge-pair /// The op-row pump duplex capacity, the crate's bridge-pair
/// convention (the wave-2 loopback harness's per-direction capacity), /// convention (the wave-2 loopback harness's per-direction capacity),
/// keeping every pump's backpressure bounded. /// keeping every pump's backpressure bounded.
#[cfg(any(feature = "session", feature = "git"))] #[cfg(any(feature = "session", feature = "git"))]
const OP_DUPLEX_CAPACITY: usize = 65_536; const OP_DUPLEX_CAPACITY: usize = 65_536;
/// The pump-side read buffer — the SSH-side granularity (per-chunk /// The pump-side read buffer, the SSH-side granularity (per-chunk
/// payloads are bounded by the dial's `maximum_packet_size`). /// payloads are bounded by the dial's `maximum_packet_size`).
#[cfg(feature = "git")] #[cfg(feature = "git")]
const PUMP_BUF: usize = 16 * 1024; const PUMP_BUF: usize = 16 * 1024;
@@ -137,14 +137,14 @@ const DRAIN_MESSAGE_CAP: usize = 1024;
/// The originator label the door puts on the wire's `direct-tcpip` /// The originator label the door puts on the wire's `direct-tcpip`
/// opens (the SSH originator fields are what a vanilla client fills /// opens (the SSH originator fields are what a vanilla client fills
/// in for its own local listener; this door's hub-driven dials carry /// in for its own local listener; this door's hub-driven dials carry
/// the door's label and no port — recorded decision: nothing /// the door's label and no port, recorded decision: nothing
/// invented, nothing leaked, the peer's logs name the door). /// invented, nothing leaked, the peer's logs name the door).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
const TUNNEL_ORIGINATOR: &str = "alk-ssh"; const TUNNEL_ORIGINATOR: &str = "alk-ssh";
/// The wire exit status → the family `i32` exit-code convention: a /// The wire exit status → the family `i32` exit-code convention: a
/// code beyond the family `i32` range is unwritable as an exit-chunk /// code beyond the family `i32` range is unwritable as an exit-chunk
/// code; the clamp rides (defensive corner — vanilla command codes /// code; the clamp rides (defensive corner, vanilla command codes
/// are far below it; the mapping never invents a value, it narrows). /// are far below it; the mapping never invents a value, it narrows).
#[cfg(any(feature = "session", feature = "git"))] #[cfg(any(feature = "session", feature = "git"))]
fn family_exit_code(status: u32) -> i32 { fn family_exit_code(status: u32) -> i32 {
@@ -155,7 +155,7 @@ fn family_exit_code(status: u32) -> i32 {
/// op taxonomy (a channel-open failure → /// op taxonomy (a channel-open failure →
/// [`OpError::OpenDeclined`](crate::OpError::OpenDeclined), /// [`OpError::OpenDeclined`](crate::OpError::OpenDeclined),
/// family-mirrored; everything else transparent). The russh level's /// family-mirrored; everything else transparent). The russh level's
/// test observable (`POC-1 §6`'s `Err(ChannelOpenFailure)` — the /// test observable (`POC-1 §6`'s `Err(ChannelOpenFailure)`, the
/// awaiting typed call's resolution) rides this point; the /// awaiting typed call's resolution) rides this point; the
/// connection stays usable after every decline (row-pinned). /// connection stays usable after every decline (row-pinned).
#[cfg(any(feature = "session", feature = "git", feature = "tunnel"))] #[cfg(any(feature = "session", feature = "git", feature = "tunnel"))]
@@ -165,7 +165,7 @@ fn open_error(error: russh::Error) -> OpError {
/// The request-reply drain (the §module docs' recorded mechanics, /// The request-reply drain (the §module docs' recorded mechanics,
/// shared by the exec/pty rows and the git row): Success/Failure via /// shared by the exec/pty rows and the git row): Success/Failure via
/// `wait()` — never an `Err` from the request call — `continue` past /// `wait()`, never an `Err` from the request call; `continue` past
/// the interleaved rest (the POC-3 §D.3 break-on-first-message trap, /// the interleaved rest (the POC-3 §D.3 break-on-first-message trap,
/// not repeated), message-capped. `Failure` is the wire /// not repeated), message-capped. `Failure` is the wire
/// request-refusal class ([`OpError::RequestRefused`]); the /// request-refusal class ([`OpError::RequestRefused`]); the
@@ -197,28 +197,28 @@ async fn drain_launch_request(channel: &mut russh::Channel<Msg>) -> Result<(), O
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The sftp placeholder — every build (OQ-13; no feature, no dep) // The sftp placeholder, every build (OQ-13; no feature, no dep)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
impl SshConnection { impl SshConnection {
/// The sftp op — the client-side half of the OQ-13 decline /// The sftp op, the client-side half of the OQ-13 decline
/// placeholder: **declines in every build, before any wire op** /// placeholder: **declines in every build, before any wire op**
/// (never a half-opened subsystem channel). The feature exists /// (never a half-opened subsystem channel). The feature exists
/// and ships in the build string; the route does not, and arrives /// and ships in the build string; the route does not, and arrives
/// with alkfs as its own ADR — both directions' placeholders are /// with alkfs as its own ADR; both directions' placeholders are
/// then replaced by real rows (`client.md` §What the crate does /// then replaced by real rows (`client.md` §What the crate does
/// *not* include). Compiles feature-free; the `Ok` type is /// *not* include). Compiles feature-free; the `Ok` type is
/// `Infallible` by design — nothing can ever populate it here. /// `Infallible` by design; nothing can ever populate it here.
pub async fn sftp(&self) -> Result<std::convert::Infallible, OpError> { pub async fn sftp(&self) -> Result<std::convert::Infallible, OpError> {
Err(OpError::SftpNotServed) Err(OpError::SftpNotServed)
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The session rows — exec / pty (under `session`) // The session rows: exec / pty (under `session`)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// A remote **session** — the family session plane over the runner /// A remote **session**: the family session plane over the runner
/// (the session rows' surface): the alktty chunk framing in both /// (the session rows' surface): the alktty chunk framing in both
/// directions, exactly the plane the server direction's session /// directions, exactly the plane the server direction's session
/// bridge speaks (`tty_map`), reversed. Two duplex halves /// bridge speaks (`tty_map`), reversed. Two duplex halves
@@ -245,7 +245,7 @@ impl std::fmt::Debug for RemoteSession {
} }
} }
/// Which request launches a session — the family op's payload /// Which request launches a session: the family op's payload
/// (§the row inventory; pipe-mode exec vs the shell form). /// (§the row inventory; pipe-mode exec vs the shell form).
#[cfg(feature = "session")] #[cfg(feature = "session")]
enum LaunchRequest { enum LaunchRequest {
@@ -255,11 +255,11 @@ enum LaunchRequest {
#[cfg(feature = "session")] #[cfg(feature = "session")]
impl SshConnection { impl SshConnection {
/// The pipe-mode exec row: `ssh host "cmd"` in, family-typed — /// The pipe-mode exec row: `ssh host "cmd"` in, family-typed;
/// the session channel's open + the wire `exec` request, then the /// the session channel's open + the wire `exec` request, then the
/// chunk-framed session (§the family plane; stderr separated). /// chunk-framed session (§the family plane; stderr separated).
/// The command rides the wire verbatim (the remote end's shell /// The command rides the wire verbatim (the remote end's shell
/// parses it — the vanilla exec grammar; never argv-split). /// parses it, the vanilla exec grammar; never argv-split).
pub async fn exec(&self, command: impl Into<String>) -> Result<RemoteSession, OpError> { pub async fn exec(&self, command: impl Into<String>) -> Result<RemoteSession, OpError> {
let command = command.into(); let command = command.into();
if command.len() > self.budgets().max_exec_command_len { if command.len() > self.budgets().max_exec_command_len {
@@ -272,7 +272,7 @@ impl SshConnection {
/// carries the family /// carries the family
/// [`alktty::negotiation::TerminalParamsWire`](alktty::negotiation:: /// [`alktty::negotiation::TerminalParamsWire`](alktty::negotiation::
/// TerminalParamsWire) first (its acceptance drained), then the /// TerminalParamsWire) first (its acceptance drained), then the
/// exec. **A refused pty refuses the whole row** — never a /// exec. **A refused pty refuses the whole row**: never a
/// half-capability, never a silent pty-less fall. /// half-capability, never a silent pty-less fall.
pub async fn pty_exec( pub async fn pty_exec(
&self, &self,
@@ -292,8 +292,8 @@ impl SshConnection {
} }
/// The shell row ("own a shell on the runner"): with /// The shell row ("own a shell on the runner"): with
/// `params` — the pty mode (the interactive `ssh` shape); with /// `params`, this is the pty mode (the interactive `ssh` shape); with
/// `None` — the pipe-mode shell (the `ssh -T` shape; a pty-less /// `None`, the pipe-mode shell (the `ssh -T` shape; a pty-less
/// shell request is legitimate vanilla and served, never declined, /// shell request is legitimate vanilla and served, never declined,
/// mirroring the server direction's decided parity). /// mirroring the server direction's decided parity).
pub async fn shell( pub async fn shell(
@@ -361,7 +361,7 @@ async fn open_remote_session(
/// `CTRL_IN` `Eof` / the hub's input half dropping = the wire EOF /// `CTRL_IN` `Eof` / the hub's input half dropping = the wire EOF
/// *once*, then the idle-for-kill state (the session keeps pumping); /// *once*, then the idle-for-kill state (the session keeps pumping);
/// the terminal kill (from the output pump's terminal paths) → the /// the terminal kill (from the output pump's terminal paths) → the
/// wire `channel_close` — the session's drop is wire-clean. /// wire `channel_close`; the session's drop is wire-clean.
#[cfg(feature = "session")] #[cfg(feature = "session")]
async fn session_input_pump( async fn session_input_pump(
write_half: russh::ChannelWriteHalf<Msg>, write_half: russh::ChannelWriteHalf<Msg>,
@@ -432,12 +432,12 @@ async fn session_input_pump(
} }
} }
/// One frame read from the hub→door plane — the *no-disambiguation* /// One frame read from the hub→door plane, in the *no-disambiguation*
/// read direction: this plane's chunks are the family protocol's own /// read direction: this plane's chunks are the family protocol's own
/// (stdin `0` / `CTRL_IN` `3`); no negotiation-error frames exist /// (stdin `0` / `CTRL_IN` `3`); no negotiation-error frames exist
/// toward the door, so the read is the protocol's direct /// toward the door, so the read is the protocol's direct
/// [`ChunkReader::read_chunk`] (a `0x00`-leading frame here is a /// [`ChunkReader::read_chunk`] (a `0x00`-leading frame here is a
/// zero-length stdin chunk — the sentinel — and reads as exactly /// zero-length stdin chunk (the sentinel) and reads as exactly
/// that). A rogue frame is the bounds-checked [`RawError`], mapped to /// that). A rogue frame is the bounds-checked [`RawError`], mapped to
/// the pump's [`HubFrame::Break`]. /// the pump's [`HubFrame::Break`].
#[cfg(feature = "session")] #[cfg(feature = "session")]
@@ -459,7 +459,7 @@ async fn hub_frame(reader: &mut alktty::wire::ChunkReader<tokio::io::DuplexStrea
/// The session row's **output pump**: the wire → the hub's chunk /// The session row's **output pump**: the wire → the hub's chunk
/// plane (§the family plane). `data` → stdout chunks, /// plane (§the family plane). `data` → stdout chunks,
/// `extended_data(1)` → stderr chunks, and the `ExitStatus` / /// `extended_data(1)` → stderr chunks, and the `ExitStatus` /
/// `ExitSignal` terminal → the in-band exit chunk (last) — then the /// `ExitSignal` terminal → the in-band exit chunk (last), then the
/// kill fires and the input pump closes the wire. A channel end with /// kill fires and the input pump closes the wire. A channel end with
/// no report → no exit chunk; the output half just ends. /// no report → no exit chunk; the output half just ends.
#[cfg(feature = "session")] #[cfg(feature = "session")]
@@ -512,10 +512,10 @@ async fn session_output_pump(
} }
} }
/// The family exit chunk's write — `{"type":"exit","code":N}` as the /// The family exit chunk's write: `{"type":"exit","code":N}` as the
/// in-band terminal chunk (exit-chunk-is-last), serialized by the /// in-band terminal chunk (exit-chunk-is-last), serialized by the
/// family currency's own serializer. An unserializable exit (not a /// family currency's own serializer. An unserializable exit (not a
/// reachable shape) writes nothing — the no-report family, never a /// reachable shape) writes nothing; the no-report family, never a
/// corrupted frame. /// corrupted frame.
#[cfg(feature = "session")] #[cfg(feature = "session")]
async fn write_exit_chunk( async fn write_exit_chunk(
@@ -533,16 +533,16 @@ async fn write_exit_chunk(
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The tunnel rows — direct-tcpip / direct-streamlocal (under `tunnel`) // The tunnel rows: direct-tcpip / direct-streamlocal (under `tunnel`)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// A tunnel channel — the tunnel rows' surface: a plain byte duplex /// A tunnel channel, the tunnel rows' surface: a plain byte duplex
/// over the open channel (`-L`'s pump; the alksocks `DialFn` composes /// over the open channel (`-L`'s pump; the alksocks `DialFn` composes
/// exactly this type). Nothing vanilla-typed rides the family /// exactly this type). Nothing vanilla-typed rides the family
/// surface — the duplex's `poll_shutdown` carries the wire EOF. /// surface; the duplex's `poll_shutdown` carries the wire EOF.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub struct TunnelChannel { pub struct TunnelChannel {
/// The russh channel's byte duplex (`pub(crate)` — the `-R` leg's /// The russh channel's byte duplex (`pub(crate)`; the `-R` leg's
/// acceptor payloads construct the type over a server-initiated /// acceptor payloads construct the type over a server-initiated
/// open's `into_stream()` conversion; downstream crates see the /// open's `into_stream()` conversion; downstream crates see the
/// type opaque as ever). /// type opaque as ever).
@@ -595,11 +595,11 @@ impl tokio::io::AsyncWrite for TunnelChannel {
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
impl SshConnection { impl SshConnection {
/// The tunnel-target row (`-L`; the alksocks `DialFn` composes /// The tunnel-target row (`-L`; the alksocks `DialFn` composes
/// over it): the typed open — server reject → /// over it): the typed open; server reject →
/// [`OpError::OpenDeclined`](crate::OpError) with the mirrored /// [`OpError::OpenDeclined`](crate::OpError) with the mirrored
/// class, and the connection stays usable (row-pinned per /// class, and the connection stays usable (row-pinned per
/// decline class). Between the open and the `into_stream()` /// decline class). Between the open and the `into_stream()`
/// conversion **nothing else happens** — no channel-queue /// conversion **nothing else happens**: no channel-queue
/// consumption (POC-1 §6's caveat, respected by construction; the /// consumption (POC-1 §6's caveat, respected by construction; the
/// data pump is the raw byte duplex). /// data pump is the raw byte duplex).
pub async fn tunnel_direct( pub async fn tunnel_direct(
@@ -618,7 +618,7 @@ impl SshConnection {
} }
/// The streamlocal twin (`direct-streamlocal@openssh.com`): same /// The streamlocal twin (`direct-streamlocal@openssh.com`): same
/// typed-open/pump shape, the OQ-11 runtime-Unix gate mirrored — /// typed-open/pump shape, the OQ-11 runtime-Unix gate mirrored;
/// **compiles wherever `tunnel` compiles**, and off-Unix the op /// **compiles wherever `tunnel` compiles**, and off-Unix the op
/// declines cleanly ([`OpError::UnixUnavailable`](crate::OpError)) /// declines cleanly ([`OpError::UnixUnavailable`](crate::OpError))
/// before any wire op. /// before any wire op.
@@ -646,15 +646,15 @@ impl SshConnection {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The git row — upstream exec consumption (under `git`) // The git row: upstream exec consumption (under `git`)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// An upstream **git service channel** — the git row's surface: the /// An upstream **git service channel**, the git row's surface: the
/// raw pack duplex (the git protocol's own byte plane rides the /// raw pack duplex (the git protocol's own byte plane rides the
/// channel untouched), the sshd-side stderr split out, and the /// channel untouched), the sshd-side stderr split out, and the
/// consumed exit report at the family's `i32` exit-code convention /// consumed exit report at the family's `i32` exit-code convention
/// (`ExitStatus(N)` → `N`; `ExitSignal` → `−signum` through the /// (`ExitStatus(N)` → `N`; `ExitSignal` → `−signum` through the
/// shared table — the mapping never invents; a receive `Err` on /// shared table, the mapping never invents; a receive `Err` on
/// `exit` = the pump ended with **no exit report at all**, the /// `exit` = the pump ended with **no exit report at all**, the
/// no-exit-chunk close family's mirror). /// no-exit-chunk close family's mirror).
#[cfg(feature = "git")] #[cfg(feature = "git")]
@@ -666,7 +666,7 @@ pub struct GitPackSession {
/// the pack). /// the pack).
pub output: tokio::io::DuplexStream, pub output: tokio::io::DuplexStream,
/// The wire `extended_data(1)` (the remote program's stderr /// The wire `extended_data(1)` (the remote program's stderr
/// diagnostics) — raw bytes; a hub uninterested in diagnostics /// diagnostics), raw bytes; a hub uninterested in diagnostics
/// drops the half (the pump then discards further stderr). /// drops the half (the pump then discards further stderr).
pub stderr: tokio::io::DuplexStream, pub stderr: tokio::io::DuplexStream,
/// The consumed exit report (see the type docs). /// The consumed exit report (see the type docs).
@@ -683,7 +683,7 @@ impl std::fmt::Debug for GitPackSession {
#[cfg(feature = "git")] #[cfg(feature = "git")]
impl SshConnection { impl SshConnection {
/// The upstream git row: the shared exec grammar's command /// The upstream git row: the shared exec grammar's command
/// (`git-{upload,receive}-pack '/<repo>'` — built through /// (`git-{upload,receive}-pack '/<repo>'`, built through
/// `crate::dispatch`'s construction arm, the parse arm's exact /// `crate::dispatch`'s construction arm, the parse arm's exact
/// inverse; no forked grammar), pointed at the vanilla `sshd`: /// inverse; no forked grammar), pointed at the vanilla `sshd`:
/// typed open + `exec`, the pack duplex pumped, the exit report /// typed open + `exec`, the pack duplex pumped, the exit report
@@ -695,9 +695,9 @@ impl SshConnection {
/// [`OpError::InvalidRepoId`](crate::OpError); /// [`OpError::InvalidRepoId`](crate::OpError);
/// the constructed command is checked against the exec-length /// the constructed command is checked against the exec-length
/// budget ([`crate::Limits::max_exec_command_len`]) → /// budget ([`crate::Limits::max_exec_command_len`]) →
/// [`OpError::CommandTooLong`](crate::OpError) — both **before any /// [`OpError::CommandTooLong`](crate::OpError), both **before any
/// wire op**. (The grammar's other decline classes — /// wire op**. (The grammar's other decline classes,
/// `unknown-service` / `service-only` — are structurally /// `unknown-service` / `service-only`, are structurally
/// unreachable here: the [`alkgit::Service`] input type carries /// unreachable here: the [`alkgit::Service`] input type carries
/// only the two serving services; the construction arm can emit /// only the two serving services; the construction arm can emit
/// no other token.) /// no other token.)
@@ -793,7 +793,7 @@ async fn git_input_pump(
/// The git row's **output pump**: the wire → the pack duplex + /// The git row's **output pump**: the wire → the pack duplex +
/// stderr; the exit report consumed here (the family code) and the /// stderr; the exit report consumed here (the family code) and the
/// kill fired — then the input pump closes the wire. A channel end /// kill fired, then the input pump closes the wire. A channel end
/// with no report → no exit fired; the `exit` oneshot drops (the /// with no report → no exit fired; the `exit` oneshot drops (the
/// hub's receive reads the no-report mirror). /// hub's receive reads the no-report mirror).
#[cfg(feature = "git")] #[cfg(feature = "git")]
@@ -848,7 +848,7 @@ mod tests {
use russh::ChannelOpenFailure; use russh::ChannelOpenFailure;
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept /// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
/// leaks up) + POC-1 §6 — the typed open's decline mapping /// leaks up) + POC-1 §6: the typed open's decline mapping
/// preserves the wire class (`error.rs`'s mirror rows; this /// preserves the wire class (`error.rs`'s mirror rows; this
/// pins the op rows' own consumption point). /// pins the op rows' own consumption point).
#[test] #[test]
@@ -870,7 +870,7 @@ mod tests {
} }
/// - *Contract stamp:* the family exit-currency clamp (§the /// - *Contract stamp:* the family exit-currency clamp (§the
/// family plane) — ordinary codes ride identity, beyond-range /// family plane); ordinary codes ride identity, beyond-range
/// ones narrow (never invent). /// ones narrow (never invent).
#[cfg(any(feature = "session", feature = "git"))] #[cfg(any(feature = "session", feature = "git"))]
#[test] #[test]
+81 -81
View File
@@ -1,20 +1,20 @@
//! The pure configuration surface — the shapes every later wave's //! The pure configuration surface: the shapes every later wave's
//! constructors take (serve-config, dial-config, [`Limits`]). Types //! constructors take (serve-config, dial-config, [`Limits`]). Types
//! only: russh `Config` construction is wave-2 material, dialing lands //! only: russh `Config` construction is wave-2 material, dialing lands
//! at wave 4; nothing here touches the wire or the vault. //! at wave 4; nothing here touches the wire or the vault.
//! //!
//! Two directions, one budget model (`overview.md` §Public surface): //! Two directions, one budget model (`overview.md` §Public surface):
//! //!
//! - [`ServeConfig`] — the server direction's assembly-supplied serving //! - [`ServeConfig`]: the server direction's assembly-supplied serving
//! configuration: the OQ-08 fields ([`ServeConfig::shell_argv`] / //! configuration: the OQ-08 fields ([`ServeConfig::shell_argv`] /
//! [`ServeConfig::exec_wrapper_argv`]), the injected identity, //! [`ServeConfig::exec_wrapper_argv`]), the injected identity,
//! host-key and tunnel handles, the per-feature family-seam handles //! host-key and tunnel handles, the per-feature family-seam handles
//! (the wave-3 extension — [`SessionSeam`] carries the alktty //! (the wave-3 extension, where [`SessionSeam`] carries the alktty
//! backends map + backend selector + ownership provider; //! backends map + backend selector + ownership provider;
//! [`GitSeam`] carries the alkgit backend quartet + the caller-owned //! [`GitSeam`] carries the alkgit backend quartet + the caller-owned
//! pipeline + the push-options flag), the OQ-09 `cwd`/`env` values, //! pipeline + the push-options flag), the OQ-09 `cwd`/`env` values,
//! and the [`Limits`]. //! and the [`Limits`].
//! - [`DialConfig`] — the client direction's dial configuration //! - [`DialConfig`]: the client direction's dial configuration
//! (`client.md` §The dial surface): address, `username`, key source, //! (`client.md` §The dial surface): address, `username`, key source,
//! host-key policy, and the bounded-resource knobs (carried by //! host-key policy, and the bounded-resource knobs (carried by
//! [`Limits`]). //! [`Limits`]).
@@ -22,7 +22,7 @@
//! Crate rules this module encodes: //! Crate rules this module encodes:
//! //!
//! - **Unset-declines, no crate defaults** (OQ-08): the OQ-08 fields //! - **Unset-declines, no crate defaults** (OQ-08): the OQ-08 fields
//! are `Option` and their unset semantics live in the type shape — //! are `Option` and their unset semantics live in the type shape,
//! an unset field makes its row decline with a clean error at //! an unset field makes its row decline with a clean error at
//! request time (wave-2/3 consumption), never a fallback, never //! request time (wave-2/3 consumption), never a fallback, never
//! construction-time validation (`error.rs`'s no-config-error //! construction-time validation (`error.rs`'s no-config-error
@@ -33,10 +33,10 @@
//! [`ServeConfig::git_seam`] unset → the git row declines even //! [`ServeConfig::git_seam`] unset → the git row declines even
//! though `git` is on (the decline matrix's config-driven notes; //! though `git` is on (the decline matrix's config-driven notes;
//! the feature alone configures nothing). The tunnel branch carries //! the feature alone configures nothing). The tunnel branch carries
//! its flat `Option<Arc<ChannelClient>>` — an egress-only //! its flat `Option<Arc<ChannelClient>>`, because an egress-only
//! (route-1-only) door needs no struct for leg 1 — and the `-R` //! (route-1-only) door needs no struct for leg 1, and the `-R`
//! listen leg's registration seam landed with `w3-tunnel-listen` //! listen leg's registration seam landed with `w3-tunnel-listen`
//! (`ServeConfig::tunnel_listen_seam` — the wave-3 seam-config's //! (`ServeConfig::tunnel_listen_seam`, the wave-3 seam-config's
//! second course-correction; the shape decision recorded on //! second course-correction; the shape decision recorded on
//! [`TunnelListenSeam`]: unset → the listen rows decline even with //! [`TunnelListenSeam`]: unset → the listen rows decline even with
//! a fabric leg present, because the door-side inbound-op serving //! a fabric leg present, because the door-side inbound-op serving
@@ -48,17 +48,17 @@
//! no vault lifecycle of its own. A git-only tunnel build simply //! no vault lifecycle of its own. A git-only tunnel build simply
//! leaves [`ServeConfig::channel_client`] absent. //! leaves [`ServeConfig::channel_client`] absent.
//! - **Bounded resources everywhere** (convention 9): every session //! - **Bounded resources everywhere** (convention 9): every session
//! — both directions — carries a [`Limits`]; there is no //! in both directions carries a [`Limits`]; there is no
//! unbounded-loop or unbounded-buffer configuration shape the //! unbounded-loop or unbounded-buffer configuration shape the
//! assembly can build from these types. //! assembly can build from these types.
//! //!
//! Knob-name parity with the russh 0.65 pin (ADR-002): //! Knob-name parity with the russh 0.65 pin (ADR-002):
//! `window_size`, `maximum_packet_size`, `channel_buffer_size`, //! `window_size`, `maximum_packet_size`, `channel_buffer_size`,
//! `inactivity_timeout`, `keepalive_interval`, `keepalive_max`, //! `inactivity_timeout`, `keepalive_interval`, `keepalive_max`,
//! `max_auth_attempts` — wave-2/wave-4 map these fields into the russh //! `max_auth_attempts`; wave-2/wave-4 map these fields into the russh
//! `Config` verbatim. russh's rekey `Limits` stay russh-enforced at //! `Config` verbatim. russh's rekey `Limits` stay russh-enforced at
//! its own RFC-4253 §9 defaults (wave-2); they are not carried here — //! its own RFC-4253 §9 defaults (wave-2); they are not carried here,
//! this module's [`Limits`] is the crate's in-crate + mapped-knob //! since this module's [`Limits`] is the crate's in-crate + mapped-knob
//! budget model. //! budget model.
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -84,15 +84,15 @@ use crate::key_source::{HostKeySource, KeySource};
/// The bounded-resources budget model (`overview.md` /// The bounded-resources budget model (`overview.md`
/// §Bounded-resources policy): the in-crate caps plus the russh-knob /// §Bounded-resources policy): the in-crate caps plus the russh-knob
/// mapping with the documented default postures. Every session — /// mapping with the documented default postures. Every session,
/// server and client direction — carries one (`AGENTS.md` convention /// server or client direction, carries one (`AGENTS.md` convention
/// 9); there is no unbounded configuration shape. /// 9); there is no unbounded configuration shape.
/// ///
/// Carried by value (`Copy`): a [`Limits`] is plain tuning data, and /// Carried by value (`Copy`): a [`Limits`] is plain tuning data, and
/// wave-2/4 copies it into the russh `Config` construction. /// wave-2/4 copies it into the russh `Config` construction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits { pub struct Limits {
/// The crates' own cap on concurrent channels per session — /// The crates' own cap on concurrent channels per session;
/// opened channels beyond the cap are declined (fail-closed /// opened channels beyond the cap are declined (fail-closed
/// refuse, never queue). The per-identity policy layer (when the /// refuse, never queue). The per-identity policy layer (when the
/// alkcall bridge carries it) is additional and comes with the /// alkcall bridge carries it) is additional and comes with the
@@ -102,39 +102,39 @@ pub struct Limits {
pub max_concurrent_channels: usize, pub max_concurrent_channels: usize,
/// The parse-input cap for the wave-3 exec grammar: a wire exec /// The parse-input cap for the wave-3 exec grammar: a wire exec
/// command string longer than this declines before any parse or /// command string longer than this declines before any parse or
/// resource allocation (decline-first, alloc-never — the ADR-006 /// resource allocation (decline-first, alloc-never, the ADR-006
/// dispatch contract). Assembly-tunable. /// dispatch contract). Assembly-tunable.
/// ///
/// Default: `DEFAULT_MAX_EXEC_COMMAND_LEN`. /// Default: `DEFAULT_MAX_EXEC_COMMAND_LEN`.
pub max_exec_command_len: usize, pub max_exec_command_len: usize,
/// Maximal number of allowed authentication attempts — mapped to /// Maximal number of allowed authentication attempts, mapped to
/// russh's `max_auth_attempts` (server direction). Default `10` /// russh's `max_auth_attempts` (server direction). Default `10`
/// per ADR-004 decision 6: the OpenSSH counting `none`-probe /// per ADR-004 decision 6: the OpenSSH counting `none`-probe
/// consumes one attempt, so `10` leaves `IdentitiesOnly`-style /// consumes one attempt, so `10` leaves `IdentitiesOnly`-style
/// clients at least nine real key attempts. /// clients at least nine real key attempts.
pub max_auth_attempts: usize, pub max_auth_attempts: usize,
/// The initial channel receive window — mapped to russh's /// The initial channel receive window, mapped to russh's
/// `window_size` (per-channel memory bound). /// `window_size` (per-channel memory bound).
/// ///
/// Default: 2 MiB (russh's own default posture, kept). /// Default: 2 MiB (russh's own default posture, kept).
pub window_size: u32, pub window_size: u32,
/// The maximal size of a single SSH packet — mapped to russh's /// The maximal size of a single SSH packet, mapped to russh's
/// `maximum_packet_size` (per-channel record-size bound). /// `maximum_packet_size` (per-channel record-size bound).
/// ///
/// Default: 32 KiB (russh's own default posture, kept). /// Default: 32 KiB (russh's own default posture, kept).
pub maximum_packet_size: u32, pub maximum_packet_size: u32,
/// Per-channel unprocessed-message buffer — mapped to russh's /// Per-channel unprocessed-message buffer, mapped to russh's
/// `channel_buffer_size` (the backpressure bound); messages past /// `channel_buffer_size` (the backpressure bound); messages past
/// it propagate backpressure to the TCP stream, never an /// it propagate backpressure to the TCP stream, never an
/// unbounded buffer. /// unbounded buffer.
/// ///
/// Default: 100 (russh's own default posture, kept). /// Default: 100 (russh's own default posture, kept).
pub channel_buffer_size: usize, pub channel_buffer_size: usize,
/// The session wall clock — the connection is garbage-collected /// The session wall clock; the connection is garbage-collected
/// after this long with no activity. Mapped to russh's /// after this long with no activity. Mapped to russh's
/// `Option<inactivity_timeout>` as always-set (`Some(value)`) in /// `Option<inactivity_timeout>` as always-set (`Some(value)`) in
/// wave 2/4: the bounded-resources policy's "set /// wave 2/4: the bounded-resources policy's "set
/// (assembly-tunable)" posture — the crate never constructs a /// (assembly-tunable)" posture; the crate never constructs a
/// session with no wall clock. Assembly-tunable. /// session with no wall clock. Assembly-tunable.
/// ///
/// Default: `DEFAULT_INACTIVITY_TIMEOUT`. /// Default: `DEFAULT_INACTIVITY_TIMEOUT`.
@@ -142,13 +142,13 @@ pub struct Limits {
/// Client-direction keepalive interval (cloud fleets): the /// Client-direction keepalive interval (cloud fleets): the
/// dead-peer probe, mapped to russh's `keepalive_interval`. /// dead-peer probe, mapped to russh's `keepalive_interval`.
/// ///
/// Default: `Some(15 s)` — the reverse-runner-evidence posture /// Default: `Some(15 s)`, the reverse-runner-evidence posture
/// (OpenSSH `ServerAliveInterval=15` parity): with russh's own /// (OpenSSH `ServerAliveInterval=15` parity): with russh's own
/// default (`None`) a dead NAT'd connection sits unnoticed until /// default (`None`) a dead NAT'd connection sits unnoticed until
/// an exec health check fails (`reverse-runner.md` lesson 3). /// an exec health check fails (`reverse-runner.md` lesson 3).
/// `None` is available to the assembly but is not the posture. /// `None` is available to the assembly but is not the posture.
pub keepalive_interval: Option<Duration>, pub keepalive_interval: Option<Duration>,
/// How many unanswered keepalives close the connection — mapped /// How many unanswered keepalives close the connection, mapped
/// to russh's `keepalive_max`. /// to russh's `keepalive_max`.
/// ///
/// Default: `3` (OpenSSH `ServerAliveCountMax=3` parity, the /// Default: `3` (OpenSSH `ServerAliveCountMax=3` parity, the
@@ -204,30 +204,30 @@ impl Default for Limits {
/// seams): what `drive_session[_pre_negotiated]` takes that the /// seams): what `drive_session[_pre_negotiated]` takes that the
/// assembly composes (alktty 0.8.2's `adapter.rs:205-241` signature). /// assembly composes (alktty 0.8.2's `adapter.rs:205-241` signature).
/// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option` /// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option`
/// struct per seam, not flat `Option` fields — each seam is consumed /// struct per seam, not flat `Option` fields; each seam is consumed
/// atomically by exactly one seam call, so a struct admits no /// atomically by exactly one seam call, so a struct admits no
/// half-supplied seam states and the unset-declines contract reads as /// half-supplied seam states and the unset-declines contract reads as
/// one `Option`: **`ServeConfig::session_seam` unset → the session /// one `Option`: **`ServeConfig::session_seam` unset → the session
/// rows decline even though the `session` feature is on** (the /// rows decline even though the `session` feature is on** (the
/// decline matrix's config-driven note), checked at request time by /// decline matrix's config-driven note), checked at request time by
/// the mappings — no construction-time validation. Cloned into each /// the mappings; no construction-time validation. Cloned into each
/// spawned serving task (all payload types `Arc` or plain data). /// spawned serving task (all payload types `Arc` or plain data).
#[cfg(feature = "session")] #[cfg(feature = "session")]
#[derive(Clone)] #[derive(Clone)]
pub struct SessionSeam { pub struct SessionSeam {
/// The alktty backends map — the `TtyAdapter`-style registry keyed /// The alktty backends map, the `TtyAdapter`-style registry keyed
/// by the `NegotiateRequest.backend` selector string. **Assembled /// by the `NegotiateRequest.backend` selector string. **Assembled
/// by the assembly**: alkssh never touches a real backend, and the /// by the assembly**: alkssh never touches a real backend, and the
/// `local` backend shapes stay alktty-side (this dep's `local` /// `local` backend shapes stay alktty-side (this dep's `local`
/// feature is not enabled — the map holds assembly-supplied /// feature is not enabled; the map holds assembly-supplied
/// implementations of alktty's [`TtyBackend`] only). /// implementations of alktty's [`TtyBackend`] only).
pub backends: Arc<HashMap<String, Arc<dyn TtyBackend>>>, pub backends: Arc<HashMap<String, Arc<dyn TtyBackend>>>,
/// The backend **selector** — the `NegotiateRequest.backend` /// The backend **selector**, the `NegotiateRequest.backend`
/// string alkssh writes into the locally-constructed negotiation /// string alkssh writes into the locally-constructed negotiation
/// (`w3-tty-session`'s negotiation rows; the wire command/args /// (`w3-tty-session`'s negotiation rows; the wire command/args
/// never reach it). Assembly-supplied, never wire-derived. /// never reach it). Assembly-supplied, never wire-derived.
pub backend: String, pub backend: String,
/// The ADR-050 ownership provider — passed through /// The ADR-050 ownership provider, passed through
/// `drive_session[_pre_negotiated]` verbatim (the family ACL /// `drive_session[_pre_negotiated]` verbatim (the family ACL
/// layer riding alongside; an alkcall type, no new dep). /// layer riding alongside; an alkcall type, no new dep).
pub ownership: Option<Arc<dyn OwnershipProvider>>, pub ownership: Option<Arc<dyn OwnershipProvider>>,
@@ -248,27 +248,27 @@ impl std::fmt::Debug for SessionSeam {
/// The `git` feature's alkgit seam handle (`mapping.md` §The seams): /// The `git` feature's alkgit seam handle (`mapping.md` §The seams):
/// what `serve_duplex_resolved` takes that the assembly composes /// what `serve_duplex_resolved` takes that the assembly composes
/// (alkgit 0.1.0's `duplex.rs:1412-1426` signature — the session /// (alkgit 0.1.0's `duplex.rs:1412-1426` signature: the session
/// budget, the four backend traits, the caller-owned pipeline, the /// budget, the four backend traits, the caller-owned pipeline, the
/// push-options gate; the `limits` field landed with `w3-git-mapping` /// push-options gate; the `limits` field landed with `w3-git-mapping`
/// — the first seam consumer — as a `w3-seam-config` course /// the first seam consumer, as a `w3-seam-config` course
/// correction: alkgit's ADR-009 budget is assembly config, the door /// correction: alkgit's ADR-009 budget is assembly config, the door
/// never invents it). Same whole-`Option`-struct decision as /// never invents it). Same whole-`Option`-struct decision as
/// [`SessionSeam`]: /// [`SessionSeam`]:
/// **`ServeConfig::git_seam` unset → the git row declines even though /// **`ServeConfig::git_seam` unset → the git row declines even though
/// the `git` feature is on**, checked at request time by the /// the `git` feature is on**, checked at request time by the
/// mappings — no construction-time validation. /// mappings; no construction-time validation.
#[cfg(feature = "git")] #[cfg(feature = "git")]
#[derive(Clone)] #[derive(Clone)]
pub struct GitSeam { pub struct GitSeam {
/// The alkgit session budget (alkgit ADR-009's per-session bundle — /// The alkgit session budget (alkgit ADR-009's per-session bundle:
/// wall clock, pack-size caps, negotiation rounds): **constructed /// wall clock, pack-size caps, negotiation rounds): **constructed
/// by the assembly** (alkgit's own contract — the door never /// by the assembly** (alkgit's own contract: the door never
/// invents budget values), passed into `serve_duplex_resolved` /// invents budget values), passed into `serve_duplex_resolved`
/// verbatim (`w3-git-mapping`). Carried `Copy`; [`alkgit::Limits`]'s /// verbatim (`w3-git-mapping`). Carried `Copy`; [`alkgit::Limits`]'s
/// own defaults are the assembly's baseline. /// own defaults are the assembly's baseline.
pub limits: alkgit::Limits, pub limits: alkgit::Limits,
/// The repo-id resolution trait (`GitRegistry::resolve`) — the /// The repo-id resolution trait (`GitRegistry::resolve`), the
/// ADR-007 pre-protocol gate's other half; alkssh never resolves /// ADR-007 pre-protocol gate's other half; alkssh never resolves
/// (the collapsed-generic-denial posture lives in alkgit). /// (the collapsed-generic-denial posture lives in alkgit).
pub registry: Arc<dyn GitRegistry>, pub registry: Arc<dyn GitRegistry>,
@@ -278,13 +278,13 @@ pub struct GitSeam {
/// The pack generation + negotiation-ack trait /// The pack generation + negotiation-ack trait
/// (`generate`/`common_haves`). /// (`generate`/`common_haves`).
pub packgen: Arc<dyn GitPackGen>, pub packgen: Arc<dyn GitPackGen>,
/// The pack-preparation trait (`prepare` — never applies; the /// The pack-preparation trait (`prepare` never applies; the
/// `GitRefs::apply_updates` CAS stays the single ingestion home). /// `GitRefs::apply_updates` CAS stays the single ingestion home).
pub ingest: Arc<dyn GitPackIngest>, pub ingest: Arc<dyn GitPackIngest>,
/// The assembler's push-options flag (alkgit's ADR-013 §11 gate, /// The assembler's push-options flag (alkgit's ADR-013 §11 gate,
/// passed through verbatim) — `false` in the v1 default. /// passed through verbatim), `false` in the v1 default.
pub push_options_negotiated: bool, pub push_options_negotiated: bool,
/// The caller-owned, server-wide pipeline permit — alkgit's /// The caller-owned, server-wide pipeline permit: alkgit's
/// pack-pipeline concurrency bound, *shared across every session /// pack-pipeline concurrency bound, *shared across every session
/// the serving direction takes* (the caller keeps its own handle; /// the serving direction takes* (the caller keeps its own handle;
/// `Arc` for the share). One per `DialConfig`-independent serve /// `Arc` for the share). One per `DialConfig`-independent serve
@@ -308,7 +308,7 @@ impl std::fmt::Debug for GitSeam {
} }
/// The `tunnel` feature's `-R` listen-leg registration seam (the /// The `tunnel` feature's `-R` listen-leg registration seam (the
/// `w3-tunnel-listen` shape — the wave-3 seam-config's second /// `w3-tunnel-listen` shape, the wave-3 seam-config's second
/// course-correction, resolved per its own decision record): the /// course-correction, resolved per its own decision record): the
/// assembly-supplied `Arc<ChannelClient>` alone cannot carry the /// assembly-supplied `Arc<ChannelClient>` alone cannot carry the
/// door-side acceptance of the fabric's inbound forwarded opens, /// door-side acceptance of the fabric's inbound forwarded opens,
@@ -324,11 +324,11 @@ impl std::fmt::Debug for GitSeam {
/// **`ServeConfig::tunnel_listen_seam` unset → the `-R` listen rows /// **`ServeConfig::tunnel_listen_seam` unset → the `-R` listen rows
/// (`tcpip_forward` / `streamlocal_forward` / both cancel forms) /// (`tcpip_forward` / `streamlocal_forward` / both cancel forms)
/// decline even under the `tunnel` feature with a fabric leg present** /// decline even under the `tunnel` feature with a fabric leg present**
/// — an egress-only door (the POC-3 stage-C posture) is legitimate /// : an egress-only door (the POC-3 stage-C posture) is legitimate
/// config; the feature alone configures nothing. Checked at request /// config; the feature alone configures nothing. Checked at request
/// time by the mappings; no construction-time validation. The /// time by the mappings; no construction-time validation. The
/// registration itself (the alkcall openable registration the seam /// registration itself (the alkcall openable registration the seam
/// fronts) is alkssh-side work at [`crate::SshAdapter::new`] — a /// fronts) is alkssh-side work at [`crate::SshAdapter::new`]; a
/// failed registration is a failed construction (fail-closed, never a /// failed registration is a failed construction (fail-closed, never a
/// half-configured adapter). /// half-configured adapter).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -336,7 +336,7 @@ pub struct TunnelListenSeam {
/// The **shared** per-connection `OperationRegistry` the fabric /// The **shared** per-connection `OperationRegistry` the fabric
/// leg's inbound ops dispatch against: the exact `Arc` the /// leg's inbound ops dispatch against: the exact `Arc` the
/// assembly passed in this leg's `ChannelClient` construction /// assembly passed in this leg's `ChannelClient` construction
/// (`ServingConfig.registry`) — alkssh registers the /// (`ServingConfig.registry`): alkssh registers the
/// `tunnel/forwarded` openable into it, and alkcall's dispatch /// `tunnel/forwarded` openable into it, and alkcall's dispatch
/// reads it per call (the W2 post-hoc-registration posture). The /// reads it per call (the W2 post-hoc-registration posture). The
/// door's own ACL entry rides the registration; arrivals lacking /// door's own ACL entry rides the registration; arrivals lacking
@@ -355,7 +355,7 @@ use alkcall::registry::registration::OperationRegistry;
impl TunnelListenSeam { impl TunnelListenSeam {
/// Builds the seam over the assembly's shared per-connection /// Builds the seam over the assembly's shared per-connection
/// registry (the `ServingConfig.registry` the same fabric leg's /// registry (the `ServingConfig.registry` the same fabric leg's
/// `ChannelClient` was constructed with — one `Arc`, both sides). /// `ChannelClient` was constructed with: one `Arc`, both sides).
pub fn new(registry: Arc<OperationRegistry>) -> Self { pub fn new(registry: Arc<OperationRegistry>) -> Self {
Self { registry } Self { registry }
} }
@@ -372,22 +372,22 @@ impl std::fmt::Debug for TunnelListenSeam {
/// The server direction's assembly-supplied serving configuration /// The server direction's assembly-supplied serving configuration
/// (`overview.md` §Server stack): everything wave-2's serving loop /// (`overview.md` §Server stack): everything wave-2's serving loop
/// takes at construction. Pure data + injected handles — no russh /// takes at construction. Pure data + injected handles; no russh
/// `Config` is built here (wave 2), no serving loop exists yet. /// `Config` is built here (wave 2), no serving loop exists yet.
/// ///
/// Construction carries no validation beyond the required handles: /// Construction carries no validation beyond the required handles:
/// the unfeatured/unset capability surfaces are structural `Option`s /// the unfeatured/unset capability surfaces are structural `Option`s
/// that decline at request time (the fail-closed contract) — see /// that decline at request time (the fail-closed contract); see
/// [`ServeConfig::shell_argv`] and [`ServeConfig::channel_client`]. /// [`ServeConfig::shell_argv`] and [`ServeConfig::channel_client`].
pub struct ServeConfig { pub struct ServeConfig {
/// OQ-08 field 1: the shell argv, run verbatim for /// OQ-08 field 1: the shell argv, run verbatim for
/// `shell_request` (`NegotiateRequest.cmd`). No crate default — /// `shell_request` (`NegotiateRequest.cmd`). No crate default:
/// **unset → `shell_request` declines with a clean error, never a /// **unset → `shell_request` declines with a clean error, never a
/// fallback**: the crate is not an sshd and has no account model /// fallback**: the crate is not an sshd and has no account model
/// from which a shell could be derived, so any baked-in shell /// from which a shell could be derived, so any baked-in shell
/// would be deployment policy smuggled into the crate. A /// would be deployment policy smuggled into the crate. A
/// `session` build with this unset serves neither shell nor exec /// `session` build with this unset serves neither shell nor exec
/// requests (the git row is unaffected — parsing never needs a /// requests (the git row is unaffected, since parsing never needs a
/// shell). /// shell).
/// ///
/// Set to `Some(vec)` to serve the shell row (feature-gated at /// Set to `Some(vec)` to serve the shell row (feature-gated at
@@ -396,33 +396,33 @@ pub struct ServeConfig {
/// OQ-08 field 2: the exec wrapper argv (platform-neutral, e.g. /// OQ-08 field 2: the exec wrapper argv (platform-neutral, e.g.
/// `["/bin/sh", "-c"]`). A non-git `exec_request`'s wire command /// `["/bin/sh", "-c"]`). A non-git `exec_request`'s wire command
/// string is appended as the final argv element /// string is appended as the final argv element
/// (`wrapper ++ [cmd]`) — vanilla `ssh host "cmd"` quoting /// (`wrapper ++ [cmd]`), the vanilla `ssh host "cmd"` quoting
/// semantics; command strings are never naively split. No crate /// semantics; command strings are never naively split. No crate
/// default — **unset → the non-git exec row declines with a /// default: **unset → the non-git exec row declines with a
/// clean error, never a fallback**. /// clean error, never a fallback**.
/// ///
/// Set to `Some(vec)` to serve the pipe-exec row (feature-gated /// Set to `Some(vec)` to serve the pipe-exec row (feature-gated
/// at wave 2: only under the `session` feature). /// at wave 2: only under the `session` feature).
pub exec_wrapper_argv: Option<Vec<String>>, pub exec_wrapper_argv: Option<Vec<String>>,
/// The OQ-09 posture's assembly-supplied working directory for /// The OQ-09 posture's assembly-supplied working directory for
/// alktty `NegotiateRequest.cwd` — **never wire-derived** /// alktty `NegotiateRequest.cwd`, **never wire-derived**
/// (`env_request` declines unconditionally; wire env is not /// (`env_request` declines unconditionally; wire env is not
/// family config). Unset → the backend-side default applies. /// family config). Unset → the backend-side default applies.
pub session_cwd: Option<PathBuf>, pub session_cwd: Option<PathBuf>,
/// The OQ-09 posture's assembly-supplied environment for /// The OQ-09 posture's assembly-supplied environment for
/// `NegotiateRequest.env` — **never wire-derived**. Empty by /// `NegotiateRequest.env`, **never wire-derived**. Empty by
/// default. /// default.
pub session_env: BTreeMap<String, String>, pub session_env: BTreeMap<String, String>,
/// The `session` feature's alktty seam handle ([`SessionSeam`] — /// The `session` feature's alktty seam handle ([`SessionSeam`],
/// the backends map + backend selector + ownership provider the /// the backends map + backend selector + ownership provider the
/// `drive_session[_pre_negotiated]` rows take). **Unset → the /// `drive_session[_pre_negotiated]` rows take). **Unset → the
/// session rows decline even though the `session` feature is on** /// session rows decline even though the `session` feature is on**
/// (the decline matrix's config-driven note; the feature alone /// (the decline matrix's config-driven note; the feature alone
/// configures nothing). Wave-3 extension of the wave-1 config /// configures nothing). Wave-3 extension of the wave-1 config
/// surface — shape decision recorded on [`SessionSeam`]. /// surface; shape decision recorded on [`SessionSeam`].
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub session_seam: Option<SessionSeam>, pub session_seam: Option<SessionSeam>,
/// The `git` feature's alkgit seam handle ([`GitSeam`] — the four /// The `git` feature's alkgit seam handle ([`GitSeam`], the four
/// backend traits + the caller-owned pipeline + the push-options /// backend traits + the caller-owned pipeline + the push-options
/// flag the `serve_duplex_resolved` row takes). **Unset → the git /// flag the `serve_duplex_resolved` row takes). **Unset → the git
/// row declines even though the `git` feature is on** (same /// row declines even though the `git` feature is on** (same
@@ -430,33 +430,33 @@ pub struct ServeConfig {
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub git_seam: Option<GitSeam>, pub git_seam: Option<GitSeam>,
/// The injected identity provider (alkcall's /// The injected identity provider (alkcall's
/// `IdentityProvider::resolve_from_fingerprint`) — the assembly /// `IdentityProvider::resolve_from_fingerprint`), the assembly
/// supplies it; the crate never resolves identities itself and /// supplies it; the crate never resolves identities itself and
/// holds no identity model. /// holds no identity model.
pub identity_provider: Arc<dyn IdentityProvider>, pub identity_provider: Arc<dyn IdentityProvider>,
/// The injected host-key source (the wave-1 /// The injected host-key source (the wave-1
/// [`HostKeySource`] seam, decrypt-at-construction per ADR-005 /// [`HostKeySource`] seam, decrypt-at-construction per ADR-005
/// decision 6) — feeds wave-2's `Config::keys`; a load failure /// decision 6) feeds wave-2's `Config::keys`; a load failure
/// means no server is constructed, never a half-configured one. /// means no server is constructed, never a half-configured one.
pub host_key_source: Arc<dyn HostKeySource>, pub host_key_source: Arc<dyn HostKeySource>,
/// The tunnel route-1 bridge's family-fabric entry: a held /// The tunnel route-1 bridge's family-fabric entry: a held
/// alkcall `ChannelClient` (`TunnelSession::open_direct` runs on /// alkcall `ChannelClient` (`TunnelSession::open_direct` runs on
/// it). **Absent → every tunnel row declines with a clean error /// it). **Absent → every tunnel row declines with a clean error
/// even under the `tunnel` feature** (the feature alone configures /// even under the `tunnel` feature** (the feature alone configures
/// nothing — the fabric handle is the row's other half). The /// nothing; the fabric handle is the row's other half). The
/// present handle is still gated by the `tunnel` feature at wave /// present handle is still gated by the `tunnel` feature at wave
/// 2: absent + unfeatured both decline; present + featured /// 2: absent + unfeatured both decline; present + featured
/// serves. /// serves.
pub channel_client: Option<Arc<ChannelClient>>, pub channel_client: Option<Arc<ChannelClient>>,
/// The `-R` listen leg's registration seam ([`TunnelListenSeam`] — /// The `-R` listen leg's registration seam ([`TunnelListenSeam`],
/// the shared per-connection registry the fabric's inbound /// the shared per-connection registry the fabric's inbound
/// forwarded opens dispatch against). **Unset → the listen rows /// forwarded opens dispatch against). **Unset → the listen rows
/// decline even under the `tunnel` feature with a fabric leg /// decline even under the `tunnel` feature with a fabric leg
/// present** (the egress-only door is legitimate config — the /// present** (the egress-only door is legitimate config, the
/// POC-3 stage-C posture; shape decision recorded on the type). /// POC-3 stage-C posture; shape decision recorded on the type).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub tunnel_listen_seam: Option<TunnelListenSeam>, pub tunnel_listen_seam: Option<TunnelListenSeam>,
/// The listen leg's per-arrival bookkeeping — installed by /// The listen leg's per-arrival bookkeeping, installed by
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both /// [`crate::SshAdapter::new`] when the fabric leg + seam are both
/// present (the registration site). Crate-private: never assembly /// present (the registration site). Crate-private: never assembly
/// data. /// data.
@@ -469,10 +469,10 @@ pub struct ServeConfig {
impl ServeConfig { impl ServeConfig {
/// Builds a serve-config from the two injected handles the /// Builds a serve-config from the two injected handles the
/// serving direction can never default — the identity provider /// serving direction can never default, the identity provider
/// and the host-key source. The OQ-08 fields start unset (their /// and the host-key source. The OQ-08 fields start unset (their
/// rows decline), the tunnel handle starts absent (its rows /// rows decline), the tunnel handle starts absent (its rows
/// decline), and `limits` starts at [`Limits::default()`] — /// decline), and `limits` starts at [`Limits::default()`],
/// override the `pub` fields from the assembly. /// override the `pub` fields from the assembly.
pub fn new( pub fn new(
identity_provider: Arc<dyn IdentityProvider>, identity_provider: Arc<dyn IdentityProvider>,
@@ -500,14 +500,14 @@ impl ServeConfig {
/// Whether the `shell_request` row is configurable at all in this /// Whether the `shell_request` row is configurable at all in this
/// configuration (`Some` under the `session` feature): `false` /// configuration (`Some` under the `session` feature): `false`
/// means the row declines — the unset-declines contract, checked /// means the row declines; the unset-declines contract, checked
/// at request time by the wave-2 dispatch, never here. /// at request time by the wave-2 dispatch, never here.
pub fn shell_configured(&self) -> bool { pub fn shell_configured(&self) -> bool {
self.shell_argv.is_some() self.shell_argv.is_some()
} }
/// Whether the non-git `exec_request` (pipe) row is configurable /// Whether the non-git `exec_request` (pipe) row is configurable
/// at all in this configuration: `false` means the row declines — /// at all in this configuration: `false` means the row declines;
/// the unset-declines contract, checked at request time by the /// the unset-declines contract, checked at request time by the
/// wave-2 dispatch, never here. /// wave-2 dispatch, never here.
pub fn exec_wrapper_configured(&self) -> bool { pub fn exec_wrapper_configured(&self) -> bool {
@@ -524,7 +524,7 @@ impl ServeConfig {
/// Whether the `-R` listen rows' registration seam is configured /// Whether the `-R` listen rows' registration seam is configured
/// in this configuration ([`TunnelListenSeam`] present; the /// in this configuration ([`TunnelListenSeam`] present; the
/// `tunnel` feature gate applies at the dispatch on top): `false` /// `tunnel` feature gate applies at the dispatch on top): `false`
/// means the listen rows decline — the unset-declines contract, /// means the listen rows decline; the unset-declines contract,
/// checked at request time by the mappings, never here. /// checked at request time by the mappings, never here.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub fn tunnel_listen_seam_configured(&self) -> bool { pub fn tunnel_listen_seam_configured(&self) -> bool {
@@ -534,7 +534,7 @@ impl ServeConfig {
/// Whether the session rows' family seam is configured in this /// Whether the session rows' family seam is configured in this
/// configuration ([`SessionSeam`] present; the `session` feature /// configuration ([`SessionSeam`] present; the `session` feature
/// gate applies at the dispatch on top): `false` means the rows /// gate applies at the dispatch on top): `false` means the rows
/// decline — the unset-declines contract, checked at request time /// decline; the unset-declines contract, checked at request time
/// by the wave-3 mappings, never here. /// by the wave-3 mappings, never here.
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub fn session_seam_configured(&self) -> bool { pub fn session_seam_configured(&self) -> bool {
@@ -543,8 +543,8 @@ impl ServeConfig {
/// Whether the git row's family seam is configured in this /// Whether the git row's family seam is configured in this
/// configuration ([`GitSeam`] present; the `git` feature gate /// configuration ([`GitSeam`] present; the `git` feature gate
/// applies at the dispatch on top): `false` means the row declines /// applies at the dispatch on top): `false` means the row declines;
/// — the unset-declines contract, checked at request time by the /// the unset-declines contract, checked at request time by the
/// wave-3 mappings, never here. /// wave-3 mappings, never here.
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub fn git_seam_configured(&self) -> bool { pub fn git_seam_configured(&self) -> bool {
@@ -581,21 +581,21 @@ impl std::fmt::Debug for ServeConfig {
/// The client direction's dial configuration (`client.md` §The dial /// The client direction's dial configuration (`client.md` §The dial
/// surface): everything wave-4's dial surface takes. Pure data + /// surface): everything wave-4's dial surface takes. Pure data +
/// injected handles — the russh client `Config` construction and the /// injected handles; the russh client `Config` construction and the
/// dial itself live in the crate's dial module (the `w4-dial` /// dial itself live in the crate's dial module (the `w4-dial`
/// surface). /// surface).
/// ///
/// Construction carries no validation: the key source's own /// Construction carries no validation: the key source's own
/// load path is fail-closed ([`KeySource`] — locked/absent/decryption /// load path is fail-closed ([`KeySource`]: locked/absent/decryption
/// failures are all no-dial outcomes), and the host-key policy's /// failures are all no-dial outcomes), and the host-key policy's
/// default is fail-closed ([crate::host_key::RejectAll]). /// default is fail-closed ([crate::host_key::RejectAll]).
/// ///
/// **The `username` field** (the task's sanctioned /// **The `username` field** (the task's sanctioned
/// `w3-seam-config`-pattern course-correction — the only amendment the /// `w3-seam-config`-pattern course-correction, the only amendment the
/// wave-1 shape needed): the USERAUTH_REQUEST's user string. The /// wave-1 shape needed): the USERAUTH_REQUEST's user string. The
/// wave-1 shape carried everything `client.md` §The dial surface names /// wave-1 shape carried everything `client.md` §The dial surface names
/// but structurally missed this one: russh's `authenticate_publickey` /// but structurally missed this one: russh's `authenticate_publickey`
/// requires a user, and the remote end evaluates it — the family /// requires a user, and the remote end evaluates it; the family
/// identity model stays fingerprint-only forever (`auth.md` §Host-key; /// identity model stays fingerprint-only forever (`auth.md` §Host-key;
/// the username is not identity and carries no routing semantics, /// the username is not identity and carries no routing semantics,
/// OQ-03), so this field is pure dial data the assembly supplies. No /// OQ-03), so this field is pure dial data the assembly supplies. No
@@ -603,28 +603,28 @@ impl std::fmt::Debug for ServeConfig {
/// smuggled into the crate); an empty string is accepted as data and /// smuggled into the crate); an empty string is accepted as data and
/// is the remote end's to refuse. /// is the remote end's to refuse.
pub struct DialConfig { pub struct DialConfig {
/// The dial target — `host:port` as russh's connect surface /// The dial target: `host:port` as russh's connect surface
/// takes it (the reverse-runner shape: vanilla `sshd` on the /// takes it (the reverse-runner shape: vanilla `sshd` on the
/// fleet box). The same address labels the host-key policy's /// fleet box). The same address labels the host-key policy's
/// presented-key decisions (host = the parsed host, port = the /// presented-key decisions (host = the parsed host, port = the
/// parsed port). /// parsed port).
pub address: String, pub address: String,
/// The user the dial authenticates as, verbatim on the /// The user the dial authenticates as, verbatim on the
/// USERAUTH_REQUEST — not identity (the fingerprint is; ADR-004 /// USERAUTH_REQUEST, not identity (the fingerprint is; ADR-004
/// decision 5's posture, mirrored client-side), never logged (the /// decision 5's posture, mirrored client-side), never logged (the
/// crate's event vocabulary carries no username). See the type /// crate's event vocabulary carries no username). See the type
/// docs. /// docs.
pub username: String, pub username: String,
/// The key source the dial authenticates with (the wave-1 /// The key source the dial authenticates with (the wave-1
/// [`KeySource`] seam) — never a static key, never env vars; /// [`KeySource`] seam): never a static key, never env vars;
/// decrypt-at-dial, fail-closed per ADR-005. /// decrypt-at-dial, fail-closed per ADR-005.
pub key_source: Arc<dyn KeySource>, pub key_source: Arc<dyn KeySource>,
/// The injected host-key policy (OQ-05) — the trust decision /// The injected host-key policy (OQ-05), the trust decision
/// surface the dial calls at russh's `check_server_key` hook. /// surface the dial calls at russh's `check_server_key` hook.
/// The crate is policy-blind; the built-in default policy /// The crate is policy-blind; the built-in default policy
/// ([crate::host_key::RejectAll]) connects to nothing, by design. /// ([crate::host_key::RejectAll]) connects to nothing, by design.
pub host_key_policy: Arc<dyn HostKeyPolicy>, pub host_key_policy: Arc<dyn HostKeyPolicy>,
/// The bounded-resource budget this dial runs under — the /// The bounded-resource budget this dial runs under; the
/// keepalive interval/max, the wall clock and the window/packet /// keepalive interval/max, the wall clock and the window/packet
/// knobs ride here (see [`Limits`]; the keepalive 15 s / 3 /// knobs ride here (see [`Limits`]; the keepalive 15 s / 3
/// default posture is the client direction's). /// default posture is the client direction's).
@@ -634,7 +634,7 @@ pub struct DialConfig {
impl DialConfig { impl DialConfig {
/// Builds a dial-config from the address, the user it /// Builds a dial-config from the address, the user it
/// authenticates as, and the two injected handles the dial /// authenticates as, and the two injected handles the dial
/// direction can never default — the key source and the host-key /// direction can never default, the key source and the host-key
/// policy. `limits` starts at [`Limits::default()`] (keepalive /// policy. `limits` starts at [`Limits::default()`] (keepalive
/// 15 s / 3 posture riding inside). /// 15 s / 3 posture riding inside).
pub fn new( pub fn new(
@@ -1033,7 +1033,7 @@ mod tests {
} }
// The wave-3 per-feature seam handles: the construction shapes, the // The wave-3 per-feature seam handles: the construction shapes, the
// unset-declines-preserved rows and the Debug posture — each block // unset-declines-preserved rows and the Debug posture; each block
// only runs in the build that carries its feature (compile-time // only runs in the build that carries its feature (compile-time
// feature isolation: no git build ever touches alktty types, // feature isolation: no git build ever touches alktty types,
// no session build ever touches alkgit types). // no session build ever touches alkgit types).
@@ -1131,7 +1131,7 @@ mod tests {
/// - *Contract stamp:* the `w3-tunnel-listen` seam shape (the /// - *Contract stamp:* the `w3-tunnel-listen` seam shape (the
/// unset-declines contract's tunnel extension): the seam starts /// unset-declines contract's tunnel extension): the seam starts
/// unset (the listen rows decline — an egress-only door is /// unset (the listen rows decline; an egress-only door is
/// legitimate config), sets independently of the fabric handle, /// legitimate config), sets independently of the fabric handle,
/// and Debug-shapes without registry internals. /// and Debug-shapes without registry internals.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
+51 -51
View File
@@ -1,75 +1,75 @@
//! The exec-grammar dispatch core (`w2-dispatch-decline-core`, full //! The exec-grammar dispatch core (`w2-dispatch-decline-core`, full
//! grammar `w3-git-mapping`) — the routing decision and the wire-decline //! grammar `w3-git-mapping`): the routing decision and the wire-decline
//! reason shaping the per-connection handler hooks speak //! reason shaping the per-connection handler hooks speak
//! ([`crate::handler`]). //! ([`crate::handler`]).
//! //!
//! **Parse-only** (ADR-006 point 3 as amended): an `exec_request` //! **Parse-only** (ADR-006 point 3 as amended): an `exec_request`
//! parses *first* — before dispatch and before any resource //! parses *first*, before dispatch and before any resource
//! allocation (**decline first, alloc never**, the POC-3 stage-A rule: //! allocation (**decline first, alloc never**, the POC-3 stage-A rule:
//! a leaked channel alloc is a capability leak). The parse is total: //! a leaked channel alloc is a capability leak). The parse is total:
//! it routes every input to exactly one of [`ExecRoute`]'s outcomes. //! it routes every input to exactly one of [`ExecRoute`]'s outcomes.
//! The *per-length limit* ([`crate::Limits::max_exec_command_len`]) is //! The *per-length limit* ([`crate::Limits::max_exec_command_len`]) is
//! consumed **before** this parse — the handler checks the wire byte //! consumed **before** this parse: the handler checks the wire byte
//! length first (pre-parse, pre-allocation), so everything below //! length first (pre-parse, pre-allocation), so everything below
//! assumes a sized input. Invalid UTF-8 parses to the pipe row //! assumes a sized input. Invalid UTF-8 parses to the pipe row
//! (parse-then-fail, the wave-2 rule). //! (parse-then-fail, the wave-2 rule).
//! //!
//! ## The exec grammar (`w3-git-mapping` — the wave-2 probe's named //! ## The exec grammar (`w3-git-mapping`, the wave-2 probe's named
//! replacement, its unit rows migrated) //! replacement, its unit rows migrated)
//! //!
//! What vanilla git sends over SSH (`git-upload-pack '/owner/demo'` — //! What vanilla git sends over SSH (`git-upload-pack '/owner/demo'`:
//! single-quoted repo path with a leading `/`; POC-3 stage A §2's //! single-quoted repo path with a leading `/`; POC-3 stage A §2's
//! verified grammar table): //! verified grammar table):
//! //!
//! - **`git-{upload,receive}-pack '<path>'`** → [`ExecRoute::Git`]: //! - **`git-{upload,receive}-pack '<path>'`** → [`ExecRoute::Git`]:
//! the two serving services of ADR-016, one separating space, and a //! the two serving services of ADR-016, one separating space, and a
//! path-shaped argument — quoted (the vanilla wire shape, fully //! path-shaped argument: quoted (the vanilla wire shape, fully
//! single-quote-wrapped) or unquoted absolute (leading `/`). The //! single-quote-wrapped) or unquoted absolute (leading `/`). The
//! path is normalized: quotes stripped, leading `/` stripped, and //! path is normalized: quotes stripped, leading `/` stripped, and
//! **one trailing `.git` suffix stripped** — the vanilla-clone idiom //! **one trailing `.git` suffix stripped** (the vanilla-clone idiom
//! (real git sends the remote URL's path verbatim, bare clones carry //! (real git sends the remote URL's path verbatim, bare clones carry
//! `.git`; the alkgit registry grammar (`validate_repo_id`) excludes //! `.git`; the alkgit registry grammar (`validate_repo_id`) excludes
//! `.`, so the door normalizes the idiom rather than asking //! `.`, so the door normalizes the idiom rather than asking
//! registries for dotted keys; an empty remainder keeps `.git` and //! registries for dotted keys; an empty remainder keeps `.git` and
//! fails validation — never silently emptied). The normalized path //! fails validation, never silently emptied). The normalized path
//! is *not yet validated* — the git row (feature-gated) validates it //! is *not yet validated*: the git row (feature-gated) validates it
//! against `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100 //! against `alkgit::validate_repo_id` (1–2 segments, charset, ≤ 100
//! bytes) and hands the id through; **alkssh never resolves** (the //! bytes) and hands the id through; **alkssh never resolves** (the
//! no-existence-oracle posture lives in alkgit's collapsed generic //! no-existence-oracle posture lives in alkgit's collapsed generic
//! denial, and an id failing the *shape* gate is the git row's //! denial, and an id failing the *shape* gate is the git row's
//! `invalid-repo-id` decline — see below). Single-segment or //! `invalid-repo-id` decline; see below). Single-segment or
//! `.git`-empty shapes ride the same grammar. //! `.git`-empty shapes ride the same grammar.
//! - **`git-send-pack` / `git-upload-archive`** → //! - **`git-send-pack` / `git-upload-archive`** →
//! [`ExecRoute::Declined`]([`ExecDecline::UnknownService`]): the //! [`ExecRoute::Declined`]([`ExecDecline::UnknownService`]): the
//! known non-serving git services take the honest decline-with-reason //! known non-serving git services take the honest decline-with-reason
//! surface (POC-3 §2; ADR-016's fixed refusal — `git-upload-archive` //! surface (POC-3 §2; ADR-016's fixed refusal: `git-upload-archive`
//! is absent from serving by ADR-016 §1). Decision recorded //! is absent from serving by ADR-016 §1). Decision recorded
//! (`w3-git-mapping`): they do **not** ride the pipe row; the decline //! (`w3-git-mapping`): they do **not** ride the pipe row; the decline
//! is build-independent (no feature ever serves them). //! is build-independent (no feature ever serves them).
//! - **A serving service with no argument at all** (service-only) → //! - **A serving service with no argument at all** (service-only) →
//! [`ExecRoute::Declined`]([`ExecDecline::ServiceOnly`]): SSH exec //! [`ExecRoute::Declined`]([`ExecDecline::ServiceOnly`]): SSH exec
//! must carry the repo (POC-3 §A — a subsystem-style name cannot //! must carry the repo (POC-3 §A: a subsystem-style name cannot
//! dispatch, so exec without a repo cannot either). //! dispatch, so exec without a repo cannot either).
//! - **Everything else** → [`ExecRoute::Pipe`] (parse-then-fail): not //! - **Everything else** → [`ExecRoute::Pipe`] (parse-then-fail): not
//! git — including path-shaped-but-not-absolute unquoted arguments //! git, including path-shaped-but-not-absolute unquoted arguments
//! (`git-upload-pack rel`, exact strings the wave-2 probe pinned) //! (`git-upload-pack rel`, exact strings the wave-2 probe pinned)
//! and unterminated quotes; the probe's routing verdicts are //! and unterminated quotes; the probe's routing verdicts are
//! preserved verbatim. //! preserved verbatim.
//! //!
//! ## The wire vocabulary (decision, recorded) //! ## The wire vocabulary (decision, recorded)
//! //!
//! SSH request replies carry no failure text, so — except for the open //! SSH request replies carry no failure text, so, except for the open
//! level — the *reason* lives in the event vocabulary //! level, the *reason* lives in the event vocabulary
//! ([`crate::observe`]), not on the wire. Decision: the grammar //! ([`crate::observe`]), not on the wire. Decision: the grammar
//! declinables (`unknown-service`, `service-only`, and the git row's //! declinables (`unknown-service`, `service-only`, and the git row's
//! `invalid-repo-id`) share the **request-level reply shape** with //! `invalid-repo-id`) share the **request-level reply shape** with
//! parse-then-fail — `channel_failure`, channel left usable (the //! parse-then-fail: `channel_failure`, channel left usable (the
//! POC-1 §5 session-survives-declines posture; POC-3's decline shape), //! POC-1 §5 session-survives-declines posture; POC-3's decline shape),
//! distinguishable only in the events. The *serving-path* shape //! distinguishable only in the events. The *serving-path* shape
//! (failure + close, the spawned task's) stays reserved for declined //! (failure + close, the spawned task's) stays reserved for declined
//! rows that owned the channel: the seam-unconfigured git row and the //! rows that owned the channel: the seam-unconfigured git row and the
//! real serve's own outcomes (the denial band alkgit writes before any //! real serve's own outcomes (the denial band alkgit writes before any
//! protocol byte, then exit-status + close — the wire shapes the //! protocol byte, then exit-status + close; the wire shapes the
//! `git_map` module pins). //! `git_map` module pins).
use russh::ChannelOpenFailure; use russh::ChannelOpenFailure;
@@ -80,7 +80,7 @@ use russh::ChannelOpenFailure;
/// `w3-git-mapping` grammar with the declined-with-reason class). /// `w3-git-mapping` grammar with the declined-with-reason class).
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExecRoute { pub enum ExecRoute {
/// A git-parseable command — routes to the git row. The repo path /// A git-parseable command; it routes to the git row. The repo path
/// is the normalized argument grammar (quotes, leading `/`, one /// is the normalized argument grammar (quotes, leading `/`, one
/// trailing `.git`); validation (`alkgit::validate_repo_id`) is /// trailing `.git`); validation (`alkgit::validate_repo_id`) is
/// the git row's gate (the `crate::git_map` module). /// the git row's gate (the `crate::git_map` module).
@@ -88,35 +88,35 @@ pub enum ExecRoute {
service: GitServiceRoute, service: GitServiceRoute,
repo: String, repo: String,
}, },
/// Everything else — routes to the session pipe row (the /// Everything else: routes to the session pipe row (the
/// `session` feature gate is the row's other half; unfeatured /// `session` feature gate is the row's other half; unfeatured
/// builds decline here). /// builds decline here).
Pipe, Pipe,
/// A command the grammar recognizes as git-transport-shaped but /// A command the grammar recognizes as git-transport-shaped but
/// that can never dispatch on this door — declined with a reason, /// that can never dispatch on this door; it is declined with a reason,
/// honestly and build-independently (see the grammar section). /// honestly and build-independently (see the grammar section).
Declined(ExecDecline), Declined(ExecDecline),
} }
/// The git service the exec grammar recognized — the ADR-016 wire /// The git service the exec grammar recognized: the ADR-016 wire
/// strings, as the door-side form of `alkgit::Service` (the seam maps /// strings, as the door-side form of `alkgit::Service` (the seam maps
/// them verbatim; this type keeps the parse core feature-free). /// them verbatim; this type keeps the parse core feature-free).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitServiceRoute { pub enum GitServiceRoute {
/// `git-upload-pack` — fetch. /// `git-upload-pack`: fetch.
UploadPack, UploadPack,
/// `git-receive-pack` — push. /// `git-receive-pack`: push.
ReceivePack, ReceivePack,
} }
/// A git-shaped command that can never dispatch — the decline reasons /// A git-shaped command that can never dispatch: the decline reasons
/// the grammar itself decides (the git row adds `invalid-repo-id` at /// the grammar itself decides (the git row adds `invalid-repo-id` at
/// the validation gate; that one needs `alkgit`'s grammar, this type /// the validation gate; that one needs `alkgit`'s grammar, this type
/// is the parse core's). /// is the parse core's).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecDecline { pub enum ExecDecline {
/// A known non-serving git service arrived on the wire: /// A known non-serving git service arrived on the wire:
/// `git-send-pack` / `git-upload-archive` (POC-3 §2 — the honest /// `git-send-pack` / `git-upload-archive` (POC-3 §2, the honest
/// surface; ADR-016's refusal). /// surface; ADR-016's refusal).
UnknownService { service: &'static str }, UnknownService { service: &'static str },
/// A serving service name with no repo argument at all (POC-3 /// A serving service name with no repo argument at all (POC-3
@@ -126,7 +126,7 @@ pub enum ExecDecline {
impl ExecDecline { impl ExecDecline {
/// The event-vocabulary reason for this decline (§the wire /// The event-vocabulary reason for this decline (§the wire
/// vocabulary — `&'static str` values only; no wire-carried /// vocabulary: `&'static str` values only; no wire-carried
/// service token ever rides an event). /// service token ever rides an event).
pub(crate) fn reason(self) -> &'static str { pub(crate) fn reason(self) -> &'static str {
match self { match self {
@@ -136,7 +136,7 @@ impl ExecDecline {
} }
} }
/// The exec-grammar parse — the wave-2 `classify_exec` probe's /// The exec-grammar parse: the wave-2 `classify_exec` probe's
/// replacement, its unit rows migrated (see the grammar section for /// replacement, its unit rows migrated (see the grammar section for
/// the exact shape rules). /// the exact shape rules).
pub fn parse_exec(command: &str) -> ExecRoute { pub fn parse_exec(command: &str) -> ExecRoute {
@@ -174,13 +174,13 @@ pub fn parse_exec(command: &str) -> ExecRoute {
} }
} }
/// The grammar's construction arm (`w4-op-rows`) — the client /// The grammar's construction arm (`w4-op-rows`), the client
/// direction's mirror of [`parse_exec`]: the git route + repo id → /// direction's mirror of [`parse_exec`]: the git route + repo id →
/// the vanilla wire command string, byte-for-byte the shape the /// the vanilla wire command string, byte-for-byte the shape the
/// grammar parses back (`git-{upload,receive}-pack '/<repo>'` — the /// grammar parses back (`git-{upload,receive}-pack '/<repo>'`, the
/// fully-single-quote-wrapped path with the leading `/`, POC-3 §A's /// fully-single-quote-wrapped path with the leading `/`, POC-3 §A's
/// verified shape). The client row reuses — never forks — the /// verified shape). The client row reuses the grammar, never forks
/// grammar: it hands the constructed command back through /// it: it hands the constructed command back through
/// [`parse_exec`] before any wire op, so the shared verdicts (the /// [`parse_exec`] before any wire op, so the shared verdicts (the
/// per-length cap, the decline classes) ride both arms /// per-length cap, the decline classes) ride both arms
/// (`client.md` §op rows: "the same exec grammar the server side /// (`client.md` §op rows: "the same exec grammar the server side
@@ -195,10 +195,10 @@ pub(crate) fn build_git_exec_command(service: GitServiceRoute, repo_id: &str) ->
} }
/// The argument-grammar stage: a path-shaped argument (fully wrapped /// The argument-grammar stage: a path-shaped argument (fully wrapped
/// in single quotes — the vanilla wire shape — or unquoted absolute; /// in single quotes, the vanilla wire shape, or unquoted absolute;
/// anything else is not git) normalized to the repo path: quotes /// anything else is not git) normalized to the repo path: quotes
/// stripped, leading `/` stripped, one trailing `.git` stripped (the /// stripped, leading `/` stripped, one trailing `.git` stripped (the
/// recorded normalization — see the module grammar section; an /// recorded normalization; see the module grammar section; an
/// empty-after-normalized id is *kept* for the git row's validation /// empty-after-normalized id is *kept* for the git row's validation
/// to decline as `invalid-repo-id`, never silently emptied). /// to decline as `invalid-repo-id`, never silently emptied).
fn normalize_repo_path(argument: &str) -> Option<String> { fn normalize_repo_path(argument: &str) -> Option<String> {
@@ -222,10 +222,10 @@ fn normalize_repo_path(argument: &str) -> Option<String> {
Some(normalized.to_owned()) Some(normalized.to_owned())
} }
/// The wire decline reason for a **featureless** row — a known channel /// The wire decline reason for a **featureless** row: a known channel
/// type this build serves nothing of (unfeatured or never-feature): /// type this build serves nothing of (unfeatured or never-feature):
/// `administratively prohibited`, the vanilla-observable policy /// `administratively prohibited`, the vanilla-observable policy
/// refusal (POC-1 §5 — channel-scoped, auth intact, no hangs). A /// refusal (POC-1 §5: channel-scoped, auth intact, no hangs). A
/// never-feature (`x11`, auth-agent) and an unfeatured feature row /// never-feature (`x11`, auth-agent) and an unfeatured feature row
/// share this class honestly: without the feature there is no /// share this class honestly: without the feature there is no
/// capability to configure, so nothing distinguishable exists. /// capability to configure, so nothing distinguishable exists.
@@ -234,14 +234,14 @@ pub(crate) fn unfeatured_open_decline() -> ChannelOpenFailure {
} }
/// The wire decline reason for a **cannot-bridge** tunnel open /// The wire decline reason for a **cannot-bridge** tunnel open
/// (`w3-tunnel-direct` — the wave-2 seam stub's reason surviving as /// (`w3-tunnel-direct`, the wave-2 seam stub's reason surviving as
/// the real decline): `connect failed`, the explicit reject shape /// the real decline): `connect failed`, the explicit reject shape
/// ADR-006 decision 1 pins — accept only after the fabric open /// ADR-006 decision 1 pins: accept only after the fabric open
/// succeeds, and a fabric-open failure maps to this explicit `SSH /// succeeds, and a fabric-open failure maps to this explicit `SSH
/// `ConnectFailed` (the channel never establishes, so the client /// `ConnectFailed` (the channel never establishes, so the client
/// observes a channel-scoped refusal, no hang). The same code covers /// observes a channel-scoped refusal, no hang). The same code covers
/// the other cannot-bridge classes of the direct rows (the fabric /// the other cannot-bridge classes of the direct rows (the fabric
/// entry unconfigured under the feature — the unset-declines posture — /// entry unconfigured under the feature (the unset-declines posture),
/// and wire arguments naming an impossible target; a vanilla client /// and wire arguments naming an impossible target; a vanilla client
/// cannot distinguish these from a dead target, and the event /// cannot distinguish these from a dead target, and the event
/// vocabulary records which). Policy refusals stay /// vocabulary records which). Policy refusals stay
@@ -252,7 +252,7 @@ pub(crate) fn connect_failed_open_decline() -> ChannelOpenFailure {
/// The wire decline reason for an open beyond the **in-crate /// The wire decline reason for an open beyond the **in-crate
/// concurrent-channel cap** (`overview.md` §Bounded-resources policy, /// concurrent-channel cap** (`overview.md` §Bounded-resources policy,
/// `w2-bounded-resources`): `administratively prohibited` — the cap is /// `w2-bounded-resources`): `administratively prohibited`; the cap is
/// the assembly's own configured policy ceiling, refused fail-closed /// the assembly's own configured policy ceiling, refused fail-closed
/// at the open (never queued, never half-open). OpenSSH's own /// at the open (never queued, never half-open). OpenSSH's own
/// `MaxSessions` ceiling answers the same code, so vanilla clients /// `MaxSessions` ceiling answers the same code, so vanilla clients
@@ -281,8 +281,8 @@ mod tests {
git(command).1 git(command).1
} }
/// - *Contract stamp:* ADR-006 decision 2 (the exec-only git parse /// - *Contract stamp:* ADR-006 decision 2 (the exec-only git parse:
/// — the vanilla wire shape `git-upload-pack '/owner/demo'`). /// the vanilla wire shape `git-upload-pack '/owner/demo'`).
#[test] #[test]
fn vanilla_quoted_upload_pack_parse_is_git() { fn vanilla_quoted_upload_pack_parse_is_git() {
let (service, repo) = git("git-upload-pack '/owner/demo'"); let (service, repo) = git("git-upload-pack '/owner/demo'");
@@ -316,16 +316,16 @@ mod tests {
assert_eq!( assert_eq!(
git_repo("git-upload-pack '/r.git.git'"), git_repo("git-upload-pack '/r.git.git'"),
"r.git", "r.git",
"one suffix only — the remainder fails validation honestly" "one suffix only; the remainder fails validation honestly"
); );
assert_eq!( assert_eq!(
git_repo("git-upload-pack '/.git'"), git_repo("git-upload-pack '/.git'"),
".git", ".git",
"the empty remainder is kept — the git row declines it, never a silent emptying" "the empty remainder is kept (the git row declines it, never a silent emptying)"
); );
} }
/// - *Contract stamp:* POC-3 §2 (service-only → declined — SSH /// - *Contract stamp:* POC-3 §2 (service-only → declined: SSH
/// exec must carry the repo; the decline-with-reason surface, /// exec must carry the repo; the decline-with-reason surface,
/// not the pipe row). /// not the pipe row).
#[test] #[test]
@@ -341,7 +341,7 @@ mod tests {
} }
/// - *Contract stamp:* POC-3 §2 (unknown services declined /// - *Contract stamp:* POC-3 §2 (unknown services declined
/// honestly — the decision that they take the decline surface, /// honestly; the decision that they take the decline surface,
/// not the pipe row). /// not the pipe row).
#[test] #[test]
fn unknown_services_parse_to_the_decline() { fn unknown_services_parse_to_the_decline() {
@@ -360,7 +360,7 @@ mod tests {
} }
/// - *Contract stamp:* mapping.md §Session-channel dispatch /// - *Contract stamp:* mapping.md §Session-channel dispatch
/// (non-git → parse-then-fail, the pipe row — the wave-2 probe's /// (non-git → parse-then-fail, the pipe row; the wave-2 probe's
/// pinned routing verdicts, preserved). /// pinned routing verdicts, preserved).
#[test] #[test]
fn non_git_commands_parse_to_the_pipe_row() { fn non_git_commands_parse_to_the_pipe_row() {
@@ -378,7 +378,7 @@ mod tests {
} }
} }
/// Unterminated empty quotes are not the vanilla path shape — the /// Unterminated empty quotes are not the vanilla path shape; the
/// probe's pipe verdict applies. A quoted path without the leading /// probe's pipe verdict applies. A quoted path without the leading
/// slash is still path-shape (the POC's loose quote trim); it rides /// slash is still path-shape (the POC's loose quote trim); it rides
/// the grammar to the git row's validation. /// the grammar to the git row's validation.
@@ -390,7 +390,7 @@ mod tests {
} }
/// Quoted-empty and slash-only arguments parse as git with an empty /// Quoted-empty and slash-only arguments parse as git with an empty
/// normalized id — the git row's validation declines them as /// normalized id; the git row's validation declines them as
/// `invalid-repo-id` (kept, never silently emptied). /// `invalid-repo-id` (kept, never silently emptied).
#[test] #[test]
fn empty_ids_parse_git_and_the_row_downstream_declines() { fn empty_ids_parse_git_and_the_row_downstream_declines() {
@@ -399,7 +399,7 @@ mod tests {
} }
/// - *Contract stamp:* mapping.md §Session-channel dispatch (deep /// - *Contract stamp:* mapping.md §Session-channel dispatch (deep
/// paths ride the grammar to the git row — validation is the /// paths ride the grammar to the git row; validation is the
/// registry-grammar gate downstream, e.g. 3 segments decline as /// registry-grammar gate downstream, e.g. 3 segments decline as
/// `invalid-repo-id`, not at the parse). /// `invalid-repo-id`, not at the parse).
#[test] #[test]
@@ -418,7 +418,7 @@ mod tests {
/// - *Contract stamp:* `client.md` §op rows (the same exec /// - *Contract stamp:* `client.md` §op rows (the same exec
/// grammar the server side parses, reused not forked); /// grammar the server side parses, reused not forked);
/// ADR-006 decision 2 — the construction arm's command parses /// ADR-006 decision 2: the construction arm's command parses
/// back through [`parse_exec`] to exactly the route it was /// back through [`parse_exec`] to exactly the route it was
/// built from, for both services and every grammar shape. /// built from, for both services and every grammar shape.
#[cfg(feature = "git")] #[cfg(feature = "git")]
@@ -451,7 +451,7 @@ mod tests {
} }
} }
/// - *Contract stamp:* the construction arm is grammar-loyal — /// - *Contract stamp:* the construction arm is grammar-loyal:
/// it is only ever reachable for the two *serving* services /// it is only ever reachable for the two *serving* services
/// (the type is the gate); the wire shape is fully /// (the type is the gate); the wire shape is fully
/// single-quote-wrapped with the leading `/`. /// single-quote-wrapped with the leading `/`.
+65 -65
View File
@@ -1,25 +1,25 @@
//! The crate's error taxonomy — fail-closed, no catch-alls (ADR-005 //! The crate's error taxonomy: fail-closed, no catch-alls (ADR-005
//! decision 4: the vault's error taxonomy is the adapter's to reshape, //! decision 4: the vault's error taxonomy is the adapter's to reshape,
//! never to collapse). //! never to collapse).
//! //!
//! Deliberately **not** error types, per the family contracts: //! Deliberately **not** error types, per the family contracts:
//! //!
//! - **Auth declines** — dispositions (`Auth::Reject`, ADR-004), //! - **Auth declines**: dispositions (`Auth::Reject`, ADR-004),
//! never errors; a decline is an authorized outcome, not a failure. //! never errors; a decline is an authorized outcome, not a failure.
//! - **Wire decline reasons** — SSH-shaped responses to the client //! - **Wire decline reasons**: SSH-shaped responses to the client
//! (the wave-2 decline core's material), never this taxonomy. //! (the wave-2 decline core's material), never this taxonomy.
//! - **Config validation** — the OQ-08/serve-config fields are //! - **Config validation**: the OQ-08/serve-config fields are
//! `Option` and decline at request time, not construction time; no //! `Option` and decline at request time, not construction time; no
//! config-error type exists unless a need is found. //! config-error type exists unless a need is found.
use thiserror::Error; use thiserror::Error;
/// Every way a vault-backed key source can fail to produce key /// Every way a vault-backed key source can fail to produce key
/// material (ADR-005 decision 4 — the failure matrix as distinct /// material (ADR-005 decision 4: the failure matrix as distinct
/// variants). All paths are fail-closed: a failed load never yields a /// variants). All paths are fail-closed: a failed load never yields a
/// dial, and a failed host-key load never yields a server. /// dial, and a failed host-key load never yields a server.
/// ///
/// The vault itself is a secret store on this path — the error /// The vault itself is a secret store on this path; the error
/// taxonomy here is the adapter's reshaping of the vault's, never a /// taxonomy here is the adapter's reshaping of the vault's, never a
/// re-export of it. Variants are deliberately closed: no catch-all, /// re-export of it. Variants are deliberately closed: no catch-all,
/// no `Other`, no `Unknown`. /// no `Other`, no `Unknown`.
@@ -28,14 +28,14 @@ pub enum KeySourceError {
/// The vault was locked when the key was requested. /// The vault was locked when the key was requested.
/// ///
/// ADR-005 semantics: the vault participates only at use time /// ADR-005 semantics: the vault participates only at use time
/// (decrypt-at-use), so a locked vault blocks exactly that — no /// (decrypt-at-use), so a locked vault blocks exactly that: no
/// key material can be decrypted, so there is no dial; on the /// key material can be decrypted, so there is no dial; on the
/// host-key path the load feeds server construction /// host-key path the load feeds server construction
/// (decrypt-at-construction), so no server is constructed /// (decrypt-at-construction), so no server is constructed
/// (POC-2 §5). Live sessions are unaffected: the decoded /// (POC-2 §5). Live sessions are unaffected: the decoded
/// `PrivateKey` is a dial's lifetime, not a vault-coupled lease. /// `PrivateKey` is a dial's lifetime, not a vault-coupled lease.
/// ///
/// Fail-closed consequence: operation refused. Never unlock — /// Fail-closed consequence: operation refused. Never unlock;
/// the vault's lifecycle is the assembly's, never the adapter's. /// the vault's lifecycle is the assembly's, never the adapter's.
#[error("vault is locked; no key material available until unlocked")] #[error("vault is locked; no key material available until unlocked")]
Locked, Locked,
@@ -43,7 +43,7 @@ pub enum KeySourceError {
/// No blob exists at the requested key location. /// No blob exists at the requested key location.
/// ///
/// ADR-005 semantics: the assembly source has no /// ADR-005 semantics: the assembly source has no
/// `EncryptedData` record for the requested key — no vault call /// `EncryptedData` record for the requested key, so no vault call
/// is needed to know (POC-2 §3). Distinct from every other /// is needed to know (POC-2 §3). Distinct from every other
/// variant on purpose: an absent key is a configuration/ /// variant on purpose: an absent key is a configuration/
/// registration gap, while a locked/decryption failure is a /// registration gap, while a locked/decryption failure is a
@@ -55,11 +55,11 @@ pub enum KeySourceError {
#[error("no key material at the requested key location")] #[error("no key material at the requested key location")]
Absent, Absent,
/// Decryption failed — wrong mnemonic or tampered ciphertext. /// Decryption failed: wrong mnemonic or tampered ciphertext.
/// ///
/// ADR-005 semantics: the AES-256-GCM open failed (POC-2 §3): /// ADR-005 semantics: the AES-256-GCM open failed (POC-2 §3):
/// the mnemonic derives an unrelated key, or the ciphertext was /// the mnemonic derives an unrelated key, or the ciphertext was
/// altered in flight — the refuse-at-crypto class. The vault /// altered in flight (the refuse-at-crypto class). The vault
/// reports this class as opaque message text, which is the /// reports this class as opaque message text, which is the
/// honest content here. /// honest content here.
/// ///
@@ -71,31 +71,31 @@ pub enum KeySourceError {
/// The record's key version is one the vault cannot resolve. /// The record's key version is one the vault cannot resolve.
/// ///
/// ADR-005 semantics: the vault declines *before* touching /// ADR-005 semantics: the vault declines *before* touching
/// crypto — an unknown/unresolvable `key_version` (reserved/legacy /// crypto: an unknown/unresolvable `key_version` (reserved/legacy
/// v1 is the TS_PBKDF2 class; a future version the vault predates /// v1 is the TS_PBKDF2 class; a future version the vault predates
/// is the other class) is refused at derivation-path resolution /// is the other class) is refused at derivation-path resolution
/// (POC-2 §3). `EncryptedData` remains opaque: the crate never /// (POC-2 §3). `EncryptedData` remains opaque: the crate never
/// interprets versions — this variant carries only the vault's /// interprets versions; this variant carries only the vault's
/// refusal message. /// refusal message.
/// ///
/// Fail-closed consequence: no dial; on the host-key path, no /// Fail-closed consequence: no dial; on the host-key path, no
/// server construction. Rotation (higher version the vault /// server construction. Rotation (higher version the vault
/// supports) is the storage-side migration — a version the /// supports) is the storage-side migration; a version the
/// vault cannot derive never becomes a partially usable key. /// vault cannot derive never becomes a partially usable key.
#[error("vault cannot resolve the record's version: {0}")] #[error("vault cannot resolve the record's version: {0}")]
UnknownKeyVersion(String), UnknownKeyVersion(String),
/// The decrypted plaintext arrived but is not decodable key /// The decrypted plaintext arrived but is not decodable key
/// material — the one-time [`decode_secret_key`](russh::keys::decode_secret_key) /// material, that is, the one-time [`decode_secret_key`](russh::keys::decode_secret_key)
/// failure. /// failure.
/// ///
/// ADR-005 semantics: the vault decrypted *something* /// ADR-005 semantics: the vault decrypted *something*
/// successfully and the plaintext is not an OpenSSH /// successfully and the plaintext is not an OpenSSH
/// private-key PEM (decision 1's inner format). Distinct from /// private-key PEM (decision 1's inner format). Distinct from
/// [`KeySourceError::Decryption`] on purpose — the /// [`KeySourceError::Decryption`] on purpose: the
/// custody boundary held, what was guarded is not a key. The /// custody boundary held, what was guarded is not a key. The
/// underlying parse error's message is deliberately *not* /// underlying parse error's message is deliberately *not*
/// carried — the machinery's text can echo plaintext-shaped /// carried, because the machinery's text can echo plaintext-shaped
/// material (the `w5-fuzz-harness` finding: the `ssh-key` PPK /// material (the `w5-fuzz-harness` finding: the `ssh-key` PPK
/// path interpolates the raw header), so the payload is /// path interpolates the raw header), so the payload is
/// crate-owned fixed text; the RC-pinned `ssh-key` error type /// crate-owned fixed text; the RC-pinned `ssh-key` error type
@@ -107,24 +107,24 @@ pub enum KeySourceError {
Decode(String), Decode(String),
} }
/// The adapter (server-direction) error surface — the failure classes /// The adapter (server-direction) error surface, the failure classes
/// the mount point can reach, deliberately closed: /// the mount point can reach, deliberately closed:
/// ///
/// - **Construction** — the [`SshAdapter::new`](crate::SshAdapter::new) /// - **Construction**: the [`SshAdapter::new`](crate::SshAdapter::new)
/// setup failed *before any session exists*: a failed host-key load /// setup failed *before any session exists*: a failed host-key load
/// (the [`KeySourceError`] variant is preserved verbatim — locked / /// (the [`KeySourceError`] variant is preserved verbatim: locked /
/// absent / decryption / unknown-version / decode each stays itself) /// absent / decryption / unknown-version / decode each stays itself)
/// or a refused `-R` listen-leg registration (`ListenRegistration`, /// or a refused `-R` listen-leg registration (`ListenRegistration`,
/// `w3-tunnel-listen` — a fabric that could mint listens the door /// `w3-tunnel-listen`; a fabric that could mint listens the door
/// cannot serve is worse than a half-configured adapter). /// cannot serve is worse than a half-configured adapter).
/// **No adapter is constructed** (ADR-005 decision 6 — never a /// **No adapter is constructed** (ADR-005 decision 6: never a
/// half-configured adapter, never a keyless server that fails /// half-configured adapter, never a keyless server that fails
/// later). /// later).
/// - **Wire** — the russh session stack's own error, mapped /// - **Wire**: the russh session stack's own error, mapped
/// transparently (`From<russh::Error>`, the wave-1 error-model's /// transparently (`From<russh::Error>`, the wave-1 error-model's
/// reserved `From` requirement of the per-connection /// reserved `From` requirement of the per-connection
/// [`russh::server::Handler`]): KEX, transport, and IO failures of /// [`russh::server::Handler`]): KEX, transport, and IO failures of
/// a mounted session. Auth declines are *not* this class — they are /// a mounted session. Auth declines are *not* this class; they are
/// dispositions (ADR-004), and wire decline reasons are the /// dispositions (ADR-004), and wire decline reasons are the
/// dispatch's SSH-shaped responses, never this taxonomy. /// dispatch's SSH-shaped responses, never this taxonomy.
/// ///
@@ -132,7 +132,7 @@ pub enum KeySourceError {
/// direction can fail is one of these classes or a decline. /// direction can fail is one of these classes or a decline.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AdapterError { pub enum AdapterError {
/// The host-key load at adapter construction refused — fail-closed /// The host-key load at adapter construction refused, fail-closed
/// (ADR-005 decision 6). The [`KeySourceError`] variant is the /// (ADR-005 decision 6). The [`KeySourceError`] variant is the
/// original refusal class, preserved un-collapsed. /// original refusal class, preserved un-collapsed.
#[error("ssh server construction refused: {0}")] #[error("ssh server construction refused: {0}")]
@@ -140,14 +140,14 @@ pub enum AdapterError {
/// The `-R` listen leg's door-side openable registration refused /// The `-R` listen leg's door-side openable registration refused
/// at construction (`w3-tunnel-listen`): fail-closed, never a /// at construction (`w3-tunnel-listen`): fail-closed, never a
/// half-configured adapter — a construction where the fabric could /// half-configured adapter: a construction where the fabric could
/// mint listens the door cannot serve is worse than no adapter. /// mint listens the door cannot serve is worse than no adapter.
/// The string carries the registration failure detail. /// The string carries the registration failure detail.
/// ///
/// This stays the construction-time class: the wire-time sibling /// This stays the construction-time class: the wire-time sibling
/// (`w4-router-collision`, the `ListenRouter::register` /// (`w4-router-collision`, the `ListenRouter::register`
/// token-collision refusal) is a request-time **decline /// token-collision refusal) is a request-time **decline
/// disposition** (`RequestDenied`, reason `token-collision`) — /// disposition** (`RequestDenied`, reason `token-collision`);
/// a global-request hook's `Err` (this taxonomy's only route into /// a global-request hook's `Err` (this taxonomy's only route into
/// the request surface) would kill the whole russh session, and /// the request surface) would kill the whole russh session, and
/// wire decline reasons are never this taxonomy. /// wire decline reasons are never this taxonomy.
@@ -160,53 +160,53 @@ pub enum AdapterError {
} }
/// The dial surface's failure classes (the client direction's error /// The dial surface's failure classes (the client direction's error
/// counterpart of [`AdapterError`]) — deliberately closed, every /// counterpart of [`AdapterError`]), deliberately closed, every
/// variant a no-connection outcome (ADR-005 decision 4: failures are /// variant a no-connection outcome (ADR-005 decision 4: failures are
/// fail-closed and distinct, never collapsed, never a fallback dial): /// fail-closed and distinct, never collapsed, never a fallback dial):
/// ///
/// - **KeySource** — the decrypt-at-dial load refused. The /// - **KeySource**: the decrypt-at-dial load refused. The
/// [`KeySourceError`] variant is preserved verbatim (locked / absent /// [`KeySourceError`] variant is preserved verbatim (locked / absent
/// / decryption / unknown-version / decode each stays itself — the /// / decryption / unknown-version / decode each stays itself, the
/// ADR-005 decision 4 matrix read through the dial seam). No key /// ADR-005 decision 4 matrix read through the dial seam). No key
/// material, no dial. /// material, no dial.
/// - **Address** — the dial target is not a parseable `host:port`. /// - **Address**: the dial target is not a parseable `host:port`.
/// Checked at dial time (the config carries no construction-time /// Checked at dial time (the config carries no construction-time
/// validation, the family contract); a malformed target is a /// validation, the family contract); a malformed target is a
/// refusal before any connection. /// refusal before any connection.
/// - **HostKeyRejected** — the injected host-key policy /// - **HostKeyRejected**: the injected host-key policy
/// ([`crate::HostKeyPolicy`]) declined the presented server key at /// ([`crate::HostKeyPolicy`]) declined the presented server key at
/// `check_server_key` (a `false` verdict is the decline /// `check_server_key` (a `false` verdict is the decline
/// disposition; russh surfaces it as its `UnknownKey` wire error, /// disposition; russh surfaces it as its `UnknownKey` wire error,
/// which this crate maps here because that hook is the only /// which this crate maps here because that hook is the only
/// producer). The trust decision is the policy's — its own record /// producer). The trust decision is the policy's; its own record
/// (its decision log) carries the reasons; the crate never /// (its decision log) carries the reasons; the crate never
/// second-guesses or retries. /// second-guesses or retries.
/// - **AuthRejected** — the remote end refused the publickey /// - **AuthRejected**: the remote end refused the publickey
/// authentication (the family contract is exactly one /// authentication (the family contract is exactly one
/// publickey-only attempt; there is no retry, no fallback method). /// publickey-only attempt; there is no retry, no fallback method).
/// - **Wire** — the russh session stack's own error, mapped /// - **Wire**: the russh session stack's own error, mapped
/// transparently, including the dial's connect/resolve/IO classes. /// transparently, including the dial's connect/resolve/IO classes.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum DialError { pub enum DialError {
/// The dial key's decrypt-at-dial load refused — every /// The dial key's decrypt-at-dial load refused; every
/// [`KeySourceError`] variant is a no-dial outcome, preserved /// [`KeySourceError`] variant is a no-dial outcome, preserved
/// un-collapsed (ADR-005 decision 4). /// un-collapsed (ADR-005 decision 4).
#[error("dial key source refused: {0}")] #[error("dial key source refused: {0}")]
KeySource(#[from] KeySourceError), KeySource(#[from] KeySourceError),
/// The dial target is not a parseable `host:port` address — the /// The dial target is not a parseable `host:port` address, so the
/// wire never dialed. The string carries the address as given. /// wire never dialed. The string carries the address as given.
#[error("dial target is not a usable host:port address: {0}")] #[error("dial target is not a usable host:port address: {0}")]
Address(String), Address(String),
/// The host-key policy declined the presented server key — the /// The host-key policy declined the presented server key, so the
/// dial aborts (a disposition the dial cannot proceed on; the /// dial aborts (a disposition the dial cannot proceed on; the
/// crate default [`RejectAll`](crate::RejectAll) policy makes /// crate default [`RejectAll`](crate::RejectAll) policy makes
/// every dial refuse, by design). /// every dial refuse, by design).
#[error("host-key policy declined the presented server key")] #[error("host-key policy declined the presented server key")]
HostKeyRejected, HostKeyRejected,
/// The remote end refused the publickey authentication — the /// The remote end refused the publickey authentication; this is
/// family's one-attempt publickey-only posture (no retries, no /// family's one-attempt publickey-only posture (no retries, no
/// fallback methods, never another key). /// fallback methods, never another key).
#[error("authentication was refused by the remote end")] #[error("authentication was refused by the remote end")]
@@ -224,7 +224,7 @@ impl From<russh::Error> for DialError {
/// The transparent russh mapping with one semantic upgrade: /// The transparent russh mapping with one semantic upgrade:
/// `UnknownKey` is the wire shape of a host-key policy decline /// `UnknownKey` is the wire shape of a host-key policy decline
/// (the client's `check_server_key` hook is the only producer), /// (the client's `check_server_key` hook is the only producer),
/// so it becomes [`DialError::HostKeyRejected`] — every other /// so it becomes [`DialError::HostKeyRejected`]; every other
/// error stays the transparent [`DialError::Wire`] class. /// error stays the transparent [`DialError::Wire`] class.
fn from(error: russh::Error) -> Self { fn from(error: russh::Error) -> Self {
match error { match error {
@@ -235,22 +235,22 @@ impl From<russh::Error> for DialError {
} }
/// The family-typed mirror of the SSH `CHANNEL_OPEN` rejection /// The family-typed mirror of the SSH `CHANNEL_OPEN` rejection
/// reasons (RFC 4250 §1.5 / RFC 4254 §5.2) — the op rows translate /// reasons (RFC 4250 §1.5 / RFC 4254 §5.2); the op rows translate
/// the wire's typed-open decline into this closed vocabulary /// the wire's typed-open decline into this closed vocabulary
/// (`client.md` §op rows: the family surface carries no vanilla types). /// (`client.md` §op rows: the family surface carries no vanilla types).
/// The mapping is one-to-one with russh's /// The mapping is one-to-one with russh's
/// [`russh::ChannelOpenFailure`], whose `Other` arm mirrors its /// [`russh::ChannelOpenFailure`], whose `Other` arm mirrors its
/// `code`/`reason` pair verbatim — a decline reason the client /// `code`/`reason` pair verbatim, so a decline reason the client
/// *could not map* is never silently collapsed onto a known class. /// *could not map* is never silently collapsed onto a known class.
#[derive(Debug, Clone, Error, PartialEq, Eq)] #[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum OpenDeclineReason { pub enum OpenDeclineReason {
/// The SSH `administratively prohibited` class — a policy /// The SSH `administratively prohibited` class: a policy
/// refusal at the server (the door's own unfeatured/cap /// refusal at the server (the door's own unfeatured/cap
/// declines answer with this family's server-direction mirror). /// declines answer with this family's server-direction mirror).
#[error("the server refused the open (administratively prohibited)")] #[error("the server refused the open (administratively prohibited)")]
Prohibited, Prohibited,
/// The SSH `connect failed` class — the target could not be /// The SSH `connect failed` class: the target could not be
/// bridged (a dead `-L` target, an impossible one). /// bridged (a dead `-L` target, an impossible one).
#[error("the server rejected the open (connect failed)")] #[error("the server rejected the open (connect failed)")]
ConnectFailed, ConnectFailed,
@@ -263,7 +263,7 @@ pub enum OpenDeclineReason {
#[error("the server rejected the open (resource shortage)")] #[error("the server rejected the open (resource shortage)")]
ResourceShortage, ResourceShortage,
/// A rejection reason with no standard code — the wire's own /// A rejection reason with no standard code: the wire's own
/// `code`/`reason` pair rides (the mirror never invents a class /// `code`/`reason` pair rides (the mirror never invents a class
/// for an unmappable one). /// for an unmappable one).
#[error("the server rejected the open (code {code}: {reason})")] #[error("the server rejected the open (code {code}: {reason})")]
@@ -283,12 +283,12 @@ impl From<russh::ChannelOpenFailure> for OpenDeclineReason {
} }
} }
/// The client direction's op-row failure classes (`w4-op-rows`) — /// The client direction's op-row failure classes (`w4-op-rows`),
/// the exec/pty / tunnel / git-pack op surfaces' closed taxonomy /// the exec/pty / tunnel / git-pack op surfaces' closed taxonomy
/// (`RemoteSession` / `TunnelChannel` / `GitPackSession` — the /// (`RemoteSession` / `TunnelChannel` / `GitPackSession`, the
/// feature-gated members live in the crate root's exports), deliberately distinct from [`DialError`]: /// feature-gated members live in the crate root's exports), deliberately distinct from [`DialError`]:
/// an op runs on an *already-dialled* connection, so its failure is /// an op runs on an *already-dialled* connection, so its failure is
/// never a no-connection outcome — the connection stays usable /// never a no-connection outcome: the connection stays usable
/// ([`OpError::OpenDeclined`] / [`OpError::RequestRefused`] and /// ([`OpError::OpenDeclined`] / [`OpError::RequestRefused`] and
/// [`OpError::ChannelEnded`] are channel/op-scoped, mirroring the /// [`OpError::ChannelEnded`] are channel/op-scoped, mirroring the
/// server direction's decline shapes: channel-scoped, auth intact, /// server direction's decline shapes: channel-scoped, auth intact,
@@ -299,7 +299,7 @@ impl From<russh::ChannelOpenFailure> for OpenDeclineReason {
/// ///
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum OpError { pub enum OpError {
/// The server refused the op's **channel open** — the typed /// The server refused the op's **channel open**, the typed
/// open's decline (POC-1 §6: a rejected open resolves on the /// open's decline (POC-1 §6: a rejected open resolves on the
/// awaiting call, never a `wait()` match). The family-mirrored /// awaiting call, never a `wait()` match). The family-mirrored
/// reason rides; the connection stays usable (row-pinned). /// reason rides; the connection stays usable (row-pinned).
@@ -307,7 +307,7 @@ pub enum OpError {
OpenDeclined(#[from] OpenDeclineReason), OpenDeclined(#[from] OpenDeclineReason),
/// The server refused the op's **request** on the opened channel /// The server refused the op's **request** on the opened channel
/// — the SSH-REQUEST reply's `Failure` class, observed through /// in the SSH-REQUEST reply's `Failure` class, observed through
/// the `wait()`-drain (POC-3 §D.3: Success/Failure arrive on the /// the `wait()`-drain (POC-3 §D.3: Success/Failure arrive on the
/// channel queue, not as `Err` from the request call). The /// channel queue, not as `Err` from the request call). The
/// channel is closed behind the refusal (the row owned it). /// channel is closed behind the refusal (the row owned it).
@@ -315,26 +315,26 @@ pub enum OpError {
RequestRefused, RequestRefused,
/// The channel ended before the op's request reply arrived (the /// The channel ended before the op's request reply arrived (the
/// `wait()`-drain saw the channel's end) — the op cannot /// `wait()`-drain saw the channel's end); the op cannot
/// proceed, nothing is fabricated. /// proceed, nothing is fabricated.
#[error("the channel ended before the op's request reply arrived")] #[error("the channel ended before the op's request reply arrived")]
ChannelEnded, ChannelEnded,
/// The op's command exceeds the exec-length budget /// The op's command exceeds the exec-length budget
/// (`Limits::max_exec_command_len`) — the per-length cap's /// (`Limits::max_exec_command_len`): the per-length cap's
/// client-mirror verdict, answered **before any wire op** (bounded /// client-mirror verdict, answered **before any wire op** (bounded
/// resources; the server direction's pre-parse cap mirrored at /// resources; the server direction's pre-parse cap mirrored at
/// the client's command-construction gate). /// the client's command-construction gate).
#[error("the op's command exceeds the configured exec-command length cap")] #[error("the op's command exceeds the configured exec-command length cap")]
CommandTooLong, CommandTooLong,
/// The git row's repo id is outside the ADR-008 grammar — the /// The git row's repo id is outside the ADR-008 grammar, the
/// git row's validation gate mirrored (the server direction's /// git row's validation gate mirrored (the server direction's
/// `repo_id_validates` verdict), answered before any wire op. /// `repo_id_validates` verdict), answered before any wire op.
#[error("the repository id is outside the repo-id grammar")] #[error("the repository id is outside the repo-id grammar")]
InvalidRepoId, InvalidRepoId,
/// Unix sockets are unavailable on this platform — the /// Unix sockets are unavailable on this platform; this is the
/// `direct-streamlocal` twin's runtime-Unix gate (OQ-11: /// `direct-streamlocal` twin's runtime-Unix gate (OQ-11:
/// universal compile, clean off-Unix decline **before any wire /// universal compile, clean off-Unix decline **before any wire
/// op**), mirrored from the server direction's gate. /// op**), mirrored from the server direction's gate.
@@ -344,7 +344,7 @@ pub enum OpError {
/// The reverse-forward op's channel type has **no acceptor /// The reverse-forward op's channel type has **no acceptor
/// registered** (ADR-003 §4's client-side gate surfaced to the /// registered** (ADR-003 §4's client-side gate surfaced to the
/// *issuing* side: a reverse listen whose data leg could not serve /// *issuing* side: a reverse listen whose data leg could not serve
/// arrivals is issued never — the half-capability posture, /// arrivals is issued never; the half-capability posture,
/// answered before any wire op). Featured-but-unregistered is the /// answered before any wire op). Featured-but-unregistered is the
/// exact class: the unset-declines convention mirrored (the /// exact class: the unset-declines convention mirrored (the
/// acceptor slots are assembly-mutable, install-first is the /// acceptor slots are assembly-mutable, install-first is the
@@ -353,27 +353,27 @@ pub enum OpError {
NoAcceptor, NoAcceptor,
/// The peer refused the op's reverse-forward global request (the /// The peer refused the op's reverse-forward global request (the
/// `RequestDenied` wire decline class) — a listen denial or the /// `RequestDenied` wire decline class); a listen denial or the
/// cancel forms' unknown-listener answer (sshd parity: match by /// cancel forms' unknown-listener answer (sshd parity: match by
/// the raw wire strings). Channel-scoped honesty: the connection /// the raw wire strings). Channel-scoped honesty: the connection
/// stays usable (row-pinned), and the wire's own reason classes /// stays usable (row-pinned), and the wire's own reason classes
/// are never invented here — the closed family class mirrors the /// are never invented here; the closed family class mirrors the
/// wire shape, `RequestRefused`'s global-request counterpart. /// wire shape, `RequestRefused`'s global-request counterpart.
#[error("the peer refused the reverse-forward request")] #[error("the peer refused the reverse-forward request")]
ListenRefused, ListenRefused,
/// The op's reverse-listen admission hit the connection's /// The op's reverse-listen admission hit the connection's
/// dedicated listener ceiling (the listener counter sharing /// dedicated listener ceiling (the listener counter sharing
/// `Limits::max_concurrent_channels` — the server-side /// `Limits::max_concurrent_channels`, the server-side
/// listener-counter posture mirrored; the cap relation recorded in /// listener-counter posture mirrored; the cap relation recorded in
/// `client_accept.rs`) — answered **before any wire op**: a /// `client_accept.rs`), answered **before any wire op**: a
/// request the budget cannot track is issued never. /// request the budget cannot track is issued never.
#[error("the connection's reverse-listen count is at its ceiling")] #[error("the connection's reverse-listen count is at its ceiling")]
ListenCapExceeded, ListenCapExceeded,
/// The sftp route is the OQ-13 decline placeholder (the feature /// The sftp route is the OQ-13 decline placeholder (the feature
/// exists, the route does not until alkfs lands, on either /// exists, the route does not until alkfs lands, on either
/// direction) — answered before any wire op, never a /// direction), answered before any wire op and never a
/// half-opened subsystem channel. /// half-opened subsystem channel.
#[error("the sftp route is not served (the OQ-13 decline placeholder)")] #[error("the sftp route is not served (the OQ-13 decline placeholder)")]
SftpNotServed, SftpNotServed,
@@ -389,7 +389,7 @@ impl From<russh::Error> for OpError {
/// `russh::Error::ChannelOpenFailure` and maps to /// `russh::Error::ChannelOpenFailure` and maps to
/// [`OpError::OpenDeclined`] (family-mirrored /// [`OpError::OpenDeclined`] (family-mirrored
/// [`OpenDeclineReason`]); a denied reverse-forward *global* /// [`OpenDeclineReason`]); a denied reverse-forward *global*
/// request (`RequestDenied` — the listen/cancel forms' only wire /// request (`RequestDenied`, the listen/cancel forms' only wire
/// decline shape at this pin, the rows' only producer) maps to /// decline shape at this pin, the rows' only producer) maps to
/// [`OpError::ListenRefused`]; every other russh error stays the /// [`OpError::ListenRefused`]; every other russh error stays the
/// transparent [`OpError::Wire`]. /// transparent [`OpError::Wire`].
@@ -510,7 +510,7 @@ mod tests {
} }
/// The host-key policy's `false` verdict is russh's `UnknownKey` /// The host-key policy's `false` verdict is russh's `UnknownKey`
/// wire error — the client's `check_server_key` hook is the only /// wire error; the client's `check_server_key` hook is the only
/// producer, so the transparent mapping upgrades exactly that one /// producer, so the transparent mapping upgrades exactly that one
/// class and leaves everything else `Wire`. /// class and leaves everything else `Wire`.
/// ///
@@ -602,7 +602,7 @@ mod tests {
/// - *Contract stamp:* `client.md` §op rows (no vanilla-concept /// - *Contract stamp:* `client.md` §op rows (no vanilla-concept
/// leaks up) + POC-1 §6 (the typed open's decline resolves on /// leaks up) + POC-1 §6 (the typed open's decline resolves on
/// the awaiting call) — the wire class mirrors one-to-one into /// the awaiting call); the wire class mirrors one-to-one into
/// the family vocabulary, `Other` verbatim, nothing collapsed. /// the family vocabulary, `Other` verbatim, nothing collapsed.
#[test] #[test]
fn open_decline_mirror_preserves_every_class() { fn open_decline_mirror_preserves_every_class() {
@@ -653,7 +653,7 @@ mod tests {
} }
/// - *Contract stamp:* the reverse-forward global request's wire /// - *Contract stamp:* the reverse-forward global request's wire
/// decline class (`RequestDenied` — the listen/cancel forms' /// decline class (`RequestDenied`, the listen/cancel forms'
/// only producer shape at this pin): `ListenRefused`, never the /// only producer shape at this pin): `ListenRefused`, never the
/// transparent wire class; the typed-open mapping unchanged. /// transparent wire class; the typed-open mapping unchanged.
#[test] #[test]
@@ -662,7 +662,7 @@ mod tests {
assert!(matches!(mapped, OpError::ListenRefused), "got {mapped:?}"); assert!(matches!(mapped, OpError::ListenRefused), "got {mapped:?}");
} }
/// - *Contract stamp:* POC-1 §6 — a rejected channel open /// - *Contract stamp:* POC-1 §6, in which a rejected channel open
/// surfaces on the awaiting typed call as /// surfaces on the awaiting typed call as
/// `russh::Error::ChannelOpenFailure(..)`; the op rows map it /// `russh::Error::ChannelOpenFailure(..)`; the op rows map it
/// into [`OpError::OpenDeclined`] and every other wire error /// into [`OpError::OpenDeclined`] and every other wire error
+29 -29
View File
@@ -1,4 +1,4 @@
//! The exit-currency module — mapping.md §Exit chunk's derivation //! The exit-currency module: mapping.md §Exit chunk's derivation
//! contract (rules 1–6, normative) as code, *shared by both //! contract (rules 1–6, normative) as code, *shared by both
//! directions* (`w4-op-rows` extracted it out of `tty_map.rs`, whose //! directions* (`w4-op-rows` extracted it out of `tty_map.rs`, whose
//! server-side session rows were its first and only consumer). //! server-side session rows were its first and only consumer).
@@ -12,27 +12,27 @@
//! exactly one SSH exit report shape, **never widened**. //! exactly one SSH exit report shape, **never widened**.
//! - **The client direction** (`w4-op-rows`): the SSH exit report → //! - **The client direction** (`w4-op-rows`): the SSH exit report →
//! the family `i32` code ([`signal_code_from_name`] and the //! the family `i32` code ([`signal_code_from_name`] and the
//! direct `u32 → i32` identity) — the exact inverse arm, riding the //! direct `u32 → i32` identity), the exact inverse arm riding the
//! same table. The mirror consumes the convention as-is, including //! same table. The mirror consumes the convention as-is, including
//! its one collision (the `−1` sentinel vs a genuine SIGHUP — //! its one collision (the `−1` sentinel vs a genuine SIGHUP;
//! mapping.md rule 3's symmetric-fidelity cost). //! mapping.md rule 3's symmetric-fidelity cost).
//! //!
//! No widening in either arm: the chunk is not enriched, the wire //! No widening in either arm: the chunk is not enriched, the wire
//! report is not enriched, and the *core_dumped* bool is never //! report is not enriched, and the *core_dumped* bool is never
//! asserted (alktty discards it — nothing better is knowable). //! asserted (alktty discards it; nothing better is knowable).
//! //!
//! ## The server arm's rules (mapping.md, normative) //! ## The server arm's rules (mapping.md, normative)
//! //!
//! 1. `code ≥ 0` → `exit-status(N)`. //! 1. `code ≥ 0` → `exit-status(N)`.
//! 2. negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local //! 2. negative `−N`, N ≥ 2 → `exit-signal(name)` via the crate-local
//! **Unix signum→name table** ([`SIGNAL_NAMES`]). Server-side //! **Unix signum→name table** ([`SIGNAL_NAMES`]). Server-side
//! work — the signal semantics belong to the serving platform //! work: the signal semantics belong to the serving platform
//! where the backend reaps. Deliberately *not* derived from //! where the backend reaps. Deliberately *not* derived from
//! alktty's `signal_from_name` inverse, which is a forward //! alktty's `signal_from_name` inverse, which is a forward
//! name→number map for the control row, not a derivation table. //! name→number map for the control row, not a derivation table.
//! `core_dumped` is **never asserted** — SSH's `exit-signal` bool //! `core_dumped` is **never asserted**; SSH's `exit-signal` bool
//! stays false on every row. //! stays false on every row.
//! 3. `−1` → `exit-status(255)`, never a signal claim — the //! 3. `−1` → `exit-status(255)`, never a signal claim: the
//! could-not-determine sentinel collides with the SIGHUP encoding //! could-not-determine sentinel collides with the SIGHUP encoding
//! on the pipe path, accepted symmetrically per the mapping: //! on the pipe path, accepted symmetrically per the mapping:
//! neither a fabricated signal for a wait failure nor a real one. //! neither a fabricated signal for a wait failure nor a real one.
@@ -41,13 +41,13 @@
//! 5. **The PTY-path fidelity limit (upstream)**: portable-pty 0.9 //! 5. **The PTY-path fidelity limit (upstream)**: portable-pty 0.9
//! reaps a signal-terminated Unix child as a plain code (the //! reaps a signal-terminated Unix child as a plain code (the
//! signal number is unrecoverable through its trait surface), so //! signal number is unrecoverable through its trait surface), so
//! signal-terminated PTY sessions arrive as `exit-status(1)` — a //! signal-terminated PTY sessions arrive as `exit-status(1)`, a
//! documented alktty/portable-pty ask; this table in practice //! documented alktty/portable-pty ask; this table in practice
//! sees the *pipe* path's negative encoding only. Faithful //! sees the *pipe* path's negative encoding only. Faithful
//! `exit-signal` there needs the alktty backend to conform to its //! `exit-signal` there needs the alktty backend to conform to its
//! own ADR's negative-code convention; alkssh's contract adapts //! own ADR's negative-code convention; alkssh's contract adapts
//! automatically. //! automatically.
//! 6. Unmapped negatives → rule 3 (`exit-status(255)`) — the mapping //! 6. Unmapped negatives → rule 3 (`exit-status(255)`); the mapping
//! never invents a name (defensive case: the convention's //! never invents a name (defensive case: the convention's
//! producers are `−1` and `−signum` only). //! producers are `−1` and `−signum` only).
//! //!
@@ -56,26 +56,26 @@
//! 1. `exit-status(N)` → the family code `N` (direct identity). //! 1. `exit-status(N)` → the family code `N` (direct identity).
//! 2. `exit-signal(name)` → the family code `−N` via the same //! 2. `exit-signal(name)` → the family code `−N` via the same
//! [`SIGNAL_NAMES`] table read backwards //! [`SIGNAL_NAMES`] table read backwards
//! ([`signal_code_from_name`]) — the mapping never invents a //! ([`signal_code_from_name`]); the mapping never invents a
//! signum. //! signum.
//! 3. A name with no table entry → `−1` (the sentinel; the same //! 3. A name with no table entry → `−1` (the sentinel; the same
//! honest could-not-determine semantic rule 3 consumes on the //! honest could-not-determine semantic rule 3 consumes on the
//! server arm — including a genuine SIGHUP, whose mirror rides //! server arm, including a genuine SIGHUP, whose mirror rides
//! `−1` by the same accepted collision). //! `−1` by the same accepted collision).
//! 4. No exit report at all ends a session with none — the //! 4. No exit report at all ends a session with none, the
//! no-exit-chunk close family's mirror ("reports nothing", //! no-exit-chunk close family's mirror ("reports nothing",
//! mapping.md): the caller observes the session's end, never a //! mapping.md): the caller observes the session's end, never a
//! fabricated code. //! fabricated code.
/// One SSH exit report shape derived from an exit chunk — /// One SSH exit report shape derived from an exit chunk;
/// [`derive_exit`]'s outcome. /// [`derive_exit`]'s outcome.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub(crate) enum ExitReport { pub(crate) enum ExitReport {
/// `exit-status(N)` — codes ≥ 0, the `−1` sentinel, and every /// `exit-status(N)`: codes ≥ 0, the `−1` sentinel, and every
/// unmapped negative (rules 1, 3, 4, 6). /// unmapped negative (rules 1, 3, 4, 6).
Status(u32), Status(u32),
/// `exit-signal(name)` — a mapped negative `−N`, N ≥ 2 (rule 2). /// `exit-signal(name)`: a mapped negative `−N` where N ≥ 2 (rule 2).
/// The name is the table's value, never an invention. /// The name is the table's value, never an invention.
Signal(&'static str), Signal(&'static str),
} }
@@ -83,11 +83,11 @@ pub(crate) enum ExitReport {
/// The crate-local Unix signum→name table (server arm rule 2 / client /// The crate-local Unix signum→name table (server arm rule 2 / client
/// arm rule 2): the 1–31 conventional Unix signals, as the exit /// arm rule 2): the 1–31 conventional Unix signals, as the exit
/// chunk's producers encode them (`−signum`) and as the wire's /// chunk's producers encode them (`−signum`) and as the wire's
/// `exit-signal` senders name them. Server-side work — the signal /// `exit-signal` senders name them. Server-side work: the signal
/// semantics belong to the serving platform where the backend reaps. /// semantics belong to the serving platform where the backend reaps.
/// Deliberately **not** derived from alktty's `signal_from_name` (a /// Deliberately **not** derived from alktty's `signal_from_name` (a
/// forward name→number map for the control row, not a derivation /// forward name→number map for the control row, not a derivation
/// table; deliberately narrower too — it exists for what a front-end /// table; deliberately narrower too, existing for what a front-end
/// sends, not for what a child can die by). /// sends, not for what a child can die by).
pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[ pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[
(1, "HUP"), (1, "HUP"),
@@ -124,7 +124,7 @@ pub(crate) const SIGNAL_NAMES: &[(u32, &str)] = &[
]; ];
/// The derivation contract's whole decision procedure (the §module /// The derivation contract's whole decision procedure (the §module
/// docs' rules 1–6 — normative). /// docs' rules 1–6; normative).
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub(crate) fn derive_exit(code: i32) -> ExitReport { pub(crate) fn derive_exit(code: i32) -> ExitReport {
match code { match code {
@@ -143,7 +143,7 @@ pub(crate) fn derive_exit(code: i32) -> ExitReport {
/// The client arm's inverse read (§module docs, rules 1–3): the SSH /// The client arm's inverse read (§module docs, rules 1–3): the SSH
/// `exit-signal` sender's name → the family exit-code convention's /// `exit-signal` sender's name → the family exit-code convention's
/// value (`−signum`), `−1` for a name with no table entry (the /// value (`−signum`), `−1` for a name with no table entry (the
/// sentinel — the mapping never invents a signum; a genuine SIGHUP /// sentinel; the mapping never invents a signum, and a genuine SIGHUP
/// rides it by the same accepted collision). /// rides it by the same accepted collision).
pub(crate) fn signal_code_from_name(name: &str) -> i32 { pub(crate) fn signal_code_from_name(name: &str) -> i32 {
match SIGNAL_NAMES.iter().find(|(_, n)| *n == name) { match SIGNAL_NAMES.iter().find(|(_, n)| *n == name) {
@@ -155,7 +155,7 @@ pub(crate) fn signal_code_from_name(name: &str) -> i32 {
/// The table's name → russh's `Sig` vocabulary (the /// The table's name → russh's `Sig` vocabulary (the
/// `exit_signal_request` parameter). russh's known set rides its /// `exit_signal_request` parameter). russh's known set rides its
/// variant names; anything else rides `Sig::Custom` (RFC 4254 §6.10's /// variant names; anything else rides `Sig::Custom` (RFC 4254 §6.10's
/// signal-name encoding — vanilla clients parse arbitrary names). /// signal-name encoding, since vanilla clients parse arbitrary names).
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub(crate) fn sig_from_name(name: &str) -> russh::Sig { pub(crate) fn sig_from_name(name: &str) -> russh::Sig {
match name { match name {
@@ -177,7 +177,7 @@ pub(crate) fn sig_from_name(name: &str) -> russh::Sig {
/// The SSH signal row's name translation: russh's `Sig` → the name /// The SSH signal row's name translation: russh's `Sig` → the name
/// string alktty's control vocabulary carries (the uppercase names; /// string alktty's control vocabulary carries (the uppercase names;
/// `Custom` carrying its own). Names flow to the backend verbatim — /// `Custom` carrying its own). Names flow to the backend verbatim,
/// no alkssh-side filter (mapping.md §the control rows; the client /// no alkssh-side filter (mapping.md §the control rows; the client
/// arm's `exit-signal` consumption reads through this before /// arm's `exit-signal` consumption reads through this before
/// [`signal_code_from_name`]). /// [`signal_code_from_name`]).
@@ -207,7 +207,7 @@ mod tests {
use russh::Sig; use russh::Sig;
/// - *Contract stamp:* mapping.md §Exit chunk rule 1 (`code ≥ 0` /// - *Contract stamp:* mapping.md §Exit chunk rule 1 (`code ≥ 0`
/// → `exit-status(N)` — the chunk carries the whole truth for /// → `exit-status(N)`; the chunk carries the whole truth for
/// clean exits). /// clean exits).
#[test] #[test]
fn non_negative_codes_map_to_exit_status() { fn non_negative_codes_map_to_exit_status() {
@@ -218,8 +218,8 @@ mod tests {
} }
/// - *Contract stamp:* mapping.md §Exit chunk rule 2 (negative /// - *Contract stamp:* mapping.md §Exit chunk rule 2 (negative
/// `−N`, N ≥ 2 → `exit-signal(name)` via the table — the POC-3 /// `−N`, N ≥ 2 → `exit-signal(name)` via the table; the POC-3
/// §D.2 live evidence: `−15` → `exit-signal("TERM")`). /// §D.2 live evidence is `−15` → `exit-signal("TERM")`).
#[test] #[test]
fn negative_signals_map_through_the_table() { fn negative_signals_map_through_the_table() {
for (signum, name) in SIGNAL_NAMES.iter().skip(1) { for (signum, name) in SIGNAL_NAMES.iter().skip(1) {
@@ -230,7 +230,7 @@ mod tests {
} }
/// - *Contract stamp:* mapping.md §Exit chunk rule 3 (`−1` → /// - *Contract stamp:* mapping.md §Exit chunk rule 3 (`−1` →
/// `exit-status(255)`, never a signal claim — the sentinel/ /// `exit-status(255)`, never a signal claim; the sentinel/
/// SIGHUP collision accepted symmetrically). /// SIGHUP collision accepted symmetrically).
#[test] #[test]
fn the_sentinel_is_not_a_signal() { fn the_sentinel_is_not_a_signal() {
@@ -238,7 +238,7 @@ mod tests {
} }
/// - *Contract stamp:* mapping.md §Exit chunk rule 6 (unmapped /// - *Contract stamp:* mapping.md §Exit chunk rule 6 (unmapped
/// negatives → rule 3 — the mapping never invents a name). /// negatives → rule 3; the mapping never invents a name).
#[test] #[test]
fn unmapped_negatives_report_255() { fn unmapped_negatives_report_255() {
for code in [i32::MIN, -33, -32, -100, -1_000_000] { for code in [i32::MIN, -33, -32, -100, -1_000_000] {
@@ -246,7 +246,7 @@ mod tests {
} }
} }
/// - *Contract stamp:* the table bound — every entry is the /// - *Contract stamp:* the table bound: every entry is the
/// conventional Unix signal set 1–31, sorted, names unique and /// conventional Unix signal set 1–31, sorted, names unique and
/// uppercase (the vocabulary the derivation table promises). /// uppercase (the vocabulary the derivation table promises).
#[test] #[test]
@@ -282,7 +282,7 @@ mod tests {
} }
} }
/// The client arm's mirror row — runnable in every build the arm /// The client arm's mirror row, runnable in every build the arm
/// compiles in (session or git; the git-only builds carry the client /// compiles in (session or git; the git-only builds carry the client
/// rows too). /// rows too).
#[cfg(test)] #[cfg(test)]
@@ -290,7 +290,7 @@ mod tests {
mod client_arm_rows { mod client_arm_rows {
use super::SIGNAL_NAMES; use super::SIGNAL_NAMES;
/// - *Contract stamp:* the client arm (§module docs' rules 1–3) — /// - *Contract stamp:* the client arm (§module docs' rules 1–3),
/// the inverse read through the same table, never widened: /// the inverse read through the same table, never widened:
/// mapped names ride `−signum`, unmapped names ride the `−1` /// mapped names ride `−signum`, unmapped names ride the `−1`
/// sentinel (the mapping never invents a signum). /// sentinel (the mapping never invents a signum).
+3 -3
View File
@@ -6,7 +6,7 @@
//! path (client) surfaces it at `check_server_key`, and assembly //! path (client) surfaces it at `check_server_key`, and assembly
//! registrations store it as a string. The string format //! registrations store it as a string. The string format
//! (`SSHFP`-style `SHA256:` prefix over the OpenSSH wire-key digest, //! (`SSHFP`-style `SHA256:` prefix over the OpenSSH wire-key digest,
//! unpadded base64) is a **contract invariant** — registration strings //! unpadded base64) is a **contract invariant**: registration strings
//! from any standard issuer (`ssh-keygen -lf`, alkcall-side //! from any standard issuer (`ssh-keygen -lf`, alkcall-side
//! computation) must compare equal to what this crate computes. //! computation) must compare equal to what this crate computes.
//! //!
@@ -17,13 +17,13 @@
use russh::keys::{HashAlg, PublicKey}; use russh::keys::{HashAlg, PublicKey};
/// Computes a public key's fingerprint — the single fingerprint path /// Computes a public key's fingerprint, the single fingerprint path
/// in the crate (ADR-004 decision 1's contract invariant). /// in the crate (ADR-004 decision 1's contract invariant).
/// ///
/// The returned string is the OpenSSH fingerprint format: `SHA256:` /// The returned string is the OpenSSH fingerprint format: `SHA256:`
/// followed by the unpadded base64 of the SHA-256 digest over the /// followed by the unpadded base64 of the SHA-256 digest over the
/// key's OpenSSH wire encoding, byte-identical to independent issuers /// key's OpenSSH wire encoding, byte-identical to independent issuers
/// (`ssh-keygen -lf`) — parity is proven, not assumed. Every /// (`ssh-keygen -lf`); parity is proven, not assumed. Every
/// fingerprint comparison in the crate and every registration value an /// fingerprint comparison in the crate and every registration value an
/// assembly stores is this string; no other formatting path exists. /// assembly stores is this string; no other formatting path exists.
pub fn fingerprint(public_key: &PublicKey) -> String { pub fn fingerprint(public_key: &PublicKey) -> String {
+30 -30
View File
@@ -1,4 +1,4 @@
//! The git mapping (`w3-git-mapping`) — the git branch of the spine //! The git mapping (`w3-git-mapping`), the git branch of the spine
//! (ADR-006 decision 2, exec-only, the alkforge primary purpose): the //! (ADR-006 decision 2, exec-only, the alkforge primary purpose): the
//! dispatch's [`ExecRoute::Git`](crate::dispatch::ExecRoute) route //! dispatch's [`ExecRoute::Git`](crate::dispatch::ExecRoute) route
//! dispatches to the published alkgit seam, //! dispatches to the published alkgit seam,
@@ -10,33 +10,33 @@
//! //!
//! ## The decisions recorded here (the task's decide-and-record set) //! ## The decisions recorded here (the task's decide-and-record set)
//! //!
//! - **Repo id — validate, never resolve.** The parse stage //! - **Repo id: validate, never resolve.** The parse stage
//! ([`crate::dispatch`]) normalizes the wire path; this stage //! ([`crate::dispatch`]) normalizes the wire path; this stage
//! validates the id against `alkgit::validate_repo_id` (1–2 //! validates the id against `alkgit::validate_repo_id` (1–2
//! segments, charset `a-z0-9-_`, ≤ 100 bytes — the ADR-008 //! segments, charset `a-z0-9-_`, ≤ 100 bytes, the ADR-008
//! repo-id grammar) and hands it through untouched to //! repo-id grammar) and hands it through untouched to
//! `serve_duplex_resolved`. **alkssh never resolves**: the //! `serve_duplex_resolved`. **alkssh never resolves**: the
//! resolution, the authorize policy and the collapsed generic //! resolution, the authorize policy and the collapsed generic
//! denial (unknown ≡ unauthorized, one wire shape, no existence //! denial (unknown ≡ unauthorized, one wire shape, no existence
//! oracle) live at the seam. An id failing the *shape* gate is //! oracle) live at the seam. An id failing the *shape* gate is
//! declined here — `invalid-repo-id`, request-level, reason-only on //! declined here as `invalid-repo-id`, request-level, reason-only on
//! the wire (the reply-vocabulary decision in [`crate::dispatch`]). //! the wire (the reply-vocabulary decision in [`crate::dispatch`]).
//! //!
//! - **Wire shapes per decline class** (the open/reply vocabulary): //! - **Wire shapes per decline class** (the open/reply vocabulary):
//! - *Request-level* declinables (`unknown-service`, `service-only` //! - *Request-level* declinables (`unknown-service` and `service-only`
//! — the parse core's; `invalid-repo-id` — this module's gate): //! from the parse core; `invalid-repo-id` from this module's gate):
//! `channel_failure`, channel left usable. The reply vocabulary is //! `channel_failure`, channel left usable. The reply vocabulary is
//! shared with parse-then-fail; the reason lives in the events //! shared with parse-then-fail; the reason lives in the events
//! only. //! only.
//! - *Serving-path* declines (the row owned the channel): //! - *Serving-path* declines (the row owned the channel):
//! seam-unconfigured (feature on, [`crate::GitSeam`] absent) and //! seam-unconfigured (feature on, [`crate::GitSeam`] absent) and
//! the identity-less guard — failure + close, from the spawned //! the identity-less guard, failure + close from the spawned
//! task, the wave-2 seam-stub shape. //! task, the wave-2 seam-stub shape.
//! - *The seam's own denials* are not alkssh declines: an unknown or //! - *The seam's own denials* are not alkssh declines: an unknown or
//! ungranted repo flows to `serve_duplex_resolved`, whose //! ungranted repo flows to `serve_duplex_resolved`, whose
//! collapsed generic denial writes the `ERR access denied: <repo>` //! collapsed generic denial writes the `ERR access denied: <repo>`
//! band over the channel **before any pack/ref byte** //! band over the channel **before any pack/ref byte**
//! (`SubstrateError::AccessDenied`) — the POC-3 §A.5 byte-order //! (`SubstrateError::AccessDenied`); the POC-3 §A.5 byte-order
//! evidence, contract-pinned by the denial rows in //! evidence, contract-pinned by the denial rows in
//! `tests/git_mapping.rs`. //! `tests/git_mapping.rs`.
//! //!
@@ -45,7 +45,7 @@
//! built from the channel's **split halves**, never //! built from the channel's **split halves**, never
//! `Channel::into_stream()`. `into_stream` wraps the whole channel //! `Channel::into_stream()`. `into_stream` wraps the whole channel
//! in russh's `ChannelCloseOnDrop`, whose `Drop` sends //! in russh's `ChannelCloseOnDrop`, whose `Drop` sends
//! server→client `CHANNEL_CLOSE` from a *spawned task* — inside the //! server→client `CHANNEL_CLOSE` from a *spawned task* inside the
//! seam call, racing [`serve_git_row`]'s explicit //! seam call, racing [`serve_git_row`]'s explicit
//! `exit_status_request` for a queue position on the same bounded //! `exit_status_request` for a queue position on the same bounded
//! `Sender<Msg>` (russh's `event_buffer_size`, default 10): a full //! `Sender<Msg>` (russh's `event_buffer_size`, default 10): a full
@@ -53,7 +53,7 @@
//! let the loop remove the channel, and silently no-op the report. //! let the loop remove the channel, and silently no-op the report.
//! The split-halves shape removes the competitor instead of racing //! The split-halves shape removes the competitor instead of racing
//! it: russh 0.65 keeps its `ChannelRx`/`ChannelTx` private, so //! it: russh 0.65 keeps its `ChannelRx`/`ChannelTx` private, so
//! [`GitChannelDuplex`] is built from the public API — the reader //! [`GitChannelDuplex`] is built from the public API: the reader
//! is the module's [`ChannelHalveReader`] (an owned read half //! is the module's [`ChannelHalveReader`] (an owned read half
//! repinning the private `ChannelRx`'s observation semantics //! repinning the private `ChannelRx`'s observation semantics
//! without the close-on-drop wrapper; its only `Drop` behavior //! without the close-on-drop wrapper; its only `Drop` behavior
@@ -62,14 +62,14 @@
//! sends EOF, exactly `into_stream`'s). Nothing the seam drops ever //! sends EOF, exactly `into_stream`'s). Nothing the seam drops ever
//! messages the wire, so the report's send is the sole //! messages the wire, so the report's send is the sole
//! close-contracting sender, and its queue position ahead of the //! close-contracting sender, and its queue position ahead of the
//! explicit close holds by construction — sequential awaited sends //! explicit close holds by construction: sequential awaited sends
//! on the one `Handle`, saturated buffer or not. The tty rows are //! on the one `Handle`, saturated buffer or not. The tty rows are
//! not in this class: their report precedes any close-contracting //! not in this class: their report precedes any close-contracting
//! drop within one task — the wave-3 gate verified. //! drop within one task (the wave-3 gate verified).
//! //!
//! - **Exit-status mapping** (POC-3 §A.5, decided): //! - **Exit-status mapping** (POC-3 §A.5, decided):
//! [`serve_git_row`] maps the serve outcome onto exactly two //! [`serve_git_row`] maps the serve outcome onto exactly two
//! codes — `Ok(())` → `exit-status(0)` (vanilla git exits 0 on //! codes: `Ok(())` → `exit-status(0)` (vanilla git exits 0 on
//! success) and every `SubstrateError` → `exit-status(1)`. `1` for //! success) and every `SubstrateError` → `exit-status(1)`. `1` for
//! `AccessDenied` is the POC's wire evidence (the russh client //! `AccessDenied` is the POC's wire evidence (the russh client
//! observed `ExitStatus{1}` after the denial band); `Io`/`Protocol` //! observed `ExitStatus{1}` after the denial band); `Io`/`Protocol`
@@ -77,32 +77,32 @@
//! machinery-misuse variants (`ResetRequired`/`WriterTaken`/ //! machinery-misuse variants (`ResetRequired`/`WriterTaken`/
//! `RawTailUnavailable`) never arise from the serving entry //! `RawTailUnavailable`) never arise from the serving entry
//! (substrate-internal misuse, never a wire shape) and are //! (substrate-internal misuse, never a wire shape) and are
//! defensively mapped to `1`. `255` is never fabricated — it is //! defensively mapped to `1`. `255` is never fabricated; it is
//! OpenSSH's could-not-start semantic, which this door never claims. //! OpenSSH's could-not-start semantic, which this door never claims.
//! The status is sent **before** close, from the spawned serving //! The status is sent **before** close, from the spawned serving
//! task via the cloned `Session::handle()` //! task via the cloned `Session::handle()`
//! ([`crate::serve`]'s mechanics; `mapping.md`'s exit_status row). //! ([`crate::serve`]'s mechanics; `mapping.md`'s exit_status row).
//! //!
//! - **Identity ride** (`w2-auth-bridge`'s parked item, discharged //! - **Identity ride** (`w2-auth-bridge`'s parked item, discharged
//! here): the resolved identity — the bridge's //! here): the resolved identity (the bridge's
//! `auth_succeeded`-state ride-along in handler state — is cloned //! `auth_succeeded`-state ride-along in handler state) is cloned
//! into the serving task and passed on //! into the serving task and passed on
//! `serve_duplex_resolved`'s identity param, so the visible-surface //! `serve_duplex_resolved`'s identity param, so the visible-surface
//! invariant is observable through the git row: alkgit's //! invariant is observable through the git row: alkgit's
//! `authorize` evaluation is the live witness (a granted-private //! `authorize` evaluation is the live witness (a granted-private
//! repo serves, an ungranted one denies — the harness rows pin //! repo serves, an ungranted one denies; the harness rows pin
//! both). A handler state without an identity is unreachable (russh //! both). A handler state without an identity is unreachable (russh
//! authorizes before channels exist) and declines serving-path //! authorizes before channels exist) and declines serving-path
//! (`no-identity`) — never anonymous serving. //! (`no-identity`), never anonymous serving.
//! //!
//! - **Process-mode dialect**: alkgit speaks first (fetch: the V2 //! - **Process-mode dialect**: alkgit speaks first (fetch: the V2
//! advertisement, no `# service=` prefix; push: the V0 ref //! advertisement, no `# service=` prefix; push: the V0 ref
//! advertisement) — nothing alkssh-side; the byte-level rows in //! advertisement), nothing alkssh-side; the byte-level rows in
//! `tests/git_mapping.rs` verify the dialect holds over the channel //! `tests/git_mapping.rs` verify the dialect holds over the channel
//! duplex. //! duplex.
//! //!
//! The serve outcome's event shape: the serving-path lifecycle only //! The serve outcome's event shape: the serving-path lifecycle only
//! (started / ended — the vocabulary's `warn`-failed when the wire //! (started / ended, with the vocabulary's `warn`-failed when the wire
//! reporting breaks). A serve `Err` is *not* a decline event: the //! reporting breaks). A serve `Err` is *not* a decline event: the
//! substrate already mapped its own error onto the wire (bands, the //! substrate already mapped its own error onto the wire (bands, the
//! denial), and the client's observation is the exit-status above. //! denial), and the client's observation is the exit-status above.
@@ -123,7 +123,7 @@ use crate::dispatch::GitServiceRoute;
use crate::observe; use crate::observe;
/// The git row's validation gate: the id's shape against /// The git row's validation gate: the id's shape against
/// `alkgit::validate_repo_id` — the ADR-008 repo-id grammar for /// `alkgit::validate_repo_id`, the ADR-008 repo-id grammar for
/// *rejection* only (the mapping-free posture; the id is never /// *rejection* only (the mapping-free posture; the id is never
/// decomposed, joined, or normalized into paths beyond /// decomposed, joined, or normalized into paths beyond
/// [`crate::dispatch`]'s quote/slash/`.git` stage). /// [`crate::dispatch`]'s quote/slash/`.git` stage).
@@ -150,13 +150,13 @@ fn exit_code(outcome: &Result<(), SubstrateError>) -> u32 {
} }
} }
/// The seam's duplex over the **split** channel halves — the /// The seam's duplex over the **split** channel halves, the
/// status-before-close shape's carrier (the module doc's /// status-before-close shape's carrier (the module doc's
/// §Status-before-close decision). russh 0.65 keeps its `ChannelRx`/ /// §Status-before-close decision). russh 0.65 keeps its `ChannelRx`/
/// `ChannelTx` types private, so both halves are built from public /// `ChannelTx` types private, so both halves are built from public
/// API: the reader is a [`ChannelHalveReader`] (an owned close-free /// API: the reader is a [`ChannelHalveReader`] (an owned close-free
/// read half), the writer the write half's `ChannelTx` via /// read half), the writer the write half's `ChannelTx` via
/// `make_writer_ext(None)` — its `poll_shutdown` sends EOF, exactly /// `make_writer_ext(None)`; its `poll_shutdown` sends EOF, exactly
/// `into_stream`'s semantics, its `Drop` messages nothing (the /// `into_stream`'s semantics, its `Drop` messages nothing (the
/// `ChannelTx` `Drop` is a writer-notification only). Nothing the /// `ChannelTx` `Drop` is a writer-notification only). Nothing the
/// seam drops ever messages the wire, so the report's send is the /// seam drops ever messages the wire, so the report's send is the
@@ -195,20 +195,20 @@ impl AsyncWrite for GitChannelDuplex {
} }
} }
/// The owned close-free read half — the seam-side reader. Semantics /// The owned close-free read half, the seam-side reader. Semantics
/// repinned from russh's private `ChannelRx<S>` (the `into_stream` /// repinned from russh's private `ChannelRx<S>` (the `into_stream`
/// shape's reader; identical observable behavior without the /// shape's reader; identical observable behavior without the
/// `ChannelCloseOnDrop` wrapper): `Data` fills the buffer (partial /// `ChannelCloseOnDrop` wrapper): `Data` fills the buffer (partial
/// reads buffered), `ExtendedData` with no sideband and the other /// reads buffered), `ExtendedData` with no sideband and the other
/// non-data messages are swallowed and re-read (the `_` arm), /// non-data messages are swallowed and re-read (the `_` arm),
/// `Eof` ends the stream, and the peer's `Close` leaves the reader /// `Eof` ends the stream, and the peer's `Close` leaves the reader
/// to the channel's sender-death EOF (`wait()` → `None` — the /// to the channel's sender-death EOF (`wait()` → `None`, the
/// channel's internal sender drops at the loop's close processing). /// channel's internal sender dropping at the loop's close processing).
/// ///
/// The `wait()` access is future-owned: each read round-trips the /// The `wait()` access is future-owned: each read round-trips the
/// half through a boxed [`Future`] that parks it inside russh's /// half through a boxed [`Future`] that parks it inside russh's
/// `ChannelReadHalf::wait` and hands it back with the observed /// `ChannelReadHalf::wait` and hands it back with the observed
/// message on completion — the reader is fully owned (usable under /// message on completion; the reader is fully owned (usable under
/// the seam's `'static` `T` bound) across every park, buffer and /// the seam's `'static` `T` bound) across every park, buffer and
/// drop state; no `Drop` path ever messages the wire. /// drop state; no `Drop` path ever messages the wire.
struct ChannelHalveReader { struct ChannelHalveReader {
@@ -296,15 +296,15 @@ fn git_channel_duplex(channel: Channel<Msg>) -> GitChannelDuplex {
} }
/// The spawned git serving path: dispatch to the seam over the /// The spawned git serving path: dispatch to the seam over the
/// [`git_channel_duplex`] (the split halves — the seam contracts /// [`git_channel_duplex`] (the split halves; the seam contracts
/// nothing on its drops), then the exit-status-then-close report /// nothing on its drops), then the exit-status-then-close report
/// through the cloned handle — the report's send is the sole /// through the cloned handle; the report's send is the sole
/// close-contracting sender and each send awaits before the next is /// close-contracting sender and each send awaits before the next is
/// issued, so the status-before-close order holds by construction /// issued, so the status-before-close order holds by construction
/// under any schedule. The channel was taken from the handler's store /// under any schedule. The channel was taken from the handler's store
/// by the dispatch (the store-and-take handoff); the spawned task owns /// by the dispatch (the store-and-take handoff); the spawned task owns
/// the halves and their report surface. A failing report send means /// the halves and their report surface. A failing report send means
/// the session is gone — the lifecycle's `warn`-failed records it (the /// the session is gone; the lifecycle's `warn`-failed records it (the
/// one warn of the vocabulary). /// one warn of the vocabulary).
pub(crate) async fn serve_git_row( pub(crate) async fn serve_git_row(
handle: Handle, handle: Handle,
+147 -147
View File
@@ -1,4 +1,4 @@
//! The per-connection server handler — the ADR-004 dispositional auth //! The per-connection server handler: the ADR-004 dispositional auth
//! bridge, plus the exec-grammar dispatch and the fail-closed decline //! bridge, plus the exec-grammar dispatch and the fail-closed decline
//! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth //! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth
//! hooks; `w3-git-mapping` replaced the skeleton probe with the full //! hooks; `w3-git-mapping` replaced the skeleton probe with the full
@@ -9,30 +9,30 @@
//! //!
//! russh performs all cryptographic work: [`Handler::auth_publickey`] //! russh performs all cryptographic work: [`Handler::auth_publickey`]
//! fires *after* signature and ownership verification, so this handler //! fires *after* signature and ownership verification, so this handler
//! never sees or verifies a signature — its surface is dispositions //! never sees or verifies a signature; its surface is dispositions
//! only. The single resolution path: ed25519-only gate → the crate's //! only. The single resolution path: ed25519-only gate → the crate's
//! one fingerprint path ([`crate::fingerprint::fingerprint`]) → the //! one fingerprint path ([`crate::fingerprint::fingerprint`]) → the
//! injected `IdentityProvider::resolve_from_fingerprint` → `Some` //! injected `IdentityProvider::resolve_from_fingerprint` → `Some`
//! accepts with the resolved identity stored in per-connection handler //! accepts with the resolved identity stored in per-connection handler
//! state (it rides the session for the wave-3 seam propagation — //! state (it rides the session for the wave-3 seam propagation,
//! [`Handler::auth_succeeded`] is the ride-along hook); `None` rejects //! [`Handler::auth_succeeded`] is the ride-along hook); `None` rejects
//! fail-closed — the resolved-identity-`None` path never creates a //! fail-closed; the resolved-identity-`None` path never creates a
//! weakly authorized session. //! weakly authorized session.
//! //!
//! Every other method is an **explicit** reject — [`Handler::auth_none`], //! Every other method is an **explicit** reject: [`Handler::auth_none`],
//! [`Handler::auth_password`] and [`Handler::auth_keyboard_interactive`] //! [`Handler::auth_password`] and [`Handler::auth_keyboard_interactive`]
//! each implemented, never inherited — so the posture survives a pin //! each implemented, never inherited, so the posture survives a pin
//! bump that could silently change russh's defaults (ADR-004 decision //! bump that could silently change russh's defaults (ADR-004 decision
//! 3). The non-ed25519 algorithm decline is explicit-over-inherited too //! 3). The non-ed25519 algorithm decline is explicit-over-inherited too
//! (ADR-002 decision 4). [`Handler::auth_publickey_offered`] //! (ADR-002 decision 4). [`Handler::auth_publickey_offered`]
//! accepts-and-delegates explicitly (the probe hook consumes no attempt //! accepts-and-delegates explicitly (the probe hook consumes no attempt,
//! — POC-1 verified; the delegation keeps `max_auth_attempts` counting //! POC-1 verified; the delegation keeps `max_auth_attempts` counting
//! honest). //! honest).
//! //!
//! The username carries no semantics (OQ-03 resolved): resolution is //! The username carries no semantics (OQ-03 resolved): resolution is
//! fingerprint-only — the bridge core takes no username parameter at //! fingerprint-only; the bridge core takes no username parameter at
//! all, structurally refusing any identity input from the user string. //! all, structurally refusing any identity input from the user string.
//! Auth declines are *dispositions* (`Auth::Reject`), not errors — the //! Auth declines are *dispositions* (`Auth::Reject`), not errors; the
//! error model stays key-source-only. //! error model stays key-source-only.
//! //!
//! ## The parse-only dispatch + the fail-closed decline core //! ## The parse-only dispatch + the fail-closed decline core
@@ -41,23 +41,23 @@
//! exec-parse skeleton probe live in [`crate::dispatch`].) Every //! exec-parse skeleton probe live in [`crate::dispatch`].) Every
//! `channel_open_*` hook gates per the feature graph: `session` //! `channel_open_*` hook gates per the feature graph: `session`
//! channels open only where a session-serving feature exists (`session` //! channels open only where a session-serving feature exists (`session`
//! ∨ `git` — the parse path lives inside session channels); //! ∨ `git`, since the parse path lives inside session channels);
//! `direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on //! `direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
//! the `tunnel` feature; **`x11` always declines** — never //! the `tunnel` feature; **`x11` always declines**, never
//! feature-gated (ADR-003 §5). Unfeatured and never-feature rows //! feature-gated (ADR-003 §5). Unfeatured and never-feature rows
//! decline **at the open** with a clean wire reason //! decline **at the open** with a clean wire reason
//! ([`crate::dispatch::unfeatured_open_decline`]) — never a //! ([`crate::dispatch::unfeatured_open_decline`]), never a
//! half-open channel. The `direct-tcpip` / `direct-streamlocal` //! half-open channel. The `direct-tcpip` / `direct-streamlocal`
//! featured rows are the route-1 bridge (`w3-tunnel-direct`, //! featured rows are the route-1 bridge (`w3-tunnel-direct`,
//! [`crate::tunnel_map`]): the pre-admit decline classes (the unset //! [`crate::tunnel_map`]): the pre-admit decline classes (the unset
//! fabric handle under the feature — the unset-declines posture; the //! fabric handle under the feature, the unset-declines posture; the
//! unreachable-in-practice `no-identity`; an impossible wire target), //! unreachable-in-practice `no-identity`; an impossible wire target),
//! the cap admission at the hook, then the spawned bridge — accept //! the cap admission at the hook, then the spawned bridge; accept
//! only after the fabric open succeeds, `ConnectFailed` on failure //! only after the fabric open succeeds, `ConnectFailed` on failure
//! ([`crate::dispatch::connect_failed_open_decline`], the wave-2 //! ([`crate::dispatch::connect_failed_open_decline`], the wave-2
//! seam stub's reason surviving as the real decline). //! seam stub's reason surviving as the real decline).
//! An **inbound** `forwarded-tcpip` open — arriving *from* the vanilla //! An **inbound** `forwarded-tcpip` open, arriving *from* the vanilla
//! client — declines with the same explicit `ConnectFailed` as the //! client, declines with the same explicit `ConnectFailed` as the
//! unvanilla-arrival class (`w3-tunnel-listen`: the real `-R` data //! unvanilla-arrival class (`w3-tunnel-listen`: the real `-R` data
//! leg is the door's own server-initiated opens, driven by the //! leg is the door's own server-initiated opens, driven by the
//! [`crate::tunnel_map`] arrival pump; a vanilla client never //! [`crate::tunnel_map`] arrival pump; a vanilla client never
@@ -65,47 +65,47 @@
//! The bare //! The bare
//! `auth-agent@openssh.com` channel *type* //! `auth-agent@openssh.com` channel *type*
//! (`forwarded-streamlocal@openssh.com` likewise) has no russh handler //! (`forwarded-streamlocal@openssh.com` likewise) has no russh handler
//! hook at this pin — russh rejects those opens inline //! hook at this pin; russh rejects those opens inline
//! (administratively prohibited), which is the decline the never- //! (administratively prohibited), which is the decline the never-
//! feature contract needs, but it is an inherited posture: a pin bump //! feature contract needs, but it is an inherited posture: a pin bump
//! that changed it would surface here as an accepted-then-dropped //! that changed it would surface here as an accepted-then-dropped
//! channel (third-party-upstream note; file upstream first). //! channel (third-party-upstream note; file upstream first).
//! //!
//! The session rows are **parse-only**: `exec_request` parses first //! The session rows are **parse-only**: `exec_request` parses first
//! (the full grammar, [`crate::dispatch::parse_exec`] — a git route to //! (the full grammar, [`crate::dispatch::parse_exec`], a git route to
//! the git row, a declined-with-reason grammar class to its honest //! the git row, a declined-with-reason grammar class to its honest
//! decline, anything else to the session pipe row); `pty_request` / //! decline, anything else to the session pipe row); `pty_request` /
//! `shell_request` route to the session rows. Nothing is served before //! `shell_request` route to the session rows. Nothing is served before
//! a request exists — no request, no serving, so a request-less //! a request exists: no request, no serving, so a request-less
//! session channel declines as unfeatured by construction. //! session channel declines as unfeatured by construction.
//! **Decline first, alloc never**: every decline here precedes any //! **Decline first, alloc never**: every decline here precedes any
//! channel/resource allocation (a leaked channel alloc is a //! channel/resource allocation (a leaked channel alloc is a
//! capability leak — the POC-3 stage-A rule); the per-length //! capability leak, the POC-3 stage-A rule); the per-length
//! `max_exec_command_len` cap is enforced before the parse itself. //! `max_exec_command_len` cap is enforced before the parse itself.
//! //!
//! The store-and-take channel handoff (`w3-git-mapping`) //! The store-and-take channel handoff (`w3-git-mapping`)
//! //!
//! A confirmed `session` open **stores** its [`Channel`] in //! A confirmed `session` open **stores** its [`Channel`] in
//! per-connection handler state ([`Self::take_stored_channel`]'s //! per-connection handler state ([`Self::take_stored_channel`]'s
//! supplier: `channel_open_session` accepts, then inserts — the //! supplier: `channel_open_session` accepts, then inserts; the
//! POC-3 §A.4 accepted-but-held shape); the git row's `exec_request` //! POC-3 §A.4 accepted-but-held shape); the git row's `exec_request`
//! **takes** it (`channels.remove(&id)` → `into_stream()` in the //! **takes** it (`channels.remove(&id)` → `into_stream()` in the
//! spawned serving task) and the session's packet loop keeps owning //! spawned serving task) and the session's packet loop keeps owning
//! the store: `channel_close` removes whatever it held (a channel //! the store: `channel_close` removes whatever it held (a channel
//! that served or declined without an exec frees there), and every //! that served or declined without an exec frees there), and every
//! mutation happens on the loop's task — single-writer, no lock. A //! mutation happens on the loop's task: single-writer, no lock. A
//! git exec on a channel whose stored handle is already gone //! git exec on a channel whose stored handle is already gone
//! (re-served, or never held — unvanilla) declines serving-path. //! (re-served, or never held, which is unvanilla) declines serving-path.
//! //!
//! ## The session rows' real serving paths (`w3-tty-session`) //! ## The session rows' real serving paths (`w3-tty-session`)
//! //!
//! The pty/shell/pipe rows replaced their wave-3 skeleton stubs with //! The pty/shell/pipe rows replaced their wave-3 skeleton stubs with
//! the alktty translation bridge ([`crate::tty_map`]): the pty row //! the alktty translation bridge ([`crate::tty_map`]): the pty row
//! stores the negotiated `TerminalParamsWire` ([`Self::ptys`]'s //! stores the negotiated `TerminalParamsWire` ([`Self::ptys`]'s
//! supplier — taken by the later shell/exec request on the same //! supplier, taken by the later shell/exec request on the same
//! channel, `ssh -t host "cmd"`'s pty-then-exec); the shell and pipe //! channel, `ssh -t host "cmd"`'s pty-then-exec); the shell and pipe
//! rows take the stored channel the same way the git row does and //! rows take the stored channel the same way the git row does and
//! spawn `serve_tty_row` over the configured seam — locally-built //! spawn `serve_tty_row` over the configured seam, with locally-built
//! negotiation, unset-fields-decline (OQ-08: `shell_argv` unset → //! negotiation, unset-fields-decline (OQ-08: `shell_argv` unset →
//! the shell row declines, `exec_wrapper_argv` unset → the pipe row //! the shell row declines, `exec_wrapper_argv` unset → the pipe row
//! declines, `session_seam` unset → both rows and the pty row //! declines, `session_seam` unset → both rows and the pty row
@@ -118,15 +118,15 @@
//! ## The control rows' per-channel surface (`w3-tty-session`) //! ## The control rows' per-channel surface (`w3-tty-session`)
//! //!
//! `window_change_request` and `signal` arrive on the packet loop //! `window_change_request` and `signal` arrive on the packet loop
//! **after** the serving task owns the wire — the spawned bridge is //! **after** the serving task owns the wire, and the spawned bridge is
//! reached through [`Self::bridges`]' per-channel surface //! reached through [`Self::bridges`]' per-channel surface
//! ([`crate::tty_map::BridgeSurface`]: the bounded control mpsc's //! ([`crate::tty_map::BridgeSurface`]: the bounded control mpsc's
//! `try_send` — never awaited on the packet loop — plus the //! `try_send` (never awaited on the packet loop), plus the
//! per-channel cancel). A resize for a channel whose pty is stored //! per-channel cancel). A resize for a channel whose pty is stored
//! but not yet serving updates the stored params (the final size //! but not yet serving updates the stored params (the final size
//! wins); signal names are accepted verbatim (the backend owns the //! wins); signal names are accepted verbatim (the backend owns the
//! semantics). Unfeatured builds emit the request-level decline //! semantics). Unfeatured builds emit the request-level decline
//! event with no reply — the control rows are fire-and-forget, no //! event with no reply; the control rows are fire-and-forget, no
//! SSH reply exists to send. [`Self::channel_close`] releases the //! SSH reply exists to send. [`Self::channel_close`] releases the
//! surface (firing the cancel: the alktty kill-on-drop contract is //! surface (firing the cancel: the alktty kill-on-drop contract is
//! the cancelled session's cleanup) along with the channel and pty //! the cancelled session's cleanup) along with the channel and pty
@@ -137,45 +137,45 @@
//! ## The spawn-and-return structural constraint (ADR-006 decision 4) //! ## The spawn-and-return structural constraint (ADR-006 decision 4)
//! //!
//! russh at this pin runs handler callbacks **inline inside the //! russh at this pin runs handler callbacks **inline inside the
//! session's packet loop** — a blocking handler suspends all packet //! session's packet loop**: a blocking handler suspends all packet
//! routing and deadlocks against the peer's follow-up bytes (POC-3 //! routing and deadlocks against the peer's follow-up bytes (POC-3
//! proved by construction with receive-pack). Every serving path is //! proved by construction with receive-pack). Every serving path is
//! therefore spawned through [`crate::serve::spawn_serving_path`] and //! therefore spawned through [`crate::serve::spawn_serving_path`] and
//! the handler returns fast: the serving rows (the git row's alkgit //! the handler returns fast: the serving rows (the git row's alkgit
//! dispatch, the session rows' config-driven declines and //! dispatch, the session rows' config-driven declines and
//! [`crate::tty_map`]'s bridge) all clone `session.handle()`, spawn //! [`crate::tty_map`]'s bridge) all clone `session.handle()`, spawn
//! through the clone, and are gone from the hook's stack — no hook //! through the clone, and are gone from the hook's stack; no hook
//! awaits a serving path inline. The decline keeps its wire shape //! awaits a serving path inline. The decline keeps its wire shape
//! (failure then close), riding the spawn. Exit/status reporting from //! (failure then close), riding the spawn. Exit/status reporting from
//! spawned tasks rides the same cloned handle //! spawned tasks rides the same cloned handle
//! ([`crate::serve::report_exit_status_then_close`] — the mechanics; //! ([`crate::serve::report_exit_status_then_close`], the mechanics;
//! the tty rows' derivation rules live in [`crate::tty_map`]), pinned //! the tty rows' derivation rules live in [`crate::tty_map`]), pinned
//! end-to-end by the mechanics rows in `tests/spawn_return.rs` and the //! end-to-end by the mechanics rows in `tests/spawn_return.rs` and the
//! mapping rows in `tests/git_mapping.rs` / `tests/tty_mapping.rs`. //! mapping rows in `tests/git_mapping.rs` / `tests/tty_mapping.rs`.
//! //!
//! The open-level declines (unfeatured/never-feature rows, the //! The open-level declines (unfeatured/never-feature rows, the
//! direct rows' pre-admit classes) stay inline by class: they answer //! direct rows' pre-admit classes) stay inline by class: they answer
//! the open *before* any channel is owned — nothing is served yet, //! the open *before* any channel is owned: nothing is served yet,
//! so nothing spawns. The route-1 bridge (`w3-tunnel-direct`) moved //! so nothing spawns. The route-1 bridge (`w3-tunnel-direct`) moved
//! the direct rows' accept past the fabric open: the accept and the //! the direct rows' accept past the fabric open: the accept and the
//! pump ride the spawned task, per the same constraint. The `-R` //! pump ride the spawned task, per the same constraint. The `-R`
//! listen rows' family-ward dial is the one **inline await** on the //! listen rows' family-ward dial is the one **inline await** on the
//! loop (`w3-tunnel-listen`'s recorded decision: the global request's //! loop (`w3-tunnel-listen`'s recorded decision: the global request's
//! reply + the `&mut u32` bound-port out-param are the hook's own — //! reply + the `&mut u32` bound-port out-param are the hook's own,
//! no spawned-task answer exists at this pin — and the fabric's //! no spawned-task answer exists at this pin, and the fabric's
//! answer rides the fabric leg's connections, not this session's //! answer rides the fabric leg's connections, not this session's
//! routing; bounded by `LISTEN_DIAL_TIMEOUT`), while everything under //! routing; bounded by `LISTEN_DIAL_TIMEOUT`), while everything under
//! the reply — the arrivals, the pumps — stays spawned. //! the reply (the arrivals, the pumps) stays spawned.
//! //!
//! Request-level declines: `subsystem_request` declines every name — //! Request-level declines: `subsystem_request` declines every name:
//! `sftp` is the OQ-13 placeholder (the feature exists, the route //! `sftp` is the OQ-13 placeholder (the feature exists, the route
//! does not until alkfs lands); git service names carry only a name, //! does not until alkfs lands); git service names carry only a name,
//! never a repository id, so git can never dispatch as a subsystem //! never a repository id, so git can never dispatch as a subsystem
//! (ADR-006 decision 2); everything else is unimplemented. `env_request` //! (ADR-006 decision 2); everything else is unimplemented. `env_request`
//! declines unconditionally (OQ-09 — no allowlist ships: wire env is //! declines unconditionally (OQ-09; no allowlist ships: wire env is
//! not family config). `x11_request` and `agent_request` decline //! not family config). `x11_request` and `agent_request` decline
//! (ADR-003 §5). The global listen requests (`tcpip_forward` //! (ADR-003 §5). The global listen requests (`tcpip_forward`
//! / `streamlocal_forward` + their cancel forms) — the `-R` rows //! / `streamlocal_forward` + their cancel forms), the `-R` rows
//! (`w3-tunnel-listen`): the real flow under `tunnel` with the listen //! (`w3-tunnel-listen`): the real flow under `tunnel` with the listen
//! seam configured (the family-ward `TunnelListener::open` dial, the //! seam configured (the family-ward `TunnelListener::open` dial, the
//! arrivals' arrival pump through the shared router); every decline //! arrivals' arrival pump through the shared router); every decline
@@ -186,7 +186,7 @@
//! Two decline classes, chosen for vanilla-client observability (POC-1 //! Two decline classes, chosen for vanilla-client observability (POC-1
//! §5: channel-scoped decline, auth intact, no hangs): //! §5: channel-scoped decline, auth intact, no hangs):
//! //!
//! - **Open-level declines** — channel-open failure codes (`x11` //! - **Open-level declines** (channel-open failure codes: `x11`
//! never-feature, unfeatured feature rows, the tunnel rows' //! never-feature, unfeatured feature rows, the tunnel rows'
//! identity-less guard and beyond-cap refusals → administratively //! identity-less guard and beyond-cap refusals → administratively
//! prohibited; the direct rows' cannot-bridge classes (the fabric //! prohibited; the direct rows' cannot-bridge classes (the fabric
@@ -196,54 +196,54 @@
//! connect failed). These //! connect failed). These
//! happen before the channel exists, so the SSH open-failure code is //! happen before the channel exists, so the SSH open-failure code is
//! the honest, richest reason surface. //! the honest, richest reason surface.
//! - **Request-level declines** — [`Session::channel_failure`]: the //! - **Request-level declines** ([`Session::channel_failure`]): the
//! SSH request-reply vocabulary has no failure text, so a request //! SSH request-reply vocabulary has no failure text, so a request
//! decline's "reason" is the routing decision itself (documented //! decline's "reason" is the routing decision itself (documented
//! above, in [`crate::dispatch`]'s grammar, and per feature build in //! above, in [`crate::dispatch`]'s grammar, and per feature build in
//! `mapping.md`'s decline matrix). A *capability* decline answers //! `mapping.md`'s decline matrix). A *capability* decline answers
//! failure and leaves the channel usable (the POC-1 §5 //! failure and leaves the channel usable (the POC-1 §5
//! session-survives-declines posture) — including the git grammar's //! session-survives-declines posture), including the git grammar's
//! declinables (`unknown-service` / `service-only` / //! declinables (`unknown-service` / `service-only` /
//! `invalid-repo-id`, the parsed reply shapes, `w3-git-mapping`); //! `invalid-repo-id`, the parsed reply shapes, `w3-git-mapping`);
//! a *serving-path* decline (a routable row that owns the request's //! a *serving-path* decline (a routable row that owns the request's
//! whole outcome: the seam-unconfigured git row, the session rows' //! whole outcome: the seam-unconfigured git row, the session rows'
//! unset-field and unset-seam declines — `w3-tty-session`'s OQ-08 //! unset-field and unset-seam declines, `w3-tty-session`'s OQ-08
//! shapes — and the identity-less guard) answers failure and closes //! shapes, and the identity-less guard) answers failure and closes
//! the channel — the vanilla-clean shape for a row whose serving //! the channel, the vanilla-clean shape for a row whose serving
//! path cannot exist. The serving-path decline rides the spawned //! path cannot exist. The serving-path decline rides the spawned
//! serving task (ADR-006 decision 4, §The spawn-and-return //! serving task (ADR-006 decision 4, §The spawn-and-return
//! structural constraint above); the wire shape is unchanged. The //! structural constraint above); the wire shape is unchanged. The
//! `channel-not-held` guard is the inline request-level shape (the //! `channel-not-held` guard is the inline request-level shape (the
//! channel that could not be held stays usable). The real serve's //! channel that could not be held stays usable). The real serve's
//! own outcomes are not declines: the substrate maps them (denial //! own outcomes are not declines: the substrate maps them (denial
//! band before any protocol byte; exit-status then close — the //! band before any protocol byte; exit-status then close (the
//! `exit_code` mapping decision recorded in [`crate::git_map`]; //! `exit_code` mapping decision recorded in [`crate::git_map`]);
//! the tty bridge's negotiation-error and exit-chunk decisions in //! the tty bridge's negotiation-error and exit-chunk decisions in
//! [`crate::tty_map`]). //! [`crate::tty_map`]).
//! //!
//! ## The event vocabulary on every decline (`w3-decline-tracing`) //! ## The event vocabulary on every decline (`w3-decline-tracing`)
//! //!
//! Every decline site here emits its event through //! Every decline site here emits its event through
//! [`crate::observe`] — the wave-2 gate's booked finding: the //! [`crate::observe`], the wave-2 gate's booked finding: the
//! deciding context (row, feature build, channel id, decline class, //! deciding context (row, feature build, channel id, decline class,
//! reason) rides the tracing event, not just the routing decision //! reason) rides the tracing event, not just the routing decision
//! documented per build. The vocabulary and its safety posture (no //! documented per build. The vocabulary and its safety posture (no
//! secrets, no username, no fingerprint — structurally) live in that //! secrets, no username, no fingerprint; enforced structurally) live in that
//! module; the serving-path lifecycle (started / ended / failure) //! module; the serving-path lifecycle (started / ended / failure)
//! hangs off the spawned stub body and is what the wave-3 mappings' //! hangs off the spawned stub body and is what the wave-3 mappings'
//! real serving paths inherit. //! real serving paths inherit.
//! //!
//! ## Bounded resources at the handler surface (`w2-bounded-resources`) //! ## Bounded resources at the handler surface (`w2-bounded-resources`)
//! //!
//! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy — //! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy,
//! the "concurrent channels per session — ours" row.) The in-crate //! the "concurrent channels per session: ours" row.) The in-crate
//! concurrent-channel cap ([`crate::channel_cap::ChannelCap`]) is //! concurrent-channel cap ([`crate::channel_cap::ChannelCap`]) is
//! enforced at every `channel_open_*` hook: an accept path beyond //! enforced at every `channel_open_*` hook: an accept path beyond
//! `Limits::max_concurrent_channels` is refused fail-closed at the //! `Limits::max_concurrent_channels` is refused fail-closed at the
//! open ([`crate::dispatch::cap_exceeded_open_decline`] — never //! open ([`crate::dispatch::cap_exceeded_open_decline`]; never
//! queued, never half-open), and the count releases so the cap //! queued, never half-open), and the count releases so the cap
//! recovers: through the `channel_close` hook for established //! recovers: through the `channel_close` hook for established
//! channels, and — `w3-tunnel-direct` — inside the route-1 bridge's //! channels, and, for `w3-tunnel-direct`, inside the route-1 bridge's
//! spawned task on a fabric-open failure (the admitted-but-rejected //! spawned task on a fabric-open failure (the admitted-but-rejected
//! shape: ADR-006 decision 1 rejects the open, so no `channel_close` //! shape: ADR-006 decision 1 rejects the open, so no `channel_close`
//! can ever fire for it; the release mechanism is the shared //! can ever fire for it; the release mechanism is the shared
@@ -251,14 +251,14 @@
//! its two budget surfaces (`w3-tunnel-listen`, recorded in //! its two budget surfaces (`w3-tunnel-listen`, recorded in
//! `channel_cap.rs`): the arrivals admit into the owning route's //! `channel_cap.rs`): the arrivals admit into the owning route's
//! count inside the spawned arrival tasks (a beyond-cap arrival never //! count inside the spawned arrival tasks (a beyond-cap arrival never
//! opens an SSH channel — teardown-at-birth), and the listen count is //! opens an SSH channel, which is teardown-at-birth), and the listen count is
//! its own dedicated counter against the same ceiling (the //! its own dedicated counter against the same ceiling (the
//! listener/channels mix stays unconstrained; both stay bounded). The //! listener/channels mix stays unconstrained; both stay bounded). The
//! order of the //! order of the
//! gates: the feature gate first (an unfeatured build declines //! gates: the feature gate first (an unfeatured build declines
//! everything before the cap matters), then the pure-decline classes //! everything before the cap matters), then the pure-decline classes
//! (the tunnel rows' unset-handle/unix-gate/identity/invalid-target //! (the tunnel rows' unset-handle/unix-gate/identity/invalid-target
//! checks precede the admission — decline-first-alloc-never applies //! checks precede the admission; decline-first-alloc-never applies
//! to the budget too), the cap admission before any accept; the //! to the budget too), the cap admission before any accept; the
//! admitted count is held across the fabric open and released exactly //! admitted count is held across the fabric open and released exactly
//! once on either path. The `max_exec_command_len` cap is //! once on either path. The `max_exec_command_len` cap is
@@ -272,7 +272,7 @@
//! The handler type is deliberately constructible outside this module //! The handler type is deliberately constructible outside this module
//! (re-exported `#[doc(hidden)]` from the crate root) so the wave-1 //! (re-exported `#[doc(hidden)]` from the crate root) so the wave-1
//! contract tests drive these hooks directly against the in-memory //! contract tests drive these hooks directly against the in-memory
//! doubles — the wire-facing `VerifiedPublicKey`/`PublicKeyOffer` hook //! doubles; the wire-facing `VerifiedPublicKey`/`PublicKeyOffer` hook
//! arguments are `#[non_exhaustive]` (only russh constructs them), so //! arguments are `#[non_exhaustive]` (only russh constructs them), so
//! the bridge core is its own method ([`SessionHandler::resolve_bridge`]) //! the bridge core is its own method ([`SessionHandler::resolve_bridge`])
//! the tests call with ordinary key material; the wire-level rows //! the tests call with ordinary key material; the wire-level rows
@@ -308,36 +308,36 @@ use crate::tunnel_map;
use alktty::negotiation::NegotiateRequest; use alktty::negotiation::NegotiateRequest;
/// The per-connection handler (one per mounted stream): russh constructs /// The per-connection handler (one per mounted stream): russh constructs
/// nothing here — [`crate::SshAdapter::handle`] builds one per accepted /// nothing here; [`crate::SshAdapter::handle`] builds one per accepted
/// stream and drives it through `russh::server::run_stream`. /// stream and drives it through `russh::server::run_stream`.
/// ///
/// Carries the serve-config the auth bridge and dispatch consume, the /// Carries the serve-config the auth bridge and dispatch consume, the
/// in-crate concurrent-channel cap (`ChannelCap` — see the private /// in-crate concurrent-channel cap (`ChannelCap`, see the private
/// `channel_cap` module; enforced at every `channel_open_*` accept /// `channel_cap` module; enforced at every `channel_open_*` accept
/// path, bounded resources, AGENTS.md convention 9), and the resolved /// path, bounded resources, AGENTS.md convention 9), and the resolved
/// identity once a publickey auth has been accepted (see /// identity once a publickey auth has been accepted (see
/// [`Self::resolved_identity`] — the wave-3 seam reads it). /// [`Self::resolved_identity`], which the wave-3 seam reads).
pub struct SessionHandler { pub struct SessionHandler {
serve: Arc<ServeConfig>, serve: Arc<ServeConfig>,
/// The in-crate concurrent-channel cap (`w2-bounded-resources`), /// The in-crate concurrent-channel cap (`w2-bounded-resources`),
/// shared as an `Arc` since `w3-tunnel-direct`: the route-1 bridge's /// shared as an `Arc` since `w3-tunnel-direct`: the route-1 bridge's
/// spawned task releases the count on a fabric-open failure (the /// spawned task releases the count on a fabric-open failure (the
/// admitted-but-rejected shape — no `channel_close` can ever fire /// admitted-but-rejected shape, no `channel_close` can ever fire
/// for it; see `channel_cap.rs`'s release-sites record). Admissions /// for it; see `channel_cap.rs`'s release-sites record). Admissions
/// have two sites since `w3-tunnel-listen` — the packet loop's /// have two sites since `w3-tunnel-listen`: the packet loop's
/// open hooks and the `-R` arrival tasks — and releases three; the /// open hooks and the `-R` arrival tasks, and releases three; the
/// mutex makes them sound (the reasoning, recorded in /// mutex makes them sound (the reasoning, recorded in
/// `channel_cap.rs`). /// `channel_cap.rs`).
channel_cap: Arc<ChannelCap>, channel_cap: Arc<ChannelCap>,
/// The `-R` listen leg's per-session bookkeeping /// The `-R` listen leg's per-session bookkeeping
/// (`w3-tunnel-listen`): the minted-listen map (token → pending /// (`w3-tunnel-listen`): the minted-listen map (token → pending
/// listen; written only by the packet loop's hooks — the listen /// listen; written only by the packet loop's hooks. The listen
/// global requests mints, the cancel forms + this handler's /// global requests mints, the cancel forms + this handler's
/// teardown remove, single-writer/no-lock like the `channels` /// teardown remove, single-writer/no-lock like the `channels`
/// store) and the listen count's dedicated /// store) and the listen count's dedicated
/// `max_concurrent_channels`-ceiling counter (a listen is at least /// `max_concurrent_channels`-ceiling counter (a listen is at least
/// one future channel's potential; its own counter keeps the /// one future channel's potential; its own counter keeps the
/// listener/channels mix unconstrained while both stay bounded — /// listener/channels mix unconstrained while both stay bounded,
/// no unbounded listener map). /// no unbounded listener map).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
listeners: HashMap<u32, tunnel_map::PendingListen>, listeners: HashMap<u32, tunnel_map::PendingListen>,
@@ -349,7 +349,7 @@ pub struct SessionHandler {
/// the close releases them. Written only by the packet loop's /// the close releases them. Written only by the packet loop's
/// hooks (`channel_open_session` inserts; the git row's and the /// hooks (`channel_open_session` inserts; the git row's and the
/// session rows' serving dispatches remove for their taken /// session rows' serving dispatches remove for their taken
/// channel; `channel_close` removes) — single-writer, no lock. /// channel; `channel_close` removes); single-writer, no lock.
channels: HashMap<ChannelId, Channel<Msg>>, channels: HashMap<ChannelId, Channel<Msg>>,
/// The session rows' negotiated pty params (`w3-tty-session`): /// The session rows' negotiated pty params (`w3-tty-session`):
/// `pty_request` stores, the later `shell_request`/`exec_request` /// `pty_request` stores, the later `shell_request`/`exec_request`
@@ -376,17 +376,17 @@ impl Drop for SessionHandler {
/// **session-row** serving task of this connection watches it and /// **session-row** serving task of this connection watches it and
/// cancels its alktty session (the kill-on-drop cleanup) instead /// cancels its alktty session (the kill-on-drop cleanup) instead
/// of outliving the connection. The git serving task does not /// of outliving the connection. The git serving task does not
/// watch this signal — it ends through the channel duplex (stream /// watch this signal; it ends through the channel duplex (stream
/// EOF / send failure) once the russh session dies, bounded by the /// EOF / send failure) once the russh session dies, bounded by the
/// alkgit seam's own `session_wall_clock` (the alkgit limits the /// alkgit seam's own `session_wall_clock` (the alkgit limits the
/// assembly configured). The `tunnel` build adds the `-R` /// assembly configured). The `tunnel` build adds the `-R`
/// leg's teardown (`w3-tunnel-listen`): the session's pending /// leg's teardown (`w3-tunnel-listen`): the session's pending
/// listens' tokens leave the shared router (a fabric arrival after /// listens' tokens leave the shared router (a fabric arrival after
/// the session is gone is the correlation-miss class — /// the session is gone is the correlation-miss class,
/// teardown-at-birth) and the listener objects drop here (the /// teardown-at-birth) and the listener objects drop here (the
/// fabric-side reap). Any in-flight arrival already past the /// fabric-side reap). Any in-flight arrival already past the
/// route lookup fails its dead-handle open and releases its /// route lookup fails its dead-handle open and releases its
/// admission — the never-established path. /// admission, the never-established path.
fn drop(&mut self) { fn drop(&mut self) {
#[cfg(feature = "session")] #[cfg(feature = "session")]
self.teardown.fire(); self.teardown.fire();
@@ -401,7 +401,7 @@ impl Drop for SessionHandler {
} }
impl SessionHandler { impl SessionHandler {
/// Builds the handler over the serving configuration — the /// Builds the handler over the serving configuration; the
/// identity provider inside is the assembly-supplied /// identity provider inside is the assembly-supplied
/// `alkcall::core::auth::IdentityProvider`; the bridge only /// `alkcall::core::auth::IdentityProvider`; the bridge only
/// resolves through it. /// resolves through it.
@@ -428,14 +428,14 @@ impl SessionHandler {
/// store-and-take handoff): the git row's and the session rows' /// store-and-take handoff): the git row's and the session rows'
/// dispatches remove the handle the open stored, handing the /// dispatches remove the handle the open stored, handing the
/// stream into the spawned serving task. `None` when nothing is /// stream into the spawned serving task. `None` when nothing is
/// held — a re-served or never-held channel (unvanilla; declined /// held: a re-served or never-held channel (unvanilla; declined
/// serving-path). /// serving-path).
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
fn take_stored_channel(&mut self, channel: ChannelId) -> Option<Channel<Msg>> { fn take_stored_channel(&mut self, channel: ChannelId) -> Option<Channel<Msg>> {
self.channels.remove(&channel) self.channels.remove(&channel)
} }
/// The identity this connection currently rides — `Some` only after /// The identity this connection currently rides, `Some` only after
/// a fingerprint resolution accepted a publickey auth (the bridge's /// a fingerprint resolution accepted a publickey auth (the bridge's
/// fail-closed rejecting paths never leave a weakly-authorized /// fail-closed rejecting paths never leave a weakly-authorized
/// state behind). The wave-3 seam propagation consumes this. /// state behind). The wave-3 seam propagation consumes this.
@@ -447,15 +447,15 @@ impl SessionHandler {
/// cap (bounded resources, `w2-bounded-resources`): room under /// cap (bounded resources, `w2-bounded-resources`): room under
/// `Limits::max_concurrent_channels` admits and counts the channel; /// `Limits::max_concurrent_channels` admits and counts the channel;
/// beyond the cap the open is refused fail-closed with a clean /// beyond the cap the open is refused fail-closed with a clean
/// wire reason ([`crate::dispatch::cap_exceeded_open_decline`]) — /// wire reason ([`crate::dispatch::cap_exceeded_open_decline`]),
/// never queued, never half-open. Called exactly at accept sites — /// never queued, never half-open. Called exactly at accept sites:
/// the packet loop owns every admit call (the single admit writer, /// the packet loop owns every admit call (the single admit writer,
/// [`ChannelCap`]'s recorded reasoning); a pure-decline path /// [`ChannelCap`]'s recorded reasoning); a pure-decline path
/// (unfeatured, never-feature, the tunnel rows' pre-admit classes) /// (unfeatured, never-feature, the tunnel rows' pre-admit classes)
/// never touches the count. Since `w3-tunnel-direct`, the tunnel /// never touches the count. Since `w3-tunnel-direct`, the tunnel
/// rows admit **at the hook and hold the count across the fabric /// rows admit **at the hook and hold the count across the fabric
/// open**: a fabric failure releases inside the spawned bridge /// open**: a fabric failure releases inside the spawned bridge
/// task (the admitted-but-rejected shape — no `channel_close` can /// task (the admitted-but-rejected shape; no `channel_close` can
/// ever fire); a success releases when the established channel /// ever fire); a success releases when the established channel
/// closes ([`Self::channel_close`]). The release sites are disjoint /// closes ([`Self::channel_close`]). The release sites are disjoint
/// per admission by the wire shape. /// per admission by the wire shape.
@@ -467,16 +467,16 @@ impl SessionHandler {
/// The ADR-004 decision 1 bridge core, structured out of the /// The ADR-004 decision 1 bridge core, structured out of the
/// [`Handler::auth_publickey`] hook so the contract tests can drive /// [`Handler::auth_publickey`] hook so the contract tests can drive
/// it directly: the hook's russh-owned argument shape /// it directly: the hook's russh-owned argument shape
/// (`russh::server::VerifiedPublicKey`) is `#[non_exhaustive]` — /// (`russh::server::VerifiedPublicKey`) is `#[non_exhaustive]`,
/// only russh constructs it — but this method takes the two facts /// only russh constructs it, but this method takes the two facts
/// the hook receives (the verified key and the signature's /// the hook receives (the verified key and the signature's
/// algorithm) as ordinary arguments. /// algorithm) as ordinary arguments.
/// ///
/// Ed25519-only gate first (ADR-002 decision 4 — `SkEd25519` and /// Ed25519-only gate first (ADR-002 decision 4: `SkEd25519` and
/// every other scheme share the non-ed25519 decline, which is /// every other scheme share the non-ed25519 decline, which is
/// explicit-over-inherited so it survives a pin bump), then the /// explicit-over-inherited so it survives a pin bump), then the
/// single fingerprint path → the injected provider's /// single fingerprint path → the injected provider's
/// `resolve_from_fingerprint`. No username parameter exists — the /// `resolve_from_fingerprint`. No username parameter exists; the
/// user string is never an identity input (OQ-03). `None` is the /// user string is never an identity input (OQ-03). `None` is the
/// fail-closed reject disposition; `Some` carries the identity the /// fail-closed reject disposition; `Some` carries the identity the
/// hook stores in handler state. /// hook stores in handler state.
@@ -503,7 +503,7 @@ impl SessionHandler {
/// The `session` channel-open gate (ADR-003 feature graph): /// The `session` channel-open gate (ADR-003 feature graph):
/// session channels open only where a session-serving feature /// session channels open only where a session-serving feature
/// exists — `session` ∨ `git`, because git's parse path lives /// exists: `session` ∨ `git`, because git's parse path lives
/// inside session channels. /// inside session channels.
fn serves_session_channels(&self) -> bool { fn serves_session_channels(&self) -> bool {
cfg!(feature = "session") || cfg!(feature = "git") cfg!(feature = "session") || cfg!(feature = "git")
@@ -554,7 +554,7 @@ impl russh::server::Handler for SessionHandler {
} }
} }
/// ADR-004 decision 2: accept-and-delegate, explicit — the probe /// ADR-004 decision 2: accept-and-delegate, explicit; the probe
/// hook consumes no auth attempt (POC-1 verified), so keeping it at /// hook consumes no auth attempt (POC-1 verified), so keeping it at
/// `Auth::Accept` keeps the `max_auth_attempts` counting honest; /// `Auth::Accept` keeps the `max_auth_attempts` counting honest;
/// the real disposition is [`Self::auth_publickey`]'s. /// the real disposition is [`Self::auth_publickey`]'s.
@@ -566,14 +566,14 @@ impl russh::server::Handler for SessionHandler {
Ok(Auth::Accept) Ok(Auth::Accept)
} }
/// ADR-004 decision 3: explicit family-contract decline — never the /// ADR-004 decision 3: explicit family-contract decline, never the
/// inherited default (a pin bump could silently change it). /// inherited default (a pin bump could silently change it).
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> { async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_none", "method-unimplemented"); observe::decline_auth("auth_none", "method-unimplemented");
Ok(Auth::reject()) Ok(Auth::reject())
} }
/// ADR-004 decision 3: explicit family-contract decline — password /// ADR-004 decision 3: explicit family-contract decline; password
/// auth does not exist on this door; ADR-005's key custody is the /// auth does not exist on this door; ADR-005's key custody is the
/// whole story. /// whole story.
async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> { async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
@@ -581,7 +581,7 @@ impl russh::server::Handler for SessionHandler {
Ok(Auth::reject()) Ok(Auth::reject())
} }
/// ADR-004 decision 3 + OQ-04 (resolved): explicit decline — the /// ADR-004 decision 3 + OQ-04 (resolved): explicit decline, since the
/// CA-trust story does not exist in the family identity model; /// CA-trust story does not exist in the family identity model;
/// future support is new-ADR territory. /// future support is new-ADR territory.
async fn auth_openssh_certificate( async fn auth_openssh_certificate(
@@ -604,16 +604,16 @@ impl russh::server::Handler for SessionHandler {
Ok(Auth::reject()) Ok(Auth::reject())
} }
/// A counted channel closed — the in-crate concurrent-channel /// A counted channel closed: the in-crate concurrent-channel
/// cap's decrement (`w2-bounded-resources`): every fired /// cap's decrement (`w2-bounded-resources`): every fired
/// `channel_close` corresponds to exactly one previously-admitted /// `channel_close` corresponds to exactly one previously-admitted
/// open (russh only routes `CHANNEL_CLOSE` for established /// open (russh only routes `CHANNEL_CLOSE` for established
/// channels — a declined open never established one — and consumes /// channels, a declined open never established one, and consumes
/// the close at most once), so the count recovers through this /// the close at most once), so the count recovers through this
/// single release point, saturating in [`ChannelCap::release`] on /// single release point, saturating in [`ChannelCap::release`] on
/// top. The spawned serving paths never touch the count directly: /// top. The spawned serving paths never touch the count directly:
/// their closes ride the cloned handle, the client's close reply /// their closes ride the cloned handle, the client's close reply
/// cycles back through the packet loop, and this hook fires — the /// cycles back through the packet loop, and this hook fires; the
/// single-writer reasoning in [`ChannelCap`]. /// single-writer reasoning in [`ChannelCap`].
async fn channel_close( async fn channel_close(
&mut self, &mut self,
@@ -634,15 +634,15 @@ impl russh::server::Handler for SessionHandler {
/// The session-channel open gate (ADR-003 feature graph, /// The session-channel open gate (ADR-003 feature graph,
/// `session` ∨ `git`), then the in-crate concurrent-channel cap /// `session` ∨ `git`), then the in-crate concurrent-channel cap
/// ([`Self::admit_channel_open`] — bounded resources, /// ([`Self::admit_channel_open`], bounded resources,
/// `w2-bounded-resources`): unfeatured builds decline at the open /// `w2-bounded-resources`): unfeatured builds decline at the open
/// — never a half-open channel — and a featured open beyond the /// and never a half-open channel; a featured open beyond the
/// cap is refused fail-closed with a clean wire reason /// cap is refused fail-closed with a clean wire reason
/// ([`crate::dispatch::cap_exceeded_open_decline`]), never queued. /// ([`crate::dispatch::cap_exceeded_open_decline`]), never queued.
/// A confirmed open accepts the channel and counts it; the count /// A confirmed open accepts the channel and counts it; the count
/// releases when the channel closes ([`Self::channel_close`]). The /// releases when the channel closes ([`Self::channel_close`]). The
/// parse-only dispatch serves the accepted channel through its /// parse-only dispatch serves the accepted channel through its
/// requests (nothing serves request-less — ADR-006 as amended). /// requests (nothing serves request-less, per ADR-006 as amended).
async fn channel_open_session( async fn channel_open_session(
&mut self, &mut self,
channel: Channel<Msg>, channel: Channel<Msg>,
@@ -664,7 +664,7 @@ impl russh::server::Handler for SessionHandler {
Ok(()) Ok(())
} }
/// The x11 channel open: **always declines** — a never-feature /// The x11 channel open: **always declines**, a never-feature
/// (ADR-003 §5), declined in every build, never feature-gated. The /// (ADR-003 §5), declined in every build, never feature-gated. The
/// cap never applies here: a count is consumed only at an accept /// cap never applies here: a count is consumed only at an accept
/// path, and this hook never has one. /// path, and this hook never has one.
@@ -684,14 +684,14 @@ impl russh::server::Handler for SessionHandler {
/// The `direct-tcpip` open gate (`tunnel`) and its route-1 bridge /// The `direct-tcpip` open gate (`tunnel`) and its route-1 bridge
/// dispatch (`w3-tunnel-direct`): unfeatured declines at the open; /// dispatch (`w3-tunnel-direct`): unfeatured declines at the open;
/// featured rows build the bridge target from the SSH open /// featured rows build the bridge target from the SSH open
/// arguments (`SubstrateAddr::HostPort` + `Substrate::Tcp` — an /// arguments (`SubstrateAddr::HostPort` + `Substrate::Tcp`; an
/// out-of-range wire port is an impossible target, declined /// out-of-range wire port is an impossible target, declined
/// pre-admission) and route to [`Self::dispatch_direct_open`]: /// pre-admission) and route to [`Self::dispatch_direct_open`]:
/// pre-admit decline classes first (the unset fabric handle /// pre-admit decline classes first (the unset fabric handle
/// declining under the feature — the unset-declines posture — and /// declining under the feature, the unset-declines posture, and
/// the unreachable-in-practice `no-identity` guard), then the cap /// the unreachable-in-practice `no-identity` guard), then the cap
/// admission **at the hook, held across the fabric open**, then /// admission **at the hook, held across the fabric open**, then
/// the spawned bridge per spawn-and-return — /// the spawned bridge per spawn-and-return:
/// `TunnelSession::open_direct` → accept only after the fabric /// `TunnelSession::open_direct` → accept only after the fabric
/// open succeeds (`ConnectFailed` on failure, the count released /// open succeeds (`ConnectFailed` on failure, the count released
/// in the task), `take_halves()` pumped against the taken channel. /// in the task), `take_halves()` pumped against the taken channel.
@@ -737,7 +737,7 @@ impl russh::server::Handler for SessionHandler {
} }
/// The `forwarded-tcpip` open gate (`tunnel`): an **inbound** open /// The `forwarded-tcpip` open gate (`tunnel`): an **inbound** open
/// of this channel type — arriving *from* the vanilla client — is /// of this channel type (arriving *from* the vanilla client) is
/// not the `-R` leg at all (the `-R` data leg is the door's own /// not the `-R` leg at all (the `-R` data leg is the door's own
/// server-initiated opens via [`crate::tunnel_map`]'s arrival /// server-initiated opens via [`crate::tunnel_map`]'s arrival
/// pump; a vanilla client never initiates `forwarded-tcpip`, /// pump; a vanilla client never initiates `forwarded-tcpip`,
@@ -769,10 +769,10 @@ impl russh::server::Handler for SessionHandler {
/// The `direct-streamlocal` open gate (`tunnel`) and its route-1 /// The `direct-streamlocal` open gate (`tunnel`) and its route-1
/// bridge dispatch: the `direct-tcpip` shape with the OQ-11 /// bridge dispatch: the `direct-tcpip` shape with the OQ-11
/// **runtime-Unix gate** — the row compiles wherever `tunnel` /// **runtime-Unix gate**: the row compiles wherever `tunnel`
/// compiles (universal compile, no per-platform feature fork), but /// compiles (universal compile, no per-platform feature fork), but
/// off-Unix it declines at the open **before any fabric call** /// off-Unix it declines at the open **before any fabric call**
/// (never a partial bridge — the gate precedes the unset-handle /// (never a partial bridge; the gate precedes the unset-handle
/// check, the identity guard, and the cap admission), and on-Unix /// check, the identity guard, and the cap admission), and on-Unix
/// it rides the same bridge with `SubstrateAddr::Path` + /// it rides the same bridge with `SubstrateAddr::Path` +
/// `Substrate::Unix`. /// `Substrate::Unix`.
@@ -821,9 +821,9 @@ impl russh::server::Handler for SessionHandler {
/// The exec-grammar dispatch (ADR-006 point 3 as amended; the /// The exec-grammar dispatch (ADR-006 point 3 as amended; the
/// full grammar, `w3-git-mapping`): the per-length cap checks the /// full grammar, `w3-git-mapping`): the per-length cap checks the
/// wire bytes first (pre-parse, pre-allocation — decline first, /// wire bytes first (pre-parse, pre-allocation; decline first,
/// alloc never), then [`crate::dispatch::parse_exec`] routes: /// alloc never), then [`crate::dispatch::parse_exec`] routes:
/// a git route to the git row (feature-gated — a session-only /// a git route to the git row (feature-gated; a session-only
/// build declines it as its gate), a grammar declinable to its /// build declines it as its gate), a grammar declinable to its
/// honest request-level decline, anything else to the session /// honest request-level decline, anything else to the session
/// pipe row. Invalid UTF-8 parses to the pipe row (parse-then- /// pipe row. Invalid UTF-8 parses to the pipe row (parse-then-
@@ -889,11 +889,11 @@ impl russh::server::Handler for SessionHandler {
/// The `pty_request` row (`session`): the args → /// The `pty_request` row (`session`): the args →
/// `TerminalParamsWire` (ttymodes → the reserved `{}` JSON, /// `TerminalParamsWire` (ttymodes → the reserved `{}` JSON,
/// OQ-44), stored per channel for the later shell/exec request on /// OQ-44), stored per channel for the later shell/exec request on
/// the same channel — after the seam check (a pty whose session /// the same channel, after the seam check (a pty whose session
/// can never serve is a declined allocation, /// can never serve is a declined allocation,
/// `seam-unconfigured`). Unfeatured builds decline the /// `seam-unconfigured`). Unfeatured builds decline the
/// capability. The successful row answers `channel_success` /// capability. The successful row answers `channel_success`
/// (POC-3 §D.1's shape — serving starts at the shell/exec /// (POC-3 §D.1's shape: serving starts at the shell/exec
/// request). /// request).
async fn pty_request( async fn pty_request(
&mut self, &mut self,
@@ -930,11 +930,11 @@ impl russh::server::Handler for SessionHandler {
/// The `shell_request` row (`session`): the serve-config shell /// The `shell_request` row (`session`): the serve-config shell
/// argv, verbatim, as `NegotiateRequest.cmd`; the stored pty /// argv, verbatim, as `NegotiateRequest.cmd`; the stored pty
/// params consumed (`None` → pipe-mode shell — decided: a pty-less /// params consumed (`None` → pipe-mode shell; decided: a pty-less
/// shell request is served, `ssh -T host` is legitimate vanilla); /// shell request is served, `ssh -T host` is legitimate vanilla);
/// unset-declines first (OQ-08: `shell_argv` unset → /// unset-declines first (OQ-08: `shell_argv` unset →
/// `shell-unconfigured`; the seam unset → `seam-unconfigured` /// `shell-unconfigured`; the seam unset → `seam-unconfigured`
/// — both the spawned serving-path shape). A routed request /// (both the spawned serving-path shape). A routed request
/// answers `channel_success` inline; the exit chunk reports the /// answers `channel_success` inline; the exit chunk reports the
/// outcome, status-before-close from the spawned task. /// outcome, status-before-close from the spawned task.
async fn shell_request( async fn shell_request(
@@ -998,13 +998,13 @@ impl russh::server::Handler for SessionHandler {
Ok(()) Ok(())
} }
/// `window_change_request` (`session`): the resize control row — /// `window_change_request` (`session`): the resize control row,
/// forwarded into the serving bridge's bounded control surface /// forwarded into the serving bridge's bounded control surface
/// (`try_send`, never awaited on the packet loop); for a channel /// (`try_send`, never awaited on the packet loop); for a channel
/// whose pty is stored but not yet serving, the stored params /// whose pty is stored but not yet serving, the stored params
/// resize instead (the final size wins — vanilla sshd's own /// resize instead (the final size wins, vanilla sshd's own
/// ordering); with neither, the event is dropped. Fire-and-forget /// ordering); with neither, the event is dropped. Fire-and-forget
/// on the wire — no SSH reply exists, so a decline sends none /// on the wire; no SSH reply exists, so a decline sends none
/// (the event is the record). Signal names: see [`Self::signal`]. /// (the event is the record). Signal names: see [`Self::signal`].
async fn window_change_request( async fn window_change_request(
&mut self, &mut self,
@@ -1050,11 +1050,11 @@ impl russh::server::Handler for SessionHandler {
Ok(()) Ok(())
} }
/// The `signal` row (`session`): accepted verbatim — no /// The `signal` row (`session`): accepted verbatim, with no
/// alkssh-side name filter (the backend owns the semantics; /// alkssh-side name filter (the backend owns the semantics;
/// alktty's control handle applies its own default kill for /// alktty's control handle applies its own default kill for
/// names it rejects, and the pipe-mode pid-only vs PTY /// names it rejects, and the pipe-mode pid-only vs PTY
/// process-group scope is backend semantics) — so there is no /// process-group scope is backend semantics), so there is no
/// decline shape: the request is fire-and-forget on the SSH /// decline shape: the request is fire-and-forget on the SSH
/// wire, and the bridge's bounded surface is the only path. An /// wire, and the bridge's bounded surface is the only path. An
/// unfeatured build emits the decline event with no reply. /// unfeatured build emits the decline event with no reply.
@@ -1082,7 +1082,7 @@ impl russh::server::Handler for SessionHandler {
} }
/// Every subsystem name declines (unimplemented on this door): /// Every subsystem name declines (unimplemented on this door):
/// `subsystem("sftp")` is the OQ-13 placeholder — the feature /// `subsystem("sftp")` is the OQ-13 placeholder; the feature
/// exists, the route does not until alkfs lands; git service /// exists, the route does not until alkfs lands; git service
/// names carry only a name, never a repository id, so git can /// names carry only a name, never a repository id, so git can
/// never dispatch as a subsystem (ADR-006 decision 2, POC-3 /// never dispatch as a subsystem (ADR-006 decision 2, POC-3
@@ -1101,7 +1101,7 @@ impl russh::server::Handler for SessionHandler {
} }
/// `env_request` declines unconditionally (OQ-09: no allowlist /// `env_request` declines unconditionally (OQ-09: no allowlist
/// ships — wire env is not family config; the assembly supplies /// ships, since wire env is not family config; the assembly supplies
/// the session env through [`crate::ServeConfig::session_env`]). /// the session env through [`crate::ServeConfig::session_env`]).
async fn env_request( async fn env_request(
&mut self, &mut self,
@@ -1132,7 +1132,7 @@ impl russh::server::Handler for SessionHandler {
} }
/// Agent forwarding on a session session-channel: always declines /// Agent forwarding on a session session-channel: always declines
/// (never-feature, ADR-003 §5 — agent forwarding is the /// (never-feature, ADR-003 §5; agent forwarding is the
/// anti-pattern of the alkvault custody model). `false` is the /// anti-pattern of the alkvault custody model). `false` is the
/// decline; russh replies the channel failure. /// decline; russh replies the channel failure.
async fn agent_request( async fn agent_request(
@@ -1146,7 +1146,7 @@ impl russh::server::Handler for SessionHandler {
/// The `tcpip-forward` listen request (`-R` ingress, `w3-tunnel- /// The `tcpip-forward` listen request (`-R` ingress, `w3-tunnel-
/// listen`): the gate is [`Self::serves_tunnels`]; the real flow /// listen`): the gate is [`Self::serves_tunnels`]; the real flow
/// rides [`listen_request_dispatch`] (`tunnel` builds) — the /// rides [`listen_request_dispatch`] (`tunnel` builds), the
/// family-ward `TunnelListener::open` dial, the bound-port out-param /// family-ward `TunnelListener::open` dial, the bound-port out-param
/// (`port = 0` → the fabric's actually-bound number, the sshd /// (`port = 0` → the fabric's actually-bound number, the sshd
/// parity), the route + listener registration. Unfeatured builds /// parity), the route + listener registration. Unfeatured builds
@@ -1179,13 +1179,13 @@ impl russh::server::Handler for SessionHandler {
} }
/// The `cancel-tcpip-forward` cancel form (`-KR`, mux teardown, /// The `cancel-tcpip-forward` cancel form (`-KR`, mux teardown,
/// `ExitOnForwardFailure`): 1:1 into the family listener's teardown — /// `ExitOnForwardFailure`): 1:1 into the family listener's teardown,
/// the pending listen matched by (the wire address as the request /// the pending listen matched by (the wire address as the request
/// carried it, the bound port), the route removed **before** any /// carried it, the bound port), the route removed **before** any
/// teardown (a racing arrival is already the correlation-miss /// teardown (a racing arrival is already the correlation-miss
/// class), the listener count released, the fabric channel's /// class), the listener count released, the fabric channel's
/// teardown spawned (`TunnelListener::close`). `false` for a /// teardown spawned (`TunnelListener::close`). `false` for a
/// (address, port) pair with no pending listen — the sshd parity /// (address, port) pair with no pending listen; the sshd parity
/// (`cancel of a not-found forward`). /// (`cancel of a not-found forward`).
async fn cancel_tcpip_forward( async fn cancel_tcpip_forward(
&mut self, &mut self,
@@ -1206,7 +1206,7 @@ impl russh::server::Handler for SessionHandler {
} }
/// The `streamlocal-forward` listen request: the same listen flow /// The `streamlocal-forward` listen request: the same listen flow
/// as `tcpip_forward` over the unix substrate — with the OQ-11 /// as `tcpip_forward` over the unix substrate, with the OQ-11
/// runtime-Unix gate riding the serving flow it serves (off-Unix /// runtime-Unix gate riding the serving flow it serves (off-Unix
/// it declines before any fabric call; universal compile, no /// it declines before any fabric call; universal compile, no
/// per-platform feature fork). No port out-param (the unix listen /// per-platform feature fork). No port out-param (the unix listen
@@ -1271,34 +1271,34 @@ impl russh::server::Handler for SessionHandler {
/// gate order is decline-first-alloc-never, each consumer checked /// gate order is decline-first-alloc-never, each consumer checked
/// before the next allocation is taken: /// before the next allocation is taken:
/// ///
/// 1. **The `no-identity` guard** — unreachable on the wire (russh /// 1. **The `no-identity` guard**: unreachable on the wire (russh
/// authorizes before requests serve) and fail-closed: never /// authorizes before requests serve) and fail-closed: never
/// anonymous listens. /// anonymous listens.
/// 2. **The seam checks** — the fabric handle or the listen seam /// 2. **The seam checks**: the fabric handle or the listen seam
/// (`listen_router` absent: an egress-only door — the seam unset or /// (`listen_router` absent: an egress-only door, whether the seam unset or
/// the registration never ran) decline `seam-unconfigured` before /// the registration never ran) decline `seam-unconfigured` before
/// any dial. /// any dial.
/// 3. **The OQ-11 unix gate** — the streamlocal variant's off-Unix /// 3. **The OQ-11 unix gate**: the streamlocal variant's off-Unix
/// decline rides its hook (checked before this fn runs, so the /// decline rides its hook (checked before this fn runs, so the
/// unreachable-on-Unix-only row never here). /// unreachable-on-Unix-only row never here).
/// 4. **The listener cap** — the listen count's dedicated /// 4. **The listener cap**: the listen count's dedicated
/// `max_concurrent_channels`-ceiling admission; beyond it /// `max_concurrent_channels`-ceiling admission; beyond it
/// `cap-exceeded`. /// `cap-exceeded`.
/// ///
/// Then the family-ward dial ([`crate::tunnel_map::open_listen`], the /// Then the family-ward dial ([`crate::tunnel_map::open_listen`], the
/// `resource = <the wire bind endpoint>` naming convention) — awaited /// `resource = <the wire bind endpoint>` naming convention), awaited
/// inline (the recorded inline decision: the global request's reply + /// inline (the recorded inline decision: the global request's reply +
/// the `&mut u32` out-param are the hook's; the fabric answer rides /// the `&mut u32` out-param are the hook's; the fabric answer rides
/// the fabric leg, not this session's packet loop; the dial is /// the fabric leg, not this session's packet loop; the dial is
/// bounded by `LISTEN_DIAL_TIMEOUT`, so the stall is bounded). The /// bounded by `LISTEN_DIAL_TIMEOUT`, so the stall is bounded). The
/// reply's `bound` port writes the out-param (`port = 0` → the /// reply's `bound` port writes the out-param (`port = 0` → the
/// actually-bound number, sshd parity); the route registers into the /// actually-bound number, sshd parity); the route registers into the
/// shared router — the register is the collision pre-check /// shared router; the register is the collision pre-check
/// (`w4-router-collision`): a mint on an already-held token refuses /// (`w4-router-collision`): a mint on an already-held token refuses
/// fail-closed (`token-collision`, the minted listener torn down, the /// fail-closed (`token-collision`, the minted listener torn down, the
/// cap released — a collided mint can never steal a live route; the /// cap released, so a collided mint can never steal a live route; the
/// decline rides `Ok(false)`, never the error taxonomy, since a /// decline rides `Ok(false)`, never the error taxonomy, since a
/// global-request hook's `Err` would kill the whole session) — and /// global-request hook's `Err` would kill the whole session), and
/// the pending listen into the session's /// the pending listen into the session's
/// packet-loop-only map. `Ok(true)`'s answer is the only success /// packet-loop-only map. `Ok(true)`'s answer is the only success
/// shape; every decline path answers the vanilla client's /// shape; every decline path answers the vanilla client's
@@ -1400,15 +1400,15 @@ async fn listen_request_dispatch(
/// listener's teardown. Match rule (sshd parity): the wire endpoint as /// listener's teardown. Match rule (sshd parity): the wire endpoint as
/// the original request carried it + the bound port for the tcp /// the original request carried it + the bound port for the tcp
/// variants (the client sends the same normalized string it sent /// variants (the client sends the same normalized string it sent
/// originally, and the *allocated* port for `port = 0` mints — the /// originally, and the *allocated* port for `port = 0` mints, the
/// value the listen reply carried); the path alone for the unix /// value the listen reply carried); the path alone for the unix
/// variant. Order: the entry leaves the session map and the route /// variant. Order: the entry leaves the session map and the route
/// leaves the shared router before any family teardown spawns (a /// leaves the shared router before any family teardown spawns (a
/// racing arrival that has not yet cloned its route is already the /// racing arrival that has not yet cloned its route is already the
/// correlation-miss class — no post-cancel half-serve; one that /// correlation-miss class, so no post-cancel half-serve; one that
/// cloned just before completes independently — the recorded /// cloned just before completes independently (the recorded
/// caveat), then the listener count releases, then the family /// caveat), then the listener count releases, then the family
/// teardown spawns (`TunnelListener::close` inside the spawn — a /// teardown spawns (`TunnelListener::close` inside the spawn, a
/// bounded local shutdown + entry reap, not a serving path the hook /// bounded local shutdown + entry reap, not a serving path the hook
/// may await). `Ok(true)` iff a pending listen matched. /// may await). `Ok(true)` iff a pending listen matched.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -1449,7 +1449,7 @@ fn cancel_listen_dispatch(
/// The request-level decline shape for an unfeatured/unimplemented /// The request-level decline shape for an unfeatured/unimplemented
/// capability: fail the request and leave the channel usable (POC-1 /// capability: fail the request and leave the channel usable (POC-1
/// §5 — session survives declines, no state poisoning). The decline /// §5: session survives declines, no state poisoning). The decline
/// event is emitted by the caller (the hook carries the row/reason /// event is emitted by the caller (the hook carries the row/reason
/// context); this is the wire shape only. /// context); this is the wire shape only.
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) { async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
@@ -1459,16 +1459,16 @@ async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
/// The spawned serving-path decline body (`w3-git-mapping`'s shape, /// The spawned serving-path decline body (`w3-git-mapping`'s shape,
/// reused by `w3-tty-session`'s config-driven declines): fail the /// reused by `w3-tty-session`'s config-driven declines): fail the
/// request and close the channel, through the cloned handle the task /// request and close the channel, through the cloned handle the task
/// was handed — the vanilla-clean shape for a routed row that owns /// was handed, the vanilla-clean shape for a routed row that owns
/// the request's whole outcome. The helper name keeps the wave-3 /// the request's whole outcome. The helper name keeps the wave-3
/// history (the decline IS the path it spawns). Decline-first-alloc- /// history (the decline IS the path it spawns). Decline-first-alloc-
/// never holds structurally: nothing is allocated before the failure /// never holds structurally: nothing is allocated before the failure
/// lands — the spawn's handle clone is the only resource ever taken. /// lands; the spawn's handle clone is the only resource ever taken.
/// ///
/// The serving-path lifecycle events ride this body /// The serving-path lifecycle events ride this body
/// ([`crate::observe`]): started (the task's first action), the /// ([`crate::observe`]): started (the task's first action), the
/// serving-path decline (class `serving`, the caller's vocabulary /// serving-path decline (class `serving`, the caller's vocabulary
/// reason), and — after the wire reporting has completed — ended, or /// reason), and, after the wire reporting has completed, ended, or
/// failed when a report send could not complete (the one warn in the /// failed when a report send could not complete (the one warn in the
/// vocabulary). /// vocabulary).
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
@@ -1493,7 +1493,7 @@ async fn seam_stub_decline(
/// Spawns a serving-path decline as the row's serving path (ADR-006 /// Spawns a serving-path decline as the row's serving path (ADR-006
/// decision 4): the handle is cloned here, the decline runs on its own /// decision 4): the handle is cloned here, the decline runs on its own
/// task, and the hook returns — no serving path awaits inline. The /// task, and the hook returns; no serving path awaits inline. The
/// row never serves, so the decline *is* the path: failure + close, /// row never serves, so the decline *is* the path: failure + close,
/// event-shaped ([`crate::observe`]) with the caller's reason. /// event-shaped ([`crate::observe`]) with the caller's reason.
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
@@ -1509,14 +1509,14 @@ fn spawn_seam_stub(
} }
/// The git row's routing stage (feature-gated, `w3-git-mapping`): /// The git row's routing stage (feature-gated, `w3-git-mapping`):
/// validation of the normalized id ([`crate::git_map::repo_id_validates`] — /// validation of the normalized id ([`crate::git_map::repo_id_validates`],
/// reply-shaped `invalid-repo-id`), the seam-unconfigured decline /// reply-shaped `invalid-repo-id`), the seam-unconfigured decline
/// ([`Self::serves_git_row`] alone configures nothing — the /// ([`Self::serves_git_row`] alone configures nothing, per the
/// unset-declines contract), then the store-and-take handoff and the /// unset-declines contract), then the store-and-take handoff and the
/// spawned alkgit dispatch ([`crate::git_map::serve_git_row`], the /// spawned alkgit dispatch ([`crate::git_map::serve_git_row`], the
/// resolved identity riding along). A routed exec answers /// resolved identity riding along). A routed exec answers
/// `channel_success` inline (the request-reply vocabulary: the request /// `channel_success` inline (the request-reply vocabulary: the request
/// routed; the serving outcome reports through exit-status later — /// routed; the serving outcome reports through exit-status later,
/// the POC-3 §A.5 shape), never awaited serving. /// the POC-3 §A.5 shape), never awaited serving.
#[cfg(feature = "git")] #[cfg(feature = "git")]
fn git_row_dispatch( fn git_row_dispatch(
@@ -1569,12 +1569,12 @@ fn git_row_dispatch(
} }
/// The session pipe row's dispatch (`w3-tty-session`): unset-declines /// The session pipe row's dispatch (`w3-tty-session`): unset-declines
/// first (OQ-08 — `exec_wrapper_argv` unset → `wrapper-unconfigured`; /// first (OQ-08: `exec_wrapper_argv` unset → `wrapper-unconfigured`;
/// the seam unset → `seam-unconfigured`; both the spawned serving-path /// the seam unset → `seam-unconfigured`; both the spawned serving-path
/// shape, never a fallback), then the store-and-take handoff /// shape, never a fallback), then the store-and-take handoff
/// (`channel-not-held` → the inline unvanilla decline, the git row's /// (`channel-not-held` → the inline unvanilla decline, the git row's
/// shape), the wire command appended to the wrapper argv (`wrapper ++ /// shape), the wire command appended to the wrapper argv (`wrapper ++
/// [cmd]` — never naively split), the stored pty params consumed if a /// [cmd]`, never naively split), the stored pty params consumed if a
/// pty preceded, and the spawned bridge. A routed exec answers /// pty preceded, and the spawned bridge. A routed exec answers
/// `channel_success` inline (the request routed; the exit chunk /// `channel_success` inline (the request routed; the exit chunk
/// reports the serving outcome later). /// reports the serving outcome later).
@@ -1637,7 +1637,7 @@ fn pipe_row_dispatch(
} }
/// Installs the per-channel bridge surface, clones the session /// Installs the per-channel bridge surface, clones the session
/// handle, and spawns the spawned serving path — the session rows' /// handle, and spawns the serving path, the session rows'
/// spawn step (ADR-006 decision 4: the handler returns fast; the /// spawn step (ADR-006 decision 4: the handler returns fast; the
/// routed request answers `channel_success` inline). /// routed request answers `channel_success` inline).
#[cfg(feature = "session")] #[cfg(feature = "session")]
@@ -1674,24 +1674,24 @@ fn spawn_tty_row(
} }
/// The direct tunnel rows' dispatch (`w3-tunnel-direct`): the /// The direct tunnel rows' dispatch (`w3-tunnel-direct`): the
/// pre-admit decline classes first — **decline first, alloc never** /// pre-admit decline classes first, because **decline first, alloc
/// applies to the cap budget too, so every consumer is checked before /// never** applies to the cap budget too, so every consumer is checked before
/// the admission is taken. Order (the recorded admission mechanics: /// the admission is taken. Order (the recorded admission mechanics:
/// the admission is taken at the hook — the packet-loop's single /// the admission is taken at the hook, the packet-loop's single
/// admit-writer site, no race window against the cap check — and held /// admit-writer site, no race window against the cap check, and held
/// across the fabric open): /// across the fabric open):
/// ///
/// 1. **The unset fabric handle decline** (`seam-unconfigured`, the /// 1. **The unset fabric handle decline** (`seam-unconfigured`, the
/// unset-declines posture — the feature alone configures nothing): /// unset-declines posture, the feature alone configures nothing):
/// `ConnectFailed` ([`crate::dispatch::connect_failed_open_decline`]) /// `ConnectFailed` ([`crate::dispatch::connect_failed_open_decline`])
/// — the bridge cannot be attempted at all. /// as the bridge cannot be attempted at all.
/// 2. **The `no-identity` guard** — unreachable on the wire (russh /// 2. **The `no-identity` guard**: unreachable on the wire (russh
/// authorizes before channels exist) and fail-closed: never /// authorizes before channels exist) and fail-closed: never
/// anonymous serving (the git/tty rows' shape, discharged here for /// anonymous serving (the git/tty rows' shape, discharged here for
/// the tunnel row — the parked `w2-auth-bridge` item; the /// the tunnel row, the parked `w2-auth-bridge` item; the
/// `set_identity` mechanics the rides records live in /// `set_identity` mechanics the rides records live in
/// [`crate::tunnel_map`]). /// [`crate::tunnel_map`]).
/// 3. **The impossible-target decline** (`invalid-target` — an /// 3. **The impossible-target decline** (`invalid-target`, an
/// out-of-range wire port): `ConnectFailed`. /// out-of-range wire port): `ConnectFailed`.
/// ///
/// Then the cap admission ([`Self::admit_channel_open`]; beyond it → /// Then the cap admission ([`Self::admit_channel_open`]; beyond it →
+15 -15
View File
@@ -1,27 +1,27 @@
//! The client-side host-key policy seam (OQ-05 resolution) — the //! The client-side host-key policy seam (OQ-05 resolution): the
//! trust-decision contract the dial surface calls at russh's //! trust-decision contract the dial surface calls at russh's
//! `check_server_key` hook. //! `check_server_key` hook.
//! //!
//! Two cases, one seam — the crate is **policy-blind** //! Two cases, one seam: the crate is **policy-blind**
//! (`docs/architecture/auth.md` §Host-key policy): //! (`docs/architecture/auth.md` §Host-key policy):
//! //!
//! 1. **Known beforehand** (trust anchors exist; family-built runners //! 1. **Known beforehand** (trust anchors exist; family-built runners
//! mint their host key at assembly time): assembly-supplied pins //! mint their host key at assembly time): assembly-supplied pins
//! with **hard decline on mismatch**. The crate never hard-codes //! with **hard decline on mismatch**. The crate never hard-codes
//! this — a pin policy is a [`HostKeyPolicy`] implementation the //! this; a pin policy is a [`HostKeyPolicy`] implementation the
//! assembly writes, backed by its own pin store (the host *pin* is //! assembly writes, backed by its own pin store (the host *pin* is
//! public config, ADR-005 decision 6; never vault material). //! public config, ADR-005 decision 6; never vault material).
//! 2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no //! 2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no
//! anchor until first contact): prompt-and-record, //! anchor until first contact): prompt-and-record,
//! provider-metadata match, learn-and-record, or accept-and- //! provider-metadata match, learn-and-record, or accept-and-
//! continue (the ephemeral-fleet shape) — the OpenSSH //! continue (the ephemeral-fleet shape); the OpenSSH
//! "add to known_hosts?" moment is assembly policy, implemented in //! "add to known_hosts?" moment is assembly policy, implemented in
//! its [`HostKeyPolicy`], never crate behavior. //! its [`HostKeyPolicy`], never crate behavior.
//! //!
//! Crate rules: //! Crate rules:
//! //!
//! - The built-in default policy is **fail-closed**: [`RejectAll`] //! - The built-in default policy is **fail-closed**: [`RejectAll`]
//! rejects every presented key — russh's own reject-all default, //! rejects every presented key, russh's own reject-all default,
//! kept. There is **no accept-all built-in crate option**: a //! kept. There is **no accept-all built-in crate option**: a
//! permissive policy is the assembly's to write when its fleet //! permissive policy is the assembly's to write when its fleet
//! model calls for one. //! model calls for one.
@@ -32,11 +32,11 @@
//! Result<bool, Self::Error>`): the bool *is* the trust decision, //! Result<bool, Self::Error>`): the bool *is* the trust decision,
//! and the handler's `Err` channel stays wire/IO-failure territory. //! and the handler's `Err` channel stays wire/IO-failure territory.
//! A decline is a disposition, not an error (the crate's //! A decline is a disposition, not an error (the crate's
//! error-model contract) — decline reasons are the policy's own //! error-model contract); decline reasons are the policy's own
//! record, not a typed crate surface. //! record, not a typed crate surface.
//! - The [`PresentedHostKey`]'s fingerprint string is the contract- //! - The [`PresentedHostKey`]'s fingerprint string is the contract-
//! invariant format of the crate's single fingerprint path //! invariant format of the crate's single fingerprint path
//! ([`crate::fingerprint`]) — the wave-4 dial wiring computes it //! ([`crate::fingerprint`]); the wave-4 dial wiring computes it
//! there and surfaces it here. //! there and surfaces it here.
//! //!
//! This module is the seam and its default only: the russh //! This module is the seam and its default only: the russh
@@ -45,17 +45,17 @@
use async_trait::async_trait; use async_trait::async_trait;
/// The presented host key — the decision input to /// The presented host key, the decision input to
/// [`HostKeyPolicy::decide`]: the endpoint the dial contacted and the /// [`HostKeyPolicy::decide`]: the endpoint the dial contacted and the
/// key that endpoint presented. /// key that endpoint presented.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PresentedHostKey { pub struct PresentedHostKey {
/// The hostname (or IP) the dial targeted — what the assembly's /// The hostname (or IP) the dial targeted, which the assembly's
/// pin store or prompt labels the key against. /// pin store or prompt labels the key against.
pub host: String, pub host: String,
/// The TCP port the dial targeted. /// The TCP port the dial targeted.
pub port: u16, pub port: u16,
/// The presented key's fingerprint — the single fingerprint /// The presented key's fingerprint, in the single fingerprint
/// path's `SHA256:` format ([`crate::fingerprint`]); comparison /// path's `SHA256:` format ([`crate::fingerprint`]); comparison
/// against assembly pins or recorded entries is exact string /// against assembly pins or recorded entries is exact string
/// equality by contract. /// equality by contract.
@@ -65,13 +65,13 @@ pub struct PresentedHostKey {
/// The trust-decision contract the dial surface calls (OQ-05): the /// The trust-decision contract the dial surface calls (OQ-05): the
/// crate surfaces the presented host key (host, port, fingerprint) to /// crate surfaces the presented host key (host, port, fingerprint) to
/// the assembly-provided policy, which decides accept or reject. The /// the assembly-provided policy, which decides accept or reject. The
/// crate is policy-blind — it never decides trust itself and carries /// crate is policy-blind: it never decides trust itself and carries
/// no trust model (see the module docs for the two-case model). /// no trust model (see the module docs for the two-case model).
/// ///
/// The handle shape is the family's injected-handle pattern /// The handle shape is the family's injected-handle pattern
/// (`Arc<dyn HostKeyPolicy>`, the alkcall `IdentityProvider` /// (`Arc<dyn HostKeyPolicy>`, the alkcall `IdentityProvider`
/// precedent): object-safe via `async_trait`, shared by /// precedent): object-safe via `async_trait`, shared by
/// reference — learn-and-record policies hold their state by interior /// reference; learn-and-record policies hold their state by interior
/// mutability so the same handle serves the dial and any inspection. /// mutability so the same handle serves the dial and any inspection.
/// ///
/// **Fail-closed posture**: when a policy cannot decide (its pin /// **Fail-closed posture**: when a policy cannot decide (its pin
@@ -81,19 +81,19 @@ pub struct PresentedHostKey {
#[async_trait] #[async_trait]
pub trait HostKeyPolicy: Send + Sync + 'static { pub trait HostKeyPolicy: Send + Sync + 'static {
/// Decides the trust verdict for one presented host key: `true` /// Decides the trust verdict for one presented host key: `true`
/// accepts the key for this dial, `false` declines it — the dial /// accepts the key for this dial, `false` declines it; the dial
/// connection aborts on decline (russh's `check_server_key` /// connection aborts on decline (russh's `check_server_key`
/// `false` → `Error::UnknownKey`, a hard, clean decline; a /// `false` → `Error::UnknownKey`, a hard, clean decline; a
/// mismatched pin is a hard decline, never a soft failure). /// mismatched pin is a hard decline, never a soft failure).
async fn decide(&self, presented: &PresentedHostKey) -> bool; async fn decide(&self, presented: &PresentedHostKey) -> bool;
} }
/// The built-in default host-key policy — **fail-closed**: reject /// The built-in default host-key policy, **fail-closed**: reject
/// every presented key. /// every presented key.
/// ///
/// russh's own reject-all default, kept as the crate's built-in /// russh's own reject-all default, kept as the crate's built-in
/// default policy (`docs/architecture/auth.md` §Host-key policy). A /// default policy (`docs/architecture/auth.md` §Host-key policy). A
/// dial that ships this policy can never connect — by design: trust /// dial that ships this policy can never connect, by design: trust
/// is the assembly's decision to make, and the crate's default must /// is the assembly's decision to make, and the crate's default must
/// never let a dial through on the crate's authority. There is no /// never let a dial through on the crate's authority. There is no
/// accept-all counterpart in the crate; the permissive case-2 policy /// accept-all counterpart in the crate; the permissive case-2 policy
+31 -31
View File
@@ -1,4 +1,4 @@
//! The vault-backed key sources — ADR-005's custody model in code, //! The vault-backed key sources: ADR-005's custody model in code,
//! both directions (client dial keys, server host keys). //! both directions (client dial keys, server host keys).
//! //!
//! The adapter takes *key sources*, not static keys (the alkhttp //! The adapter takes *key sources*, not static keys (the alkhttp
@@ -7,14 +7,14 @@
//! implements them over an injected alkvault handle. The crate ships //! implements them over an injected alkvault handle. The crate ships
//! the vault-backed implementations for the ADR-005 shape //! the vault-backed implementations for the ADR-005 shape
//! ([`VaultKeySource`] for one dial key, [`VaultHostKeySource`] for //! ([`VaultKeySource`] for one dial key, [`VaultHostKeySource`] for
//! the host-key set) along with the decrypt-at-use chain they run — //! the host-key set) along with the decrypt-at-use chain they run:
//! the custody machinery is the crate's, the at-rest storage is the //! the custody machinery is the crate's, the at-rest storage is the
//! assembly's. //! assembly's.
//! //!
//! Crate rules, per ADR-005: //! Crate rules, per ADR-005:
//! //!
//! - **`EncryptedData` is opaque** (decision 5): blobs flow through //! - **`EncryptedData` is opaque** (decision 5): blobs flow through
//! this module to `VaultServiceHandle::decrypt` untouched — no //! this module to `VaultServiceHandle::decrypt` untouched: no
//! `key_version` interpretation, no record-internals access //! `key_version` interpretation, no record-internals access
//! anywhere in the crate; version semantics belong to the vault. //! anywhere in the crate; version semantics belong to the vault.
//! - **At-use lifecycle** (decision 2): `vault.decrypt(blob)` → //! - **At-use lifecycle** (decision 2): `vault.decrypt(blob)` →
@@ -24,7 +24,7 @@
//! needed only at use time: locking the vault mid-session keeps //! needed only at use time: locking the vault mid-session keeps
//! live sessions streaming while new loads fail (POC-2 §2). //! live sessions streaming while new loads fail (POC-2 §2).
//! - **Inner format is OpenSSH private-key PEM, unencrypted** //! - **Inner format is OpenSSH private-key PEM, unencrypted**
//! (decision 1) — the source contract assumes it; the decode call //! (decision 1), which the source contract assumes; the decode call
//! is the single conversion. A plaintext that is not that shape is //! is the single conversion. A plaintext that is not that shape is
//! a [`KeySourceError::Decode`] refusal, not a fallback. //! a [`KeySourceError::Decode`] refusal, not a fallback.
//! - **Failure modes are fail-closed with distinct variants** //! - **Failure modes are fail-closed with distinct variants**
@@ -35,7 +35,7 @@
//! - **Plaintext never persists** (decision 7): no disk spill on this //! - **Plaintext never persists** (decision 7): no disk spill on this
//! path, ever. //! path, ever.
//! - Vault lifecycle is the assembly's: this module never unlocks //! - Vault lifecycle is the assembly's: this module never unlocks
//! the vault and never retries — a locked vault stays a refusal. //! the vault and never retries; a locked vault stays a refusal.
use alkvault::{EncryptedData, VaultServiceError, VaultServiceHandle}; use alkvault::{EncryptedData, VaultServiceError, VaultServiceHandle};
use async_trait::async_trait; use async_trait::async_trait;
@@ -44,7 +44,7 @@ use zeroize::Zeroizing;
use crate::error::KeySourceError; use crate::error::KeySourceError;
/// The client dial-key source seam — the handle the wave-4 dial-config /// The client dial-key source seam, the handle the wave-4 dial-config
/// carries (never a static key, never env material). /// carries (never a static key, never env material).
/// ///
/// Produces the decoded [`PrivateKey`] for one dial, decrypt-at-use: /// Produces the decoded [`PrivateKey`] for one dial, decrypt-at-use:
@@ -52,12 +52,12 @@ use crate::error::KeySourceError;
/// key outlives any vault state change (locking mid-session keeps /// key outlives any vault state change (locking mid-session keeps
/// established sessions streaming; POC-2 §2). Implementations must be /// established sessions streaming; POC-2 §2). Implementations must be
/// fail-closed: a load that cannot produce key material returns the /// fail-closed: a load that cannot produce key material returns the
/// matching [`KeySourceError`] variant — every variant is a /// matching [`KeySourceError`] variant; every variant is a
/// no-dial outcome, and there is no fallback key shape in the crate. /// no-dial outcome, and there is no fallback key shape in the crate.
/// ///
/// The handle shape is the family's injected-handle pattern /// The handle shape is the family's injected-handle pattern
/// (`Arc<dyn KeySource>`, the alkcall `IdentityProvider` precedent): /// (`Arc<dyn KeySource>`, the alkcall `IdentityProvider` precedent):
/// object-safe via `async_trait`, shared by reference — the vault /// object-safe via `async_trait`, shared by reference; the vault
/// handle rides the implementation (the assembly clones its /// handle rides the implementation (the assembly clones its
/// [`VaultServiceHandle`] into its source). The crate's ADR-005 /// [`VaultServiceHandle`] into its source). The crate's ADR-005
/// implementation is [`VaultKeySource`]; an assembly may implement the /// implementation is [`VaultKeySource`]; an assembly may implement the
@@ -65,7 +65,7 @@ use crate::error::KeySourceError;
/// contract (OpenSSH-PEM, unencrypted). /// contract (OpenSSH-PEM, unencrypted).
#[async_trait] #[async_trait]
pub trait KeySource: Send + Sync + 'static { pub trait KeySource: Send + Sync + 'static {
/// Loads the dial key — decryption plus the one-time /// Loads the dial key: decryption plus the one-time
/// `decode_secret_key` (ADR-005 decision 2). /// `decode_secret_key` (ADR-005 decision 2).
/// ///
/// This is the only surface where the dial direction touches /// This is the only surface where the dial direction touches
@@ -75,38 +75,38 @@ pub trait KeySource: Send + Sync + 'static {
async fn load(&self) -> Result<PrivateKey, KeySourceError>; async fn load(&self) -> Result<PrivateKey, KeySourceError>;
} }
/// The server host-key source seam — the handle the wave-2 /// The server host-key source seam, the handle the wave-2
/// serve-config carries (ADR-005 decision 6). /// serve-config carries (ADR-005 decision 6).
/// ///
/// Produces the decoded host-key set for server construction — /// Produces the decoded host-key set for server construction,
/// decrypt-**at-construction**: the wave-2 `Config` builder calls /// decrypt-**at-construction**: the wave-2 `Config` builder calls
/// [`HostKeySource::load`] when it assembles the server, and a failure /// [`HostKeySource::load`] when it assembles the server, and a failure
/// means no server is constructed (POC-2 §5; locked vault at /// means no server is constructed (POC-2 §5; locked vault at
/// bootstrap → [`KeySourceError::Locked`] → fail-closed, never a /// bootstrap → [`KeySourceError::Locked`] → fail-closed, never a
/// half-configured server). The crate is policy-blind on host *pin* /// half-configured server). The crate is policy-blind on host *pin*
/// trust — that is the [`crate::HostKeyPolicy`] seam's business — this /// trust (that is the [`crate::HostKeyPolicy`] seam's business); this
/// seam is only the *held* material (the client's host pin is public /// seam is only the *held* material (the client's host pin is public
/// config, never vault material). /// config, never vault material).
/// ///
/// The set, not a single key: `Config::keys` accepts multiple /// The set, not a single key: `Config::keys` accepts multiple
/// host keys and fleets rotate them. Implementations must be /// host keys and fleets rotate them. Implementations must be
/// fail-closed in the same shape as [`KeySource`] — see the trait's /// fail-closed in the same shape as [`KeySource`]; see the trait's
/// docs. The crate's ADR-005 implementation is [`VaultHostKeySource`]. /// docs. The crate's ADR-005 implementation is [`VaultHostKeySource`].
#[async_trait] #[async_trait]
pub trait HostKeySource: Send + Sync + 'static { pub trait HostKeySource: Send + Sync + 'static {
/// Loads the host-key set — each blob through the same /// Loads the host-key set, each blob through the same
/// decrypt-at-use chain as [`KeySource::load`]. Order follows the /// decrypt-at-use chain as [`KeySource::load`]. Order follows the
/// source's registration order. /// source's registration order.
async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError>; async fn load(&self) -> Result<Vec<PrivateKey>, KeySourceError>;
} }
/// The ADR-005 dial-key source — decrypt-at-use over an injected /// The ADR-005 dial-key source, decrypt-at-use over an injected
/// alkvault handle, backed by one at-rest `EncryptedData` record. /// alkvault handle, backed by one at-rest `EncryptedData` record.
/// ///
/// The record is injected at construction, from the assembly's /// The record is injected at construction, from the assembly's
/// registry (`None` = the assembly holds no record at this key's /// registry (`None` = the assembly holds no record at this key's
/// location → [`KeySourceError::Absent`], POC-2 §3). The vault handle /// location → [`KeySourceError::Absent`], POC-2 §3). The vault handle
/// is a clone of the assembly's — this source never unlocks, locks, or /// is a clone of the assembly's; this source never unlocks, locks, or
/// retries the vault's lifecycle state; every [`VaultKeySource::load`] /// retries the vault's lifecycle state; every [`VaultKeySource::load`]
/// is decrypt-at-usage and a locked vault is a clean /// is decrypt-at-usage and a locked vault is a clean
/// [`KeySourceError::Locked`] refusal. /// [`KeySourceError::Locked`] refusal.
@@ -134,11 +134,11 @@ impl KeySource for VaultKeySource {
} }
} }
/// The ADR-005 host-key source — decrypt-at-construction over an /// The ADR-005 host-key source, decrypt-at-construction over an
/// injected alkvault handle, backed by the injected at-rest record set. /// injected alkvault handle, backed by the injected at-rest record set.
/// ///
/// Each element of the set flows through the same decrypt-and-decode /// Each element of the set flows through the same decrypt-and-decode
/// chain as [`VaultKeySource`] (one model, both directions — /// chain as [`VaultKeySource`] (one model, both directions;
/// POC-2 §5). An empty set is [`KeySourceError::Absent`] (a host key /// POC-2 §5). An empty set is [`KeySourceError::Absent`] (a host key
/// registration gap), which the wave-2 config builder treats exactly /// registration gap), which the wave-2 config builder treats exactly
/// like every other load failure: no server is constructed. /// like every other load failure: no server is constructed.
@@ -175,7 +175,7 @@ impl HostKeySource for VaultHostKeySource {
/// The plaintext leaves the vault as /// The plaintext leaves the vault as
/// [`KeySourceError`]-mapped failure or as alkvault 0.2's /// [`KeySourceError`]-mapped failure or as alkvault 0.2's
/// `Zeroizing<String>` return (the pre-wrap unfenced window ADR-005 /// `Zeroizing<String>` return (the pre-wrap unfenced window ADR-005
/// decision 3 recorded is closed upstream — the vault fences the /// decision 3 recorded is closed upstream: the vault fences the
/// plaintext from its return onward, zeroized on drop, including the /// plaintext from its return onward, zeroized on drop, including the
/// vault's own UTF-8 failure path; OQ-07's upstream ask is resolved, /// vault's own UTF-8 failure path; OQ-07's upstream ask is resolved,
/// amending the ADR's decision 3 note). The decode is the single /// amending the ADR's decision 3 note). The decode is the single
@@ -189,28 +189,28 @@ fn decrypt_and_decode(
decode_key_arm(&plaintext) decode_key_arm(&plaintext)
} }
/// The at-use chain's decode arm — the one-time /// The at-use chain's decode arm: the one-time
/// [`decode_secret_key`] conversion of [`decrypt_and_decode`] /// [`decode_secret_key`] conversion of [`decrypt_and_decode`]
/// (ADR-005 decision 2's single conversion; this function is byte-for- /// (ADR-005 decision 2's single conversion; this function is byte-for-
/// byte that arm, extracted so the arm is the *only* shape the fuzz /// byte that arm, extracted so the arm is the *only* shape the fuzz
/// target drives). `#[doc(hidden)]` fuzz-visible surface /// target drives). `#[doc(hidden)]` fuzz-visible surface
/// (`w5-fuzz-harness`; the recorded exposure decision — the ingress is /// (`w5-fuzz-harness`; the recorded exposure decision: the ingress is
/// production-private, so the exposure is the production arm itself, /// production-private, so the exposure is the production arm itself,
/// not a fork). Not documented API: the vault-facing surface is /// not a fork). Not documented API: the vault-facing surface is
/// exactly [`VaultKeySource`] / [`VaultHostKeySource`]. /// exactly [`VaultKeySource`] / [`VaultHostKeySource`].
/// ///
/// The `Zeroizing` posture rides the signature: the plaintext /// The `Zeroizing` posture rides the signature: the plaintext
/// arrives already zeroized (alkvault 0.2's return type — decision /// arrives already zeroized (alkvault 0.2's return type, decision
/// 3's ownership moved upstream), the decode reads /// 3's ownership moved upstream), the decode reads
/// it once, and nothing is retained — `Ok` carries the decoded /// it once, and nothing is retained: `Ok` carries the decoded
/// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`] /// [`PrivateKey`], `Err` carries only the typed [`KeySourceError`]
/// class (never the plaintext). /// class (never the plaintext).
/// ///
/// The typed `Decode` payload is crate-owned fixed text, deliberately /// The typed `Decode` payload is crate-owned fixed text, deliberately
/// *not* the underlying machinery's message (the `w5-fuzz-harness` /// *not* the underlying machinery's message (the `w5-fuzz-harness`
/// finding, 2026-10-30): russh's/`ssh-key`'s PPK parse path /// finding, 2026-10-30): russh's/`ssh-key`'s PPK parse path
/// (`PpkParseError::Header`) interpolates raw header bytes — the /// (`PpkParseError::Header`) interpolates raw header bytes, the
/// plaintext's own material — into its Display, so a machinery message /// plaintext's own material, into its Display, so a machinery message
/// riding the typed payload would echo decrypted-record material /// riding the typed payload would echo decrypted-record material
/// exactly the no-persist posture (decision 7) forbids. The upstream /// exactly the no-persist posture (decision 7) forbids. The upstream
/// echo ask is recorded with the task; if the machinery gains an /// echo ask is recorded with the task; if the machinery gains an
@@ -219,7 +219,7 @@ fn decrypt_and_decode(
pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySourceError> { pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySourceError> {
decode_secret_key(plaintext, None).map_err(|_| { decode_secret_key(plaintext, None).map_err(|_| {
KeySourceError::Decode( KeySourceError::Decode(
"the plaintext is not OpenSSH private-key PEM material — the decode machinery \ "the plaintext is not OpenSSH private-key PEM material; the decode machinery \
refused it (the machinery's message is not carried: it can echo plaintext \ refused it (the machinery's message is not carried: it can echo plaintext \
material; the upstream echo ask is recorded)" material; the upstream echo ask is recorded)"
.to_owned(), .to_owned(),
@@ -228,18 +228,18 @@ pub fn decode_key_arm(plaintext: &Zeroizing<String>) -> Result<PrivateKey, KeySo
} }
/// The vault's error taxonomy reshaped into ADR-005 decision 4's /// The vault's error taxonomy reshaped into ADR-005 decision 4's
/// five-variant matrix — never re-exported, never collapsed into one. /// five-variant matrix, never re-exported, never collapsed into one.
/// ///
/// `VaultLocked` → [`KeySourceError::Locked`]; `InvalidPath` (the /// `VaultLocked` → [`KeySourceError::Locked`]; `InvalidPath` (the
/// vault's refuse-before-crypto refusal of an unresolvable derivation /// vault's refuse-before-crypto refusal of an unresolvable derivation
/// path — reserved/legacy v1, v0) → /// path, such as reserved/legacy v1 or v0) →
/// [`KeySourceError::UnknownKeyVersion`]; `Encryption` (the GCM-open /// [`KeySourceError::UnknownKeyVersion`]; `Encryption` (the GCM-open
/// class: wrong mnemonic, tampered ciphertext) is honest refuted /// class: wrong mnemonic, tampered ciphertext) is honest refuted
/// decryption → [`KeySourceError::Decryption`]. The remaining vault /// decryption → [`KeySourceError::Decryption`]. The remaining vault
/// variants cannot arise on the decrypt path (decryption never /// variants cannot arise on the decrypt path (decryption never
/// touches mnemonic parsing, key derivation outside version-path /// touches mnemonic parsing, key derivation outside version-path
/// resolution, unlock state transitions, or key-type registration; /// resolution, unlock state transitions, or key-type registration;
/// POC-2 §3) — if one were ever observed it would be a vault-side /// POC-2 §3); if one were ever observed it would be a vault-side
/// refuse-to-decrypt condition, so it lands in the `Decryption` class /// refuse-to-decrypt condition, so it lands in the `Decryption` class
/// with the vault's message intact rather than silently succeeding. /// with the vault's message intact rather than silently succeeding.
fn map_vault_error(error: VaultServiceError) -> KeySourceError { fn map_vault_error(error: VaultServiceError) -> KeySourceError {
@@ -273,14 +273,14 @@ mod tests {
use crate::error::KeySourceError; use crate::error::KeySourceError;
use crate::fingerprint::fingerprint; use crate::fingerprint::fingerprint;
/// The committed test-key fixtures (`tests/vectors/`) — the /// The committed test-key fixtures (`tests/vectors/`), the
/// wave-1 fixture set, read by this `#[cfg(test)]` module through /// wave-1 fixture set, read by this `#[cfg(test)]` module through
/// `include_str!` (the `tests/common/mod.rs` double module is the /// `include_str!` (the `tests/common/mod.rs` double module is the
/// tests/-side owner; src-unit tests never own fixture copies). /// tests/-side owner; src-unit tests never own fixture copies).
/// Throwaway `ssh-keygen`-minted keys, never registered anywhere /// Throwaway `ssh-keygen`-minted keys, never registered anywhere
/// real (`ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE` /// real (`ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE`
/// the host-key pair). Their `ssh-keygen -lf` fingerprints are /// the host-key pair). Their `ssh-keygen -lf` fingerprints are
/// recorded in `tests/vectors/fingerprints.txt` — the fixture /// recorded in `tests/vectors/fingerprints.txt`; the fixture
/// parity row below asserts this module sees the same values. /// parity row below asserts this module sees the same values.
const ALPHA_PEM: &str = include_str!("../tests/vectors/alpha.key"); const ALPHA_PEM: &str = include_str!("../tests/vectors/alpha.key");
const BRAVO_PEM: &str = include_str!("../tests/vectors/bravo.key"); const BRAVO_PEM: &str = include_str!("../tests/vectors/bravo.key");
+33 -33
View File
@@ -1,61 +1,61 @@
//! alkssh — the alk family's SSH adapter (the alkhttp-style door for the //! alkssh, the alk family's SSH adapter (the alkhttp-style door for the
//! SSH protocol): a thin wrapper around russh terminating the vanilla-ssh //! SSH protocol): a thin wrapper around russh terminating the vanilla-ssh
//! wire and bridging to/from alkcall channels (the `alk/ssh` ALPN). //! wire and bridging to/from alkcall channels (the `alk/ssh` ALPN).
//! //!
//! No SSH framing is authored in-family (family ADR-003): russh owns the //! No SSH framing is authored in-family (family ADR-003): russh owns the
//! wire, and the crate's substance is the channel-type mappings — //! wire, and the crate's substance is the channel-type mappings:
//! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec → //! session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
//! alkgit, sftp → alkfs later (a declining placeholder until then), each //! alkgit, sftp → alkfs later (a declining placeholder until then), each
//! behind its cargo feature. Git over SSH is exec-only (ADR-006): a //! behind its cargo feature. Git over SSH is exec-only (ADR-006): a
//! subsystem request can never carry the repository, so it declines. A //! subsystem request can never carry the repository, so it declines. A
//! client opening an unfeatured or unimplemented channel type gets a //! client opening an unfeatured or unimplemented channel type gets a
//! clean decline — never a half-capability, never a silent fallback; the //! clean decline, never a half-capability, never a silent fallback; the
//! default build (no features) is structurally incapable of serving //! default build (no features) is structurally incapable of serving
//! anything but declines. //! anything but declines.
//! //!
//! Identity is the family's, not SSH's: a verified key's fingerprint → //! Identity is the family's, not SSH's: a verified key's fingerprint →
//! alkcall `IdentityProvider`. Keys the crate holds (client dial keys, //! alkcall `IdentityProvider`. Keys the crate holds (client dial keys,
//! host keys) come from alkvault-backed sources — vault-encrypted at //! host keys) come from alkvault-backed sources, vault-encrypted at
//! rest, decrypted at use (ADR-005), never env vars. //! rest, decrypted at use (ADR-005), never env vars.
//! //!
//! Architecture: `docs/architecture/` (phase 1 — ADRs 001–006 accepted; //! Architecture: `docs/architecture/` (phase 1: ADRs 001–006 accepted;
//! the foundations, the server stack, the server-direction mappings, and //! the foundations, the server stack, the server-direction mappings, and
//! the client direction — dial, op rows, the `-R` acceptor leg, and the //! the client direction (dial, op rows, the `-R` acceptor leg, and the
//! lifecycle primitives — are landed through wave 4; the validation //! lifecycle primitives) are landed through wave 4; the validation
//! suite and the release are wave 5). See also `docs/research/`, //! suite and the release are wave 5). See also `docs/research/`,
//! `docs/plans/implementation.md`, and family ADR-003. //! `docs/plans/implementation.md`, and family ADR-003.
//! //!
//! # The feature graph (ADR-003 — flat, independent, fail-closed) //! # The feature graph (ADR-003: flat, independent, fail-closed)
//! //!
//! Every channel-type mapping is a cargo feature; a session opening an //! Every channel-type mapping is a cargo feature; a session opening an
//! unfeatured or unimplemented channel type is declined fail-closed — //! unfeatured or unimplemented channel type is declined fail-closed,
//! never a half-capability, never a silent fallback. **A default build //! never a half-capability, never a silent fallback. **A default build
//! (no features) has no capabilities**: it can serve nothing but clean //! (no features) has no capabilities**: it can serve nothing but clean
//! declines. No feature implies another; the graph is flat. //! declines. No feature implies another; the graph is flat.
//! //!
//! - (default, no features) — serves declines only: every open, exec, //! - (default, no features) serves declines only: every open, exec,
//! listen request answers a clean, channel-scoped refusal. //! listen request answers a clean, channel-scoped refusal.
//! - `session` — pty/shell/exec rows → the `RemoteSession` chunk //! - `session` maps the pty/shell/exec rows → the `RemoteSession` chunk
//! plane (alktty). //! plane (alktty).
//! - `git` — the exec-only git rows (`git-{upload,receive}-pack`) → //! - `git` carries the exec-only git rows (`git-{upload,receive}-pack`) →
//! alkgit; a git *subsystem* request always declines (the repository //! alkgit; a git *subsystem* request always declines (the repository
//! can never ride a subsystem channel). //! can never ride a subsystem channel).
//! - `tunnel` — `direct-tcpip` / `direct-streamlocal` and the `-R` //! - `tunnel` covers `direct-tcpip` / `direct-streamlocal` and the `-R`
//! legs (listen, `forwarded-tcpip` / `forwarded-streamlocal`) → //! legs (listen, `forwarded-tcpip` / `forwarded-streamlocal`) →
//! alktunnels. //! alktunnels.
//! - `sftp` — a reserved, currently-empty placeholder (OQ-13): the //! - `sftp`, a reserved, currently-empty placeholder (OQ-13): the
//! `sftp` subsystem request still declines in every build until the //! `sftp` subsystem request still declines in every build until the
//! alkfs mapping lands; the feature exists so the decline stays //! alkfs mapping lands; the feature exists so the decline stays
//! contract territory (tested in every matrix build). //! contract territory (tested in every matrix build).
//! //!
//! Advertisement is honest: an unfeatured channel type is never //! Advertisement is honest: an unfeatured channel type is never
//! advertised, only declined (`docs/architecture/mapping.md` §Decline //! advertised, only declined (`docs/architecture/mapping.md` §Decline
//! matrix — the fail-closed invariant is contract territory on both //! matrix; the fail-closed invariant is contract territory on both
//! sides of every feature). //! sides of every feature).
//! //!
//! # Known limits (recorded dispositions riding the published docs) //! # Known limits (recorded dispositions riding the published docs)
//! //!
//! The records below are deliberate dispositions — documented limits //! The records below are deliberate dispositions: documented limits
//! with their reasons, mirrored in the module docs and the //! with their reasons, mirrored in the module docs and the
//! architecture spec, not defects awaiting a fix: //! architecture spec, not defects awaiting a fix:
//! //!
@@ -64,29 +64,29 @@
//! per-channel window, the client's own flow control) and liveness //! per-channel window, the client's own flow control) and liveness
//! is covered by the always-set //! is covered by the always-set
//! [`Limits::inactivity_timeout`]; a deadline here would be a //! [`Limits::inactivity_timeout`]; a deadline here would be a
//! behavior change (a slow client may stall its stream — vanilla //! behavior change (a slow client may stall its stream, vanilla
//! sshd's posture included). (`tty_map.rs` module docs.) //! sshd's posture included). (`tty_map.rs` module docs.)
//! - **The PTY path's exit-signal fidelity is limited by upstream** //! - **The PTY path's exit-signal fidelity is limited by upstream**
//! (exit-currency rule 5): signal-terminated PTY sessions surface as //! (exit-currency rule 5): signal-terminated PTY sessions surface as
//! `exit-status(1)` because the backend's reap surface loses the //! `exit-status(1)` because the backend's reap surface loses the
//! signal number; the pipe path encodes negatives faithfully. An //! signal number; the pipe path encodes negatives faithfully. An
//! alktty/portable-pty ask — the contract adapts automatically //! alktty/portable-pty ask; the contract adapts automatically
//! when upstream conforms. //! when upstream conforms.
//! - **The port-0 mint is the only bound-port echo the wire carries** //! - **The port-0 mint is the only bound-port echo the wire carries**
//! (russh/sshd parity): a `tcpip_forward` success reply carries the //! (russh/sshd parity): a `tcpip_forward` success reply carries the
//! allocated port only for a `port = 0` request; an explicit-port //! allocated port only for a `port = 0` request; an explicit-port
//! request's bound port is by definition the requested one — the //! request's bound port is by definition the requested one; the
//! crate surfaces it as such, never a fabricated number. //! crate surfaces it as such, never a fabricated number.
//! - **No reconnect/restore policy.** The client direction ships //! - **No reconnect/restore policy.** The client direction ships
//! lifecycle *primitives* (bound ports, cancellable handles, clean //! lifecycle *primitives* (bound ports, cancellable handles, clean
//! [`SshConnection::close`]); reconnect-and-restore (rebinding //! [`SshConnection::close`]); reconnect-and-restore (rebinding
//! persisted tunnel ports) is assembly composition over them — the //! persisted tunnel ports) is assembly composition over them; the
//! crate never owns the policy. Drop is never the clean path at //! crate never owns the policy. Drop is never the clean path at
//! this russh pin: only an explicit `close` sends the clean //! this russh pin: only an explicit `close` sends the clean
//! disconnect. //! disconnect.
//! - **No accept-all host-key policy exists in the crate.** The //! - **No accept-all host-key policy exists in the crate.** The
//! built-in default is [`RejectAll`] (a dial with it can never //! built-in default is [`RejectAll`] (a dial with it can never
//! connect — by design); the crate is policy-blind and any //! connect, by design); the crate is policy-blind and any
//! permissive trust policy is the assembly's to write. A //! permissive trust policy is the assembly's to write. A
//! fail-closed policy posture: when a policy cannot decide, it must //! fail-closed policy posture: when a policy cannot decide, it must
//! decline. //! decline.
@@ -96,10 +96,10 @@
//! The crate is a door, not a deployment: an assembly embeds it, //! The crate is a door, not a deployment: an assembly embeds it,
//! enables the features it wants, and injects the seams (identity, //! enables the features it wants, and injects the seams (identity,
//! key sources, the per-feature family handles). The examples are //! key sources, the per-feature family handles). The examples are
//! compile-checked (`no_run`) doctests — honest sketches, not //! compile-checked (`no_run`) doctests, honest sketches, not
//! executable code. //! executable code.
//! //!
//! A server mount (the default build — a decline-only door; the //! A server mount (the default build, a decline-only door; the
//! assembly's real sources are the vault-backed [`VaultKeySource`] / //! assembly's real sources are the vault-backed [`VaultKeySource`] /
//! [`VaultHostKeySource`] shapes): //! [`VaultHostKeySource`] shapes):
//! //!
@@ -151,7 +151,7 @@
//! ``` //! ```
//! //!
//! A dial (the client direction; the built-in [`RejectAll`] //! A dial (the client direction; the built-in [`RejectAll`]
//! host-key policy connects to nothing — by design, the assembly's //! host-key policy connects to nothing, by design, and the assembly's
//! policy replaces it): //! policy replaces it):
//! //!
//! ```rust,no_run //! ```rust,no_run
@@ -182,7 +182,7 @@
//! # { //! # {
//! # let _ = conn.exec("uptime"); // the op rows (feature-gated) //! # let _ = conn.exec("uptime"); // the op rows (feature-gated)
//! # } //! # }
//! conn.close().await; // clean disconnect — drop is never the clean path //! conn.close().await; // clean disconnect; drop is never the clean path
//! # Ok::<(), alkssh::DialError>(()) //! # Ok::<(), alkssh::DialError>(())
//! # })?; //! # })?;
//! # Ok(()) //! # Ok(())
@@ -225,7 +225,7 @@ pub use client::dial;
pub use client::russh_client_config; pub use client::russh_client_config;
/// The `-R` leg's acceptor registry + the reverse-listen rows' /// The `-R` leg's acceptor registry + the reverse-listen rows'
/// payload/acceptor types + the listener handles (`w4-reverse-listen` /// payload/acceptor types + the listener handles (`w4-reverse-listen`
/// / `w4-lifecycle` — under `tunnel`; the recorded decisions live in /// / `w4-lifecycle`, under `tunnel`; the recorded decisions live in
/// the `client_accept` module docs). /// the `client_accept` module docs).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
pub use client_accept::{ pub use client_accept::{
@@ -235,14 +235,14 @@ pub use client_accept::{
#[doc(hidden)] #[doc(hidden)]
pub use client_handler::ClientHandler; pub use client_handler::ClientHandler;
/// The client op rows' surfaces (`w4-op-rows`): /// The client op rows' surfaces (`w4-op-rows`):
/// [`RemoteSession`] — the exec/pty rows' /// [`RemoteSession`]; the exec/pty rows'
/// family chunk plane (under `session`), /// family chunk plane (under `session`),
/// [`TunnelChannel`] — the direct /// [`TunnelChannel`]; the direct
/// rows' byte duplex (under `tunnel`), /// rows' byte duplex (under `tunnel`),
/// [`GitPackSession`] — the upstream git /// [`GitPackSession`]; the upstream git
/// row's pack duplex + consumed exit (under `git`). The op methods /// row's pack duplex + consumed exit (under `git`). The op methods
/// live on [`SshConnection`] (the op-row surface). Not a re-export /// live on [`SshConnection`] (the op-row surface). Not a re-export
/// here: the `sftp` row is [`SshConnection::sftp`] itself — the /// here: the `sftp` row is [`SshConnection::sftp`] itself, the
/// every-build OQ-13 decline placeholder. /// every-build OQ-13 decline placeholder.
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub use client_ops::GitPackSession; pub use client_ops::GitPackSession;
@@ -258,7 +258,7 @@ pub use config::SessionSeam;
pub use config::TunnelListenSeam; pub use config::TunnelListenSeam;
pub use config::{DialConfig, Limits, ServeConfig}; pub use config::{DialConfig, Limits, ServeConfig};
/// Fuzz-visible surface (`#[doc(hidden)]`, `w5-fuzz-harness`): the exec /// Fuzz-visible surface (`#[doc(hidden)]`, `w5-fuzz-harness`): the exec
/// grammar's parse path (`dispatch.rs`) — the `fuzz/` sidecar's /// grammar's parse path (`dispatch.rs`); the `fuzz/` sidecar's
/// `exec_parse` target drives the production parse, never a fork. Not /// `exec_parse` target drives the production parse, never a fork. Not
/// documented API: the grammar is the dispatch seam's, not a public-API /// documented API: the grammar is the dispatch seam's, not a public-API
/// promise. /// promise.
@@ -267,7 +267,7 @@ pub use dispatch::{ExecDecline, ExecRoute, GitServiceRoute, parse_exec};
pub use error::{AdapterError, DialError, KeySourceError, OpError, OpenDeclineReason}; pub use error::{AdapterError, DialError, KeySourceError, OpError, OpenDeclineReason};
pub use fingerprint::fingerprint; pub use fingerprint::fingerprint;
/// Contract-test surface (`#[doc(hidden)]`): the per-connection auth /// Contract-test surface (`#[doc(hidden)]`): the per-connection auth
/// bridge handler — constructible so the tests drive its hooks directly /// bridge handler, constructible so the tests drive its hooks directly
/// against the wave-1 doubles (see `handler.rs` §Test surface). Not /// against the wave-1 doubles (see `handler.rs` §Test surface). Not
/// documented API: the wire-facing surface is exactly `SshAdapter` + /// documented API: the wire-facing surface is exactly `SshAdapter` +
/// the config/error/host/key-source types. /// the config/error/host/key-source types.
@@ -281,7 +281,7 @@ pub use host_key::{HostKeyPolicy, PresentedHostKey, RejectAll};
pub use key_source::decode_key_arm; pub use key_source::decode_key_arm;
pub use key_source::{HostKeySource, KeySource, VaultHostKeySource, VaultKeySource}; pub use key_source::{HostKeySource, KeySource, VaultHostKeySource, VaultKeySource};
/// Contract-test surface (`#[doc(hidden)]`): the spawn-and-return /// Contract-test surface (`#[doc(hidden)]`): the spawn-and-return
/// plumbing (`ADR-006 decision 4`, see `serve.rs`) — the mechanics the /// plumbing (`ADR-006 decision 4`, see `serve.rs`), the mechanics the
/// mechanics-proof rows in `tests/spawn_return.rs` compose over the /// mechanics-proof rows in `tests/spawn_return.rs` compose over the
/// real wire to pin the constraint as tests. Not documented API. /// real wire to pin the constraint as tests. Not documented API.
#[doc(hidden)] #[doc(hidden)]
+57 -57
View File
@@ -1,5 +1,5 @@
//! The decline-path + serving-path event vocabulary //! The decline-path + serving-path event vocabulary
//! (`w3-decline-tracing` — the wave-2 gate's booked finding): the //! (`w3-decline-tracing`, the wave-2 gate's booked finding): the
//! observability surface every decline site in `src/` emits through, //! observability surface every decline site in `src/` emits through,
//! and the serving-path lifecycle skeleton ([`serving_path_started`] / //! and the serving-path lifecycle skeleton ([`serving_path_started`] /
//! [`serving_path_ended`] / [`serving_path_failed`]) the wave-3 //! [`serving_path_ended`] / [`serving_path_failed`]) the wave-3
@@ -7,7 +7,7 @@
//! //!
//! ## The family posture read (2026-10-28) //! ## The family posture read (2026-10-28)
//! //!
//! Read before choosing this shape — no alkssh-private dialect: //! Read before choosing this shape; no alkssh-private dialect:
//! //!
//! - `alktty` (`adapter.rs`, `session.rs`, `local/pipe.rs`, //! - `alktty` (`adapter.rs`, `session.rs`, `local/pipe.rs`,
//! `local/pty.rs`): free `tracing::{debug, warn}` imports, //! `local/pty.rs`): free `tracing::{debug, warn}` imports,
@@ -18,7 +18,7 @@
//! (`listen = …`, `error = %e`, `reason = …`), levels `debug` for //! (`listen = …`, `error = %e`, `reason = …`), levels `debug` for
//! structural observations, `warn` for recoverable failures, //! structural observations, `warn` for recoverable failures,
//! `error` for abnormal pump terminations. //! `error` for abnormal pump terminations.
//! - `alkcall` (`protocol/*`, `channels/*`): the widest use — the same //! - `alkcall` (`protocol/*`, `channels/*`): the widest use, with the same
//! fields convention (`channel_id`, `request_id`, `error = %e`), //! fields convention (`channel_id`, `request_id`, `error = %e`),
//! component-prefixed messages (`"channel client: …"`, //! component-prefixed messages (`"channel client: …"`,
//! `"open wrapper: …"`), `debug`/`warn` only. //! `"open wrapper: …"`), `debug`/`warn` only.
@@ -27,7 +27,7 @@
//! - `alkgit`: no tracing at all at this writing. //! - `alkgit`: no tracing at all at this writing.
//! //!
//! Never observed in the family: `info!` / `trace!` events, subscriber //! Never observed in the family: `info!` / `trace!` events, subscriber
//! setup in library code, macro or span wrappers — so none here //! setup in library code, macro or span wrappers, so none here
//! either. Adopted verbatim: free `tracing` fn calls, message-last //! either. Adopted verbatim: free `tracing` fn calls, message-last
//! lowercase with the `"decline: …"` / `"serving path …"` phrasing, //! lowercase with the `"decline: …"` / `"serving path …"` phrasing,
//! flat per-event fields (`feature_build`, `row`, `channel_id`, //! flat per-event fields (`feature_build`, `row`, `channel_id`,
@@ -37,91 +37,91 @@
//! //!
//! Every event carries exactly: //! Every event carries exactly:
//! //!
//! - `feature_build = FEATURE_BUILD` — the crate's compile-time //! - `feature_build = FEATURE_BUILD`: the crate's compile-time
//! feature set as a stable string (`"none"`, `"git"`, //! feature set as a stable string (`"none"`, `"git"`,
//! `"session+git"`, …; features joined in the order `session, git, //! `"session+git"`, …; features joined in the order `session, git,
//! tunnel, sftp`), so a captured event is self-describing per //! tunnel, sftp`), so a captured event is self-describing per
//! build. The `sftp` feature gates no route, but ships in the //! build. The `sftp` feature gates no route, but ships in the
//! build string: it is part of what the assembly deployed. //! build string: it is part of what the assembly deployed.
//! - `row` — the greppable wire row the event is about: the channel //! - `row`: the greppable wire row the event is about, the channel
//! type for open-level (e.g. `"session"`, `"direct-tcpip"`, `"x11"`), //! type for open-level (e.g. `"session"`, `"direct-tcpip"`, `"x11"`),
//! the request name for request-level (e.g. `"pty_request"`, //! the request name for request-level (e.g. `"pty_request"`,
//! `"tcpip_forward"`), the handler hook name for auth (e.g. //! `"tcpip_forward"`), the handler hook name for auth (e.g.
//! `"auth_publickey"`), the session-row hook names for the //! `"auth_publickey"`), the session-row hook names for the
//! serving-path lifecycle. //! serving-path lifecycle.
//! - `channel_id` — the wire channel id (u32), on every event of a //! - `channel_id`: the wire channel id (u32), on every event of a
//! channel; the auth class has none (the auth hooks carry no //! channel; the auth class has none (the auth hooks carry no
//! channel at this surface). //! channel at this surface).
//! //!
//! Decline events additionally carry exactly: //! Decline events additionally carry exactly:
//! //!
//! - `decline_class` — `"open"` | `"request"` | `"serving"` | //! - `decline_class`: `"open"` | `"request"` | `"serving"` |
//! `"arrival"` | `"auth"`: //! `"arrival"` | `"auth"`:
//! the two wire decline classes of `mapping.md` §Wire decline //! the two wire decline classes of `mapping.md` §Wire decline
//! reasons, plus the serving-path class (this wave the seam stub's //! reasons, plus the serving-path class (this wave the seam stub's
//! decline *is* the serving path; the wave-3 mappings emit the same //! decline *is* the serving path; the wave-3 mappings emit the same
//! class from their real spawned paths) and the auth class (the //! class from their real spawned paths) and the auth class (the
//! disposition declines of ADR-004). //! disposition declines of ADR-004).
//! - `reason` — the decline cause, mirroring the architecture's own //! - `reason`: the decline cause, mirroring the architecture's own
//! classifications: `"unfeatured"` (feature-gate absence), //! classifications: `"unfeatured"` (feature-gate absence),
//! `"never-feature"` (ADR-003 §5 rows), `"cap-exceeded"`, //! `"never-feature"` (ADR-003 §5 rows), `"cap-exceeded"`,
//! `"connect-failed"` (`w3-tunnel-direct`: the tunnel rows' //! `"connect-failed"` (`w3-tunnel-direct`: the tunnel rows'
//! cannot-bridge class — the failed fabric open, the wave-2 seam //! cannot-bridge class: the failed fabric open, the wave-2 seam
//! stub's reason surviving as the real decline; also the //! stub's reason surviving as the real decline; also the
//! `forwarded-tcpip` stub's standing decline class), //! `forwarded-tcpip` stub's standing decline class),
//! `"seam-unconfigured"` (the config-driven row decline — the git //! `"seam-unconfigured"` (the config-driven row decline: the git
//! row and, now, the session rows (feature on, seam unset; //! row and, now, the session rows (feature on, seam unset;
//! `w3-git-mapping` / `w3-tty-session`) and the tunnel rows' absent //! `w3-git-mapping` / `w3-tty-session`) and the tunnel rows' absent
//! fabric handle even under the feature (`w3-tunnel-direct`; the //! fabric handle even under the feature (`w3-tunnel-direct`; the
//! unset-declines posture)), //! unset-declines posture)),
//! `"shell-unconfigured"` / `"wrapper-unconfigured"` (`w3-tty- //! `"shell-unconfigured"` / `"wrapper-unconfigured"` (`w3-tty-
//! session`: the OQ-08 unset-field rows — feature on, the //! session`: the OQ-08 unset-field rows (feature on, the
//! serve-config field unset, never a fallback), //! serve-config field unset, never a fallback),
//! `"negotiation-failed"` (`w3-tty-session`: the alktty //! `"negotiation-failed"` (`w3-tty-session`: the alktty
//! negotiation error frame the bridge received — the session never //! negotiation error frame the bridge received; the session never
//! started; the frame's JSON detail stays alktty-side), //! started; the frame's JSON detail stays alktty-side),
//! `"unknown-service"` / `"service-only"` / `"invalid-repo-id"` //! `"unknown-service"` / `"service-only"` / `"invalid-repo-id"`
//! (the exec grammar's git declinables — the parse core's two and //! (the exec grammar's git declinables: the parse core's two and
//! the git row's validation gate; no service token or repo string //! the git row's validation gate; no service token or repo string
//! ever rides — only the vocabulary value), //! ever rides, only the vocabulary value),
//! `"exec-oversize"` (the pre-parse `max_exec_command_len` cap), //! `"exec-oversize"` (the pre-parse `max_exec_command_len` cap),
//! `"invalid-target"` (`w3-tunnel-direct`: the direct rows' //! `"invalid-target"` (`w3-tunnel-direct`: the direct rows'
//! impossible wire target — an out-of-range `direct-tcpip` port), //! impossible wire target, an out-of-range `direct-tcpip` port),
//! `"unix-unavailable"` (`w3-tunnel-direct`: the OQ-11 runtime-Unix //! `"unix-unavailable"` (`w3-tunnel-direct`: the OQ-11 runtime-Unix
//! gate — the off-Unix `direct-streamlocal` decline, answered //! gate: the off-Unix `direct-streamlocal` decline, answered
//! before any fabric call), //! before any fabric call),
//! `"no-identity"` / `"channel-not-held"` (the git row's serving- //! `"no-identity"` / `"channel-not-held"` (the git row's serving-
//! path guards — unreachable in practice, fail-closed; the tunnel //! path guards (unreachable in practice, fail-closed); the tunnel
//! rows' open-level `no-identity` guard rides the same value — //! rows' open-level `no-identity` guard rides the same value,
//! reachable never, on the wire, for the same reason), //! reachable never, on the wire, for the same reason),
//! `"unimplemented"` (the subsystem rows — OQ-13), //! `"unimplemented"` (the subsystem rows, OQ-13),
//! `"declined-by-design"` (env, OQ-09), //! `"declined-by-design"` (env, OQ-09),
//! `"correlation-miss"` / `"cap-exceeded"` / `"accept-refused"` / //! `"correlation-miss"` / `"cap-exceeded"` / `"accept-refused"` /
//! `"unvanilla-inbound"` (`w3-tunnel-listen`: the `-R` leg's arrival //! `"unvanilla-inbound"` (`w3-tunnel-listen`: the `-R` leg's arrival
//! declines — the class whose events carry the minted `listen` token //! declines, the class whose events carry the minted `listen` token
//! instead of a channel id — and the unvanilla inbound //! instead of a channel id, and the unvanilla inbound
//! `forwarded-tcpip` open), //! `forwarded-tcpip` open),
//! `"listen-failed"` / `"no-such-listen"` (`w3-tunnel-listen`: the //! `"listen-failed"` / `"no-such-listen"` (`w3-tunnel-listen`: the
//! listen dial's failed/bounded outcome and the cancel forms' //! listen dial's failed/bounded outcome and the cancel forms'
//! not-found pair) — the global-request and arrival rows, and on the //! not-found pair), belonging to the global-request and arrival
//! auth class `"method-unimplemented"` / `"cert-unsupported"` / //! rows; on the auth class, `"method-unimplemented"` / `"cert-unsupported"` /
//! `"non-ed25519"` / `"fingerprint-unresolved"`. //! `"non-ed25519"` / `"fingerprint-unresolved"`.
//! Client-side (the dial handler's `server_channel_open_*` //! Client-side (the dial handler's `server_channel_open_*`
//! skeleton, `w4-dial`): `"no-acceptor"` (the decline-all default — //! skeleton, `w4-dial`): `"no-acceptor"` (the decline-all default:
//! a server-initiated open with no registered acceptor; the //! a server-initiated open with no registered acceptor; the
//! registry is `w4-reverse-listen`'s) and `"never-feature"` (the //! registry is `w4-reverse-listen`'s) and `"never-feature"` (the
//! x11/agent rows — ADR-003 §5). //! x11/agent rows, ADR-003 §5).
//! - `exec_route` — only on `exec_request`-row events: the dispatch //! - `exec_route` rides only `exec_request`-row events: the dispatch
//! route the parse routed to (`"git"` | `"pipe"`), so a git row's //! route the parse routed to (`"git"` | `"pipe"`), so a git row's
//! decline is distinguishable from a pipe row's in combined builds. //! decline is distinguishable from a pipe row's in combined builds.
//! Other rows carry none (a `None` field is not recorded by //! Other rows carry none (a `None` field is not recorded by
//! `tracing`). //! `tracing`).
//! //!
//! Levels: declines and the lifecycle's started/ended emit at //! Levels: declines and the lifecycle's started/ended emit at
//! `debug` — a decline is the door's normal, honest posture (vanilla //! `debug`: a decline is the door's normal, honest posture (vanilla
//! clients probe unfeatured rows routinely; nothing abnormal //! clients probe unfeatured rows routinely; nothing abnormal
//! happened), matching the family's structural-observation level. //! happened), matching the family's structural-observation level.
//! Only [`serving_path_failed`] is `warn` — a serving path that could //! Only [`serving_path_failed`] is `warn`: a serving path that could
//! not complete its wire reporting is a real failure (the family's //! not complete its wire reporting is a real failure (the family's
//! recoverable-failure level). //! recoverable-failure level).
//! //!
@@ -129,11 +129,11 @@
//! //!
//! **No secret material ever enters an event; never the username.** //! **No secret material ever enters an event; never the username.**
//! Structurally enforced, not recalled: the helpers take only //! Structurally enforced, not recalled: the helpers take only
//! `&'static str` vocabulary values and the wire channel id — there //! `&'static str` vocabulary values and the wire channel id; there
//! is no parameter a username, command string, env name/value, repo //! is no parameter a username, command string, env name/value, repo
//! path, key, or fingerprint could ride. In particular *the //! path, key, or fingerprint could ride. In particular *the
//! fingerprint stays out of the events* (`auth.md` §Dispositions: log //! fingerprint stays out of the events* (`auth.md` §Dispositions: log
//! exposure is deployment policy — the crate's events are //! exposure is deployment policy; the crate's events are
//! safe-by-default at every level, so the fingerprint is a //! safe-by-default at every level, so the fingerprint is a
//! deployer-side computation over its own logs, never crate-emitted). //! deployer-side computation over its own logs, never crate-emitted).
//! The `error` field carries only russh protocol errors. The wire //! The `error` field carries only russh protocol errors. The wire
@@ -141,7 +141,7 @@
//! appears in any captured field. If a future decline site needs //! appears in any captured field. If a future decline site needs
//! request-argument detail (e.g. the git grammar's decline reasons in //! request-argument detail (e.g. the git grammar's decline reasons in
//! `w3-git-mapping`), it must add an explicitly-vocabulary'd field and //! `w3-git-mapping`), it must add an explicitly-vocabulary'd field and
//! record its safety reasoning — dynamic strings do not ride this //! record its safety reasoning; dynamic strings do not ride this
//! module by default. //! module by default.
/// The compile-time feature set, as a stable string (`"none"`, /// The compile-time feature set, as a stable string (`"none"`,
@@ -194,7 +194,7 @@ pub(crate) fn decline_open(row: &'static str, reason: &'static str, channel_id:
/// build does not serve (`subsystem` / `env` / `x11` / agent, an /// build does not serve (`subsystem` / `env` / `x11` / agent, an
/// unfeatured parse route) or a not-yet-landed listen form. The wire /// unfeatured parse route) or a not-yet-landed listen form. The wire
/// answer is `channel_failure` (the channel stays usable) or the /// answer is `channel_failure` (the channel stays usable) or the
/// denied-request reply — the event is the deciding context. /// denied-request reply; the event is the deciding context.
pub(crate) fn decline_request( pub(crate) fn decline_request(
row: &'static str, row: &'static str,
reason: &'static str, reason: &'static str,
@@ -214,7 +214,7 @@ pub(crate) fn decline_request(
/// An auth-class decline: a disposition of the ADR-004 bridge turned /// An auth-class decline: a disposition of the ADR-004 bridge turned
/// the method away. No channel id exists at this surface; the /// the method away. No channel id exists at this surface; the
/// *fingerprint never rides the event* (see §Safety posture) — the /// *fingerprint never rides the event* (see §Safety posture); the
/// `reason` codes distinguish the decline causes without exposing who. /// `reason` codes distinguish the decline causes without exposing who.
pub(crate) fn decline_auth(row: &'static str, reason: &'static str) { pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
tracing::debug!( tracing::debug!(
@@ -226,7 +226,7 @@ pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
); );
} }
/// A request-level decline on a **global** request — the `-R` listen /// A request-level decline on a **global** request: the `-R` listen
/// forms (`mapping.md` §Global-request rows). No channel id exists at /// forms (`mapping.md` §Global-request rows). No channel id exists at
/// this surface; the fields are otherwise the `request` vocabulary. /// this surface; the fields are otherwise the `request` vocabulary.
pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) { pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
@@ -241,12 +241,12 @@ pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
/// A decline on the `-R` leg's **arrival** surface (`w3-tunnel- /// A decline on the `-R` leg's **arrival** surface (`w3-tunnel-
/// listen`): a fabric inbound forwarded open that cannot become an /// listen`): a fabric inbound forwarded open that cannot become an
/// SSH channel — `correlation-miss` (no route for the minted token — /// SSH channel: `correlation-miss` (no route for the minted token,
/// teardown-at-birth), `cap-exceeded` (the per-arrival admission), or /// teardown-at-birth), `cap-exceeded` (the per-arrival admission), or
/// `accept-refused` (the vanilla client rejected the SSH open — the /// `accept-refused` (the vanilla client rejected the SSH open; the
/// count released, never a half-open channel). No SSH channel id /// count released, never a half-open channel). No SSH channel id
/// exists yet at this surface (that is the point of the declines), so /// exists yet at this surface (that is the point of the declines), so
/// the minted `listen` token — the correlation key itself — rides /// the minted `listen` token (the correlation key itself) rides
/// instead; the token is bookkeeping metadata (the producer's minted /// instead; the token is bookkeeping metadata (the producer's minted
/// correlation number), never secret material. /// correlation number), never secret material.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -265,7 +265,7 @@ pub(crate) fn decline_arrival(row: &'static str, reason: &'static str, listen: u
/// failure-then-close rather than serving. This wave that is the seam /// failure-then-close rather than serving. This wave that is the seam
/// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3 /// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3
/// mappings emit it from their real spawned paths wherever serving /// mappings emit it from their real spawned paths wherever serving
/// itself cannot proceed — the class the spawned paths inherit. /// itself cannot proceed, the class the spawned paths inherit.
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
pub(crate) fn decline_serving( pub(crate) fn decline_serving(
row: &'static str, row: &'static str,
@@ -284,7 +284,7 @@ pub(crate) fn decline_serving(
); );
} }
/// Spawned-path started — emitted as the spawned task's first action, /// Spawned-path started, emitted as the spawned task's first action,
/// so "started" is the task's own honest start, not the hook's spawn /// so "started" is the task's own honest start, not the hook's spawn
/// call. The mapping tasks' serving paths inherit this shape. /// call. The mapping tasks' serving paths inherit this shape.
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))] #[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
@@ -302,7 +302,7 @@ pub(crate) fn serving_path_started(
); );
} }
/// Spawned-path ended — the serving future finished (cleanly, or with /// Spawned-path ended: the serving future finished (cleanly, or with
/// the decline it was wired to answer); emitted *after* the path's /// the decline it was wired to answer); emitted *after* the path's
/// wire reporting has completed. /// wire reporting has completed.
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))] #[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
@@ -320,14 +320,14 @@ pub(crate) fn serving_path_ended(
); );
} }
/// Spawned-path failure — the serving path could not complete its /// Spawned-path failure: the serving path could not complete its
/// wire reporting (the session handle's sends failed: the session is /// wire reporting (the session handle's sends failed: the session is
/// gone, or the report never landed). The vocabulary's one `warn` — /// gone, or the report never landed). The vocabulary's one `warn`,
/// the family's recoverable-failure level. Only russh protocol errors /// the family's recoverable-failure level. Only russh protocol errors
/// ride the `error` field (§Safety posture); the mapping tasks emit /// ride the `error` field (§Safety posture); the mapping tasks emit
/// this from their real paths when serving fails before reporting. /// this from their real paths when serving fails before reporting.
/// (The tunnel bridge never calls this: its wire reporting is /// (The tunnel bridge never calls this: its wire reporting is
/// open-level — `accept`/`reject` cannot fail send-wise — so the fn /// open-level (`accept`/`reject` cannot fail send-wise), so the fn
/// stays gated to the git/session rows.) /// stays gated to the git/session rows.)
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
pub(crate) fn serving_path_failed<E: std::fmt::Debug>( pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
@@ -350,7 +350,7 @@ pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
mod tests { mod tests {
use tracing::subscriber::with_default; use tracing::subscriber::with_default;
/// A fake error shape — the failure row only pins that a Debug /// A fake error shape; the failure row only pins that a Debug
/// error value rides the `error` field. /// error value rides the `error` field.
#[derive(Debug)] #[derive(Debug)]
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
@@ -362,10 +362,10 @@ mod tests {
/// A minimal field-capturing visitor shared by these rows and the /// A minimal field-capturing visitor shared by these rows and the
/// wire rows in `tests/observe.rs` (the test convention recorded /// wire rows in `tests/observe.rs` (the test convention recorded
/// in the task notes — the family has no event-capture precedent, /// in the task notes: the family has no event-capture precedent,
/// only alktunnels' fmt-subscriber dev-dep for debug output, so /// only alktunnels' fmt-subscriber dev-dep for debug output, so
/// the convention is a hand-rolled minimal [`tracing::Subscriber`] /// the convention is a hand-rolled minimal [`tracing::Subscriber`]
/// — no `tracing-subscriber` dependency). /// with no `tracing-subscriber` dependency).
struct Visitor(Vec<(String, String)>); struct Visitor(Vec<(String, String)>);
impl tracing::field::Visit for Visitor { impl tracing::field::Visit for Visitor {
@@ -379,7 +379,7 @@ mod tests {
} }
/// Captures the fields of the one event the emit fn fires, via /// Captures the fields of the one event the emit fn fires, via
/// the thread-local default subscriber (`with_default` — these /// the thread-local default subscriber (`with_default`; these
/// are direct helper rows; the spawned-task surfaces are driven /// are direct helper rows; the spawned-task surfaces are driven
/// over the wire in `tests/observe.rs` against the global /// over the wire in `tests/observe.rs` against the global
/// capture). /// capture).
@@ -425,8 +425,8 @@ mod tests {
.unwrap_or_else(|| panic!("no field {name} in {fields:?}")) .unwrap_or_else(|| panic!("no field {name} in {fields:?}"))
} }
/// - *Contract stamp:* the event vocabulary (`w3-decline-tracing`) /// - *Contract stamp:* the event vocabulary (`w3-decline-tracing`):
/// — an open decline carries exactly the base shape, values /// an open decline carries exactly the base shape, values
/// included. /// included.
#[test] #[test]
fn decline_open_carries_the_full_vocabulary() { fn decline_open_carries_the_full_vocabulary() {
@@ -449,11 +449,11 @@ mod tests {
"reason".to_owned(), "reason".to_owned(),
], ],
"an open decline carries exactly the vocabulary, nothing else \ "an open decline carries exactly the vocabulary, nothing else \
(the message field first — the event! macro's shape)" (the message field first, per the event! macro's shape)"
); );
} }
/// - *Contract stamp:* the `exec_route` field — present on the /// - *Contract stamp:* the `exec_route` field: present on the
/// exec row, absent elsewhere (a `None` field is not recorded /// exec row, absent elsewhere (a `None` field is not recorded
/// by `tracing`). /// by `tracing`).
#[test] #[test]
@@ -480,7 +480,7 @@ mod tests {
assert_eq!(field(&fields, "reason"), "method-unimplemented"); assert_eq!(field(&fields, "reason"), "method-unimplemented");
} }
/// - *Contract stamp:* the `exec_route` field — present on the /// - *Contract stamp:* the `exec_route` field: present on the
/// exec row, absent elsewhere (a `None` field is not recorded /// exec row, absent elsewhere (a `None` field is not recorded
/// by `tracing`). The pty/shell lifecycle rows carry no route. /// by `tracing`). The pty/shell lifecycle rows carry no route.
#[cfg(any(feature = "git", feature = "session"))] #[cfg(any(feature = "git", feature = "session"))]
@@ -503,9 +503,9 @@ mod tests {
assert_eq!(field(&fields, "error"), "Probe"); assert_eq!(field(&fields, "error"), "Probe");
} }
/// - *Contract stamp:* the arrival vocabulary (`w3-tunnel-listen`) /// - *Contract stamp:* the arrival vocabulary (`w3-tunnel-listen`):
/// — an arrival decline carries the minted `listen` token /// an arrival decline carries the minted `listen` token
/// instead of a channel id (none exists yet — that is the point /// instead of a channel id (none exists yet; that is the point
/// of the declines; the token is bookkeeping metadata, never /// of the declines; the token is bookkeeping metadata, never
/// secret material). /// secret material).
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -530,7 +530,7 @@ mod tests {
/// The feature-build string: every enabled feature appears; a /// The feature-build string: every enabled feature appears; a
/// featureless build reads `none`. The expected half is derived /// featureless build reads `none`. The expected half is derived
/// from `cfg!` independently of the crate's match — divergence /// from `cfg!` independently of the crate's match, so divergence
/// breaks this row loudly. /// breaks this row loudly.
#[test] #[test]
fn feature_build_string_reflects_the_cfgs() { fn feature_build_string_reflects_the_cfgs() {
+11 -11
View File
@@ -1,22 +1,22 @@
//! The spawn-and-return plumbing (`w2-spawn-return`) — ADR-006 //! The spawn-and-return plumbing (`w2-spawn-return`): ADR-006
//! decision 4, the structural constraint of the whole spine, in code. //! decision 4, the structural constraint of the whole spine, in code.
//! //!
//! russh at this pin runs server handler callbacks **inline inside the //! russh at this pin runs server handler callbacks **inline inside the
//! session's packet loop** — a blocking handler suspends all packet //! session's packet loop**: a blocking handler suspends all packet
//! routing for the connection and deadlocks against the peer's //! routing for the connection and deadlocks against the peer's
//! immediate follow-up bytes (POC-3 proved by construction with //! immediate follow-up bytes (POC-3 proved by construction with
//! receive-pack). The crate's answer is mechanical, not conventional: //! receive-pack). The crate's answer is mechanical, not conventional:
//! //!
//! - **Every serving path is spawned** ([`spawn_serving_path`]) — the //! - **Every serving path is spawned** ([`spawn_serving_path`]): the
//! handler hook clones the session handle //! handler hook clones the session handle
//! (`Session::handle()`), hands it into its serving future, spawns, //! (`Session::handle()`), hands it into its serving future, spawns,
//! and returns. No serving path is ever awaited inline; the dispatch //! and returns. No serving path is ever awaited inline; the dispatch
//! seam stubs ([`crate::handler`]) hang off this helper, so the //! seam stubs ([`crate::handler`]) hang off this helper, so the
//! wave-3 mappings inherit the pattern mechanically from the stubs. //! wave-3 mappings inherit the pattern mechanically from the stubs.
//! The serving-path *events* (started / ended / failure — //! The serving-path *events* (started / ended / failure, via
//! [`crate::observe`]) are the spawned task's own actions: the stub //! [`crate::observe`]) are the spawned task's own actions: the stub
//! body emits them, the mappings' real paths will too. //! body emits them, the mappings' real paths will too.
//! - **Exit/status reporting rides the cloned handle** — the spawned //! - **Exit/status reporting rides the cloned handle**: the spawned
//! task's only report surface is the handle clone it was handed; //! task's only report surface is the handle clone it was handed;
//! [`report_exit_status_then_close`] preserves the wire ordering the //! [`report_exit_status_then_close`] preserves the wire ordering the
//! `mapping.md` `exit_status` row pins: `exit-status` sent **before** //! `mapping.md` `exit_status` row pins: `exit-status` sent **before**
@@ -28,7 +28,7 @@
//! //!
//! ## Test surface //! ## Test surface
//! //!
//! Both helpers are re-exported `#[doc(hidden)]` from the crate root — //! Both helpers are re-exported `#[doc(hidden)]` from the crate root,
//! the `handler.rs` §Test surface pattern: the mechanics-proof rows in //! the `handler.rs` §Test surface pattern: the mechanics-proof rows in
//! `tests/spawn_return.rs` compose them over the real wire (a minimal //! `tests/spawn_return.rs` compose them over the real wire (a minimal
//! proof handler standing in for the crate's own auth/dispatch hooks, //! proof handler standing in for the crate's own auth/dispatch hooks,
@@ -41,7 +41,7 @@ use russh::ChannelId;
use russh::server::Handle; use russh::server::Handle;
/// Spawns one serving path on its own task (ADR-006 decision 4): the /// Spawns one serving path on its own task (ADR-006 decision 4): the
/// caller — the handler hook — clones the session handle, hands it /// caller (the handler hook) clones the session handle, hands it
/// into the serving future, calls this, and returns immediately. /// into the serving future, calls this, and returns immediately.
/// Nothing here is ever awaited inline: an inline serve deadlocks /// Nothing here is ever awaited inline: an inline serve deadlocks
/// against the peer's follow-up bytes (POC-3 proved by construction). /// against the peer's follow-up bytes (POC-3 proved by construction).
@@ -50,7 +50,7 @@ pub fn spawn_serving_path(serving: impl Future<Output = ()> + Send + 'static) {
} }
/// The spawned task's exit report through the cloned handle: /// The spawned task's exit report through the cloned handle:
/// `exit-status(N)` then close — the ordering contract /// `exit-status(N)` then close; the ordering contract
/// (`mapping.md`'s `exit_status` row: sent **before** close, from the /// (`mapping.md`'s `exit_status` row: sent **before** close, from the
/// spawned task). A failed send means the session is gone; nothing is /// spawned task). A failed send means the session is gone; nothing is
/// reportable then. /// reportable then.
@@ -67,8 +67,8 @@ mod tests {
use super::spawn_serving_path; use super::spawn_serving_path;
/// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return: the /// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return: the
/// handler call returns while the serving path is still running /// handler call returns while the serving path is still running,
/// — handler-returns-fast is structural). /// so handler-returns-fast is structural).
#[tokio::test] #[tokio::test]
async fn spawn_returns_while_the_serving_path_is_still_pending() { async fn spawn_returns_while_the_serving_path_is_still_pending() {
let (release, waited) = tokio::sync::oneshot::channel::<()>(); let (release, waited) = tokio::sync::oneshot::channel::<()>();
@@ -82,7 +82,7 @@ mod tests {
assert!( assert!(
!done.load(Ordering::Acquire), !done.load(Ordering::Acquire),
"the spawn call returned while the serving path was still gated — \ "the spawn call returned while the serving path was still gated; \
no serving path is awaited inline" no serving path is awaited inline"
); );
+59 -58
View File
@@ -1,11 +1,11 @@
//! The session mapping (`w3-tty-session`) — the biggest single-row //! The session mapping (`w3-tty-session`), the biggest single-row
//! surface of the spine ([`crate::dispatch::ExecRoute::Pipe`] + the //! surface of the spine ([`crate::dispatch::ExecRoute::Pipe`] + the
//! `pty_request`/`shell_request` rows): pty/shell/pipe-exec serve //! `pty_request`/`shell_request` rows): pty/shell/pipe-exec serve
//! through the published alktty seam //! through the published alktty seam
//! (`drive_session_pre_negotiated(writer, reader, req, backends, //! (`drive_session_pre_negotiated(writer, reader, req, backends,
//! ownership, identity)`) over a **translation bridge, not a byte //! ownership, identity)`) over a **translation bridge, not a byte
//! pump** (POC-3 §D.1): alktty's data plane is chunk-framed both //! pump** (POC-3 §D.1): alktty's data plane is chunk-framed both
//! directions (`alktty::wire` — 5-byte header + payload), the SSH //! directions (`alktty::wire`, 5-byte header + payload), the SSH
//! channel is a plain byte plane, so the bridge wraps the channel //! channel is a plain byte plane, so the bridge wraps the channel
//! with small adapters around a pair of in-memory duplexes. //! with small adapters around a pair of in-memory duplexes.
//! //!
@@ -15,7 +15,7 @@
//! //!
//! - `pty_request` args → `alktty::negotiation::TerminalParamsWire` //! - `pty_request` args → `alktty::negotiation::TerminalParamsWire`
//! (`term`/`cols`/`rows`/pixel dims; SSH ttymodes → the **reserved //! (`term`/`cols`/`rows`/pixel dims; SSH ttymodes → the **reserved
//! modes JSON `{}`** — alktty OQ-44's reservation: backends must //! modes JSON `{}`** per alktty OQ-44's reservation: backends must
//! ignore modes in v1, so the door writes `{}` and defers //! ignore modes in v1, so the door writes `{}` and defers
//! faithfully). Stored per channel (the POC-3 accepted-but-held //! faithfully). Stored per channel (the POC-3 accepted-but-held
//! shape, [`crate::handler`]'s store) and consumed by the later //! shape, [`crate::handler`]'s store) and consumed by the later
@@ -24,29 +24,29 @@
//! - `shell_request` → `NegotiateRequest{carriage: "raw", backend: //! - `shell_request` → `NegotiateRequest{carriage: "raw", backend:
//! the seam selector, tty: the consumed pty params, cmd: the //! the seam selector, tty: the consumed pty params, cmd: the
//! serve-config shell argv **verbatim**, cwd: session_cwd, env: //! serve-config shell argv **verbatim**, cwd: session_cwd, env:
//! session_env}` — the wire never carries command, args, cwd or //! session_env}`, and the wire never carries command, args, cwd or
//! env (OQ-08/OQ-09; the `env_request` row stays declined //! env (OQ-08/OQ-09; the `env_request` row stays declined
//! unconditionally). **A `shell_request` without a preceding pty //! unconditionally). **A `shell_request` without a preceding pty
//! serves pipe-mode (`tty = None`)** — decided: vanilla sshd serves //! serves pipe-mode (`tty = None`)**, decided: vanilla sshd serves
//! a shell without a terminal too (`ssh -T host` is legitimate //! a shell without a terminal too (`ssh -T host` is legitimate
//! vanilla), so a pty-less shell request is served, never declined. //! vanilla), so a pty-less shell request is served, never declined.
//! - `exec_request` (pipe row) → `cmd = exec_wrapper_argv ++ [cmd]` //! - `exec_request` (pipe row) → `cmd = exec_wrapper_argv ++ [cmd]`
//! (the OQ-08 argv append — vanilla quoting semantics; the wire //! (the OQ-08 argv append, vanilla quoting semantics; the wire
//! command string is **never naively split**), `tty = //! command string is **never naively split**), `tty =
//! Some(consumed pty)` when a pty preceded, else `None` (pipe //! Some(consumed pty)` when a pty preceded, else `None` (pipe
//! mode). //! mode).
//! - **Unset-declines** (OQ-08, never a fallback): `shell_argv` //! - **Unset-declines** (OQ-08, never a fallback): `shell_argv`
//! unset → `shell_request` declines; `exec_wrapper_argv` unset → //! unset → `shell_request` declines; `exec_wrapper_argv` unset →
//! the pipe row declines. Wire shape (decided + recorded): the //! the pipe row declines. Wire shape (decided + recorded): the
//! **serving-path shape — `channel_failure` then channel close, //! **serving-path shape: `channel_failure` then channel close,
//! from a spawned task** ([`crate::serve`]'s mechanics; the same //! from a spawned task** ([`crate::serve`]'s mechanics; the same
//! spawned-stub body the git row uses) — the class of a routable //! spawned-stub body the git row uses), the class of a routable
//! row that owns the request's whole outcome; the session channel //! row that owns the request's whole outcome; the session channel
//! closes the way it would when the request itself cannot exist. //! closes the way it would when the request itself cannot exist.
//! `session_seam` unset (feature on, seam absent) → the same //! `session_seam` unset (feature on, seam absent) → the same
//! shape, reason `seam-unconfigured` (the decline matrix's //! shape, reason `seam-unconfigured` (the decline matrix's
//! config-driven note); `pty_request` checks the seam too — a pty //! config-driven note); `pty_request` checks the seam too, since a
//! whose session can never serve is a doomed allocation. Field //! pty whose session can never serve is a doomed allocation. Field
//! checks precede the seam check (the row's own unset field is the //! checks precede the seam check (the row's own unset field is the
//! first decline); the decline never takes the channel and never //! first decline); the decline never takes the channel and never
//! consumes stored pty state. //! consumes stored pty state.
@@ -63,10 +63,10 @@
//! - **SSH → chunks** (the `client_recv` side of alktty's session): //! - **SSH → chunks** (the `client_recv` side of alktty's session):
//! a bounded [`tokio::sync::mpsc`] (the //! a bounded [`tokio::sync::mpsc`] (the
//! [`CONTROL_SURFACE_BOUND`] bound) feeds a framing task that //! [`CONTROL_SURFACE_BOUND`] bound) feeds a framing task that
//! writes `alktty::wire::Chunk`s into the duplex — the same mpsc //! writes `alktty::wire::Chunk`s into the duplex, the same mpsc
//! the control rows ride ([`BridgeSurface`], §below). SSH `data` → //! the control rows ride ([`BridgeSurface`], §below). SSH `data` →
//! `Chunk::stdin`; SSH channel EOF → **the zero-length //! `Chunk::stdin`; SSH channel EOF → **the zero-length
//! stdin-chunk sentinel** — decided (verified against alktty 0.8.1's //! stdin-chunk sentinel**, decided (verified against alktty 0.8.1's
//! adapter pump, `adapter.rs::pump_client_to_backend`): a //! adapter pump, `adapter.rs::pump_client_to_backend`): a
//! zero-length stdin chunk shuts the backend's stdin, *identical //! zero-length stdin chunk shuts the backend's stdin, *identical
//! in effect* to `ControlMessage::Eof` on `STREAM_CTRL_IN`; the //! in effect* to `ControlMessage::Eof` on `STREAM_CTRL_IN`; the
@@ -80,9 +80,9 @@
//! *error frame* is length-prefixed JSON whose first length byte //! *error frame* is length-prefixed JSON whose first length byte
//! is `0x00`; a raw chunk's first byte is a `stream_type` ≤ 4). //! is `0x00`; a raw chunk's first byte is a `stream_type` ≤ 4).
//! stdout chunks → SSH `data`; stderr chunks → SSH //! stdout chunks → SSH `data`; stderr chunks → SSH
//! `extended_data(1)` (PTY mode merges upstream — the backend's //! `extended_data(1)` (PTY mode merges upstream; the backend's
//! stderr stream is `None` there, so the translation is universal: //! stderr stream is `None` there, so the translation is universal
//! no mode-switch exists); the exit chunk (`STREAM_CTRL_OUT`) → //! and no mode-switch exists); the exit chunk (`STREAM_CTRL_OUT`) →
//! the derivation contract (§below). No unbounded buffering in //! the derivation contract (§below). No unbounded buffering in
//! either adapter (AGENTS.md convention 9). //! either adapter (AGENTS.md convention 9).
//! - **The negotiation-error frame is the bridge's serving-path //! - **The negotiation-error frame is the bridge's serving-path
@@ -91,23 +91,24 @@
//! ownership), and a failure answers a length-prefixed error //! ownership), and a failure answers a length-prefixed error
//! frame instead of chunks. Decided + recorded: the bridge maps //! frame instead of chunks. Decided + recorded: the bridge maps
//! that frame to a serving-path decline event //! that frame to a serving-path decline event
//! (`negotiation-failed`) and an **`exit-status(255)` then close** //! (`negotiation-failed`) and an **`exit-status(255)` then close**,
//! — the one place this door claims 255: the session **never //! the one place this door claims 255: the session **never
//! started** (no exit chunk exists, nothing ran, so 255 is the //! started** (no exit chunk exists, nothing ran, so 255 is the
//! honest could-not-start semantic — contrast the git row, whose //! honest could-not-start semantic, contrasted with the git row,
//! whose
//! service *runs* its substrate and honestly reports `1`). The //! service *runs* its substrate and honestly reports `1`). The
//! error frame's JSON detail stays alktty-side; only the //! error frame's JSON detail stays alktty-side; only the
//! vocabulary value rides the event. //! vocabulary value rides the event.
//! - A bridge read that ends **without** an exit chunk reports //! - A bridge read that ends **without** an exit chunk reports
//! nothing (the chunk is the sole fidelity input; its absence is //! nothing (the chunk is the sole fidelity input; its absence is
//! neither 0 nor 255 — no fabricated report): plain close, and //! neither 0 nor 255, no fabricated report): plain close, and
//! the lifecycle's ended event. //! the lifecycle's ended event.
//! - **Known limit (recorded disposition, wave-3 gate booked item 3): //! - **Known limit (recorded disposition, wave-3 gate booked item 3):
//! the stdout [`tokio::io::AsyncWriteExt::write_all`] carries no //! the stdout [`tokio::io::AsyncWriteExt::write_all`] carries no
//! deadline.** Backpressure is bounded (the bounded bridging duplex, //! deadline.** Backpressure is bounded (the bounded bridging duplex,
//! russh's per-channel window and the client's own flow control bound //! russh's per-channel window and the client's own flow control bound
//! what can pile up) and liveness is covered by the inactivity //! what can pile up) and liveness is covered by the inactivity
//! timeout ([`crate::Limits::inactivity_timeout`], always set) — a //! timeout ([`crate::Limits::inactivity_timeout`], always set); a
//! deadline here would be a behavior change (a slow client may stall //! deadline here would be a behavior change (a slow client may stall
//! its stream, vanilla sshd's posture included), not a coverage //! its stream, vanilla sshd's posture included), not a coverage
//! minor. So the limit is recorded, not "fixed". //! minor. So the limit is recorded, not "fixed".
@@ -116,7 +117,7 @@
//! //!
//! The requests arrive on the packet loop after the serving task //! The requests arrive on the packet loop after the serving task
//! owns the wire, and reach the spawned bridge through the //! owns the wire, and reach the spawned bridge through the
//! **per-channel control surface** ([`BridgeSurface`] — handler //! **per-channel control surface** ([`BridgeSurface`], handler
//! state while the row serves): a bounded mpsc whose send half //! state while the row serves): a bounded mpsc whose send half
//! forward_control `try_send`s the control chunk (never awaited on //! forward_control `try_send`s the control chunk (never awaited on
//! the packet loop). `window_change_request` → //! the packet loop). `window_change_request` →
@@ -124,23 +125,23 @@
//! (alktty's uppercase-name vocabulary). Decisions, recorded: //! (alktty's uppercase-name vocabulary). Decisions, recorded:
//! the SSH control rows are fire-and-forget (no reply exists on //! the SSH control rows are fire-and-forget (no reply exists on
//! the wire), so **a full or closed surface drops the control //! the wire), so **a full or closed surface drops the control
//! event** — the bound keeps a stalled bridge from stalling the //! event**; the bound keeps a stalled bridge from stalling the
//! packet loop (bounded resources, convention 9); **signal names //! packet loop (bounded resources, convention 9); **signal names
//! are accepted verbatim** — no alkssh-side name filter: the //! are accepted verbatim**, no alkssh-side name filter: the
//! backend owns the rejection semantics (alktty passes the name to //! backend owns the rejection semantics (alktty passes the name to
//! the backend's control handle, whose semantics decide), and the //! the backend's control handle, whose semantics decide), and the
//! pipe-mode pid-only vs PTY process-group *scope* is backend //! pipe-mode pid-only vs PTY process-group *scope* is backend
//! semantics (the mapping row note), so there is **no decline //! semantics (the mapping row note), so there is **no decline
//! shape** for a name — nothing on the wire can answer. A resize //! shape** for a name; nothing on the wire can answer. A resize
//! arriving for a channel whose pty is stored but not yet serving //! arriving for a channel whose pty is stored but not yet serving
//! updates the stored params instead (the final size wins — //! updates the stored params instead (the final size wins,
//! vanilla sshd's own ordering); a resize with neither a serving //! vanilla sshd's own ordering); a resize with neither a serving
//! bridge nor a stored pty is dropped. //! bridge nor a stored pty is dropped.
//! //!
//! ## The exit derivation contract (mapping.md §Exit chunk, rules 1–6) //! ## The exit derivation contract (mapping.md §Exit chunk, rules 1–6)
//! //!
//! Normative text and the whole derivation machinery now live in //! Normative text and the whole derivation machinery now live in
//! [`crate::exit_code`] — the shared exit-currency module consumed by //! [`crate::exit_code`], the shared exit-currency module consumed by
//! *both* directions (`w4-op-rows` extracted it out of this module: //! *both* directions (`w4-op-rows` extracted it out of this module:
//! the client rows consume wire exit reports back into the family's //! the client rows consume wire exit reports back into the family's
//! `i32` exit-code convention, the exact inverse arm of the same //! `i32` exit-code convention, the exact inverse arm of the same
@@ -156,11 +157,11 @@
//! //!
//! The serving task watches two signals: the **per-channel cancel** //! The serving task watches two signals: the **per-channel cancel**
//! (fired by `channel_close`, or surfaced by the surface's drop) and //! (fired by `channel_close`, or surfaced by the surface's drop) and
//! the **connection teardown** ([`Teardown`] — fired by the //! the **connection teardown** ([`Teardown`], fired by the
//! handler's `Drop`; the russh session ended, however it ended). On //! handler's `Drop`; the russh session ended, however it ended). On
//! either, the task aborts the alktty session task — dropping its //! either, the task aborts the alktty session task, dropping its
//! not-yet-resolved `exit_code` future, which is alktty's //! not-yet-resolved `exit_code` future (alktty's
//! kill-on-drop contract firing (its ADR-056) — then closes the SSH //! kill-on-drop contract firing, its ADR-056), then closes the SSH
//! channel and ends. Nothing is fabricated on a cancel. The //! channel and ends. Nothing is fabricated on a cancel. The
//! detached stdin/framing tasks are bounded by their duplex/mpsc/ //! detached stdin/framing tasks are bounded by their duplex/mpsc/
//! russh-window bounds and end on the same signals' effects. //! russh-window bounds and end on the same signals' effects.
@@ -182,7 +183,7 @@ use crate::config::SessionSeam;
use crate::exit_code::{ExitReport, derive_exit, sig_from_name}; use crate::exit_code::{ExitReport, derive_exit, sig_from_name};
use crate::observe; use crate::observe;
/// The negotiation frame the bridge speaks locally — every locally /// The negotiation frame the bridge speaks locally; every locally
/// constructed request reads `"raw"` (alktty's v1 carriage). /// constructed request reads `"raw"` (alktty's v1 carriage).
pub(crate) const CARRIAGE: &str = "raw"; pub(crate) const CARRIAGE: &str = "raw";
@@ -192,11 +193,11 @@ pub(crate) const CARRIAGE: &str = "raw";
/// (§the control rows). The shim's scripted rows exercise a handful. /// (§the control rows). The shim's scripted rows exercise a handful.
pub(crate) const CONTROL_SURFACE_BOUND: usize = 64; pub(crate) const CONTROL_SURFACE_BOUND: usize = 64;
/// The translation bridge duplex capacity — the wave-2 loopback /// The translation bridge duplex capacity, the wave-2 loopback
/// harness's per-direction convention, kept for the bridge pairs. /// harness's per-direction convention, kept for the bridge pairs.
const BRIDGE_DUPLEX_CAPACITY: usize = 65_536; const BRIDGE_DUPLEX_CAPACITY: usize = 65_536;
/// The stdin pump's read buffer — the SSH-side chunk granularity /// The stdin pump's read buffer, the SSH-side chunk granularity
/// (per-chunk payloads are bounded in practice by russh's /// (per-chunk payloads are bounded in practice by russh's
/// `maximum_packet_size`, here by [`MAX_CHUNK_LEN`] on the frame). /// `maximum_packet_size`, here by [`MAX_CHUNK_LEN`] on the frame).
const STDIN_PUMP_BUF: usize = 16 * 1024; const STDIN_PUMP_BUF: usize = 16 * 1024;
@@ -210,7 +211,7 @@ pub(crate) fn clamp_u16(value: u32) -> u16 {
/// The `pty_request` args → `TerminalParamsWire` conversion (§the /// The `pty_request` args → `TerminalParamsWire` conversion (§the
/// negotiation section): SSH ttymodes → the **reserved modes JSON /// negotiation section): SSH ttymodes → the **reserved modes JSON
/// `{}`** (alktty OQ-44's reservation — the map is honest: nothing /// `{}`** (alktty OQ-44's reservation; the map is honest: nothing
/// is carried, exactly what the reservation asks). /// is carried, exactly what the reservation asks).
pub fn terminal_params( pub fn terminal_params(
term: &str, term: &str,
@@ -251,15 +252,15 @@ pub(crate) fn negotiate_request(
} }
} }
/// One parsed bridge-side frame from alktty's `client_send` half — /// One parsed bridge-side frame from alktty's `client_send` half:
/// the 4-byte first-window disambiguation's outcome (§the /// the 4-byte first-window disambiguation's outcome (§the
/// translation bridge). /// translation bridge).
pub(crate) enum BridgeFrame { pub(crate) enum BridgeFrame {
/// A raw alktty chunk (data or server→client control). /// A raw alktty chunk (data or server→client control).
Chunk(Chunk), Chunk(Chunk),
/// The length-prefixed negotiation error frame — the session /// The length-prefixed negotiation error frame: the session
/// failed validation/allocation/ownership at the seam. The bytes /// failed validation/allocation/ownership at the seam. The bytes
/// are alktty-side detail (never read here — only the /// are alktty-side detail (never read here, only the
/// vocabulary value rides the decline event). /// vocabulary value rides the decline event).
NegotiationError, NegotiationError,
/// The half ended (alktty's task completed or was cancelled) /// The half ended (alktty's task completed or was cancelled)
@@ -269,10 +270,10 @@ pub(crate) enum BridgeFrame {
Break, Break,
} }
/// Reads one bridge frame — the disambiguation invariant: the first /// Reads one bridge frame (the disambiguation invariant: the first
/// byte `0x00` is a negotiation error frame's length-prefix high /// byte `0x00` is a negotiation error frame's length-prefix high
/// byte; anything ≤ 4 is a chunk `stream_type`. Bounds-checked /// byte; anything ≤ 4 is a chunk `stream_type`). Bounds-checked
/// against [`MAX_CHUNK_LEN`] both directions — a rogue length never /// against [`MAX_CHUNK_LEN`] both directions, so a rogue length never
/// triggers an oversized read. /// triggers an oversized read.
pub(crate) async fn read_bridge_frame( pub(crate) async fn read_bridge_frame(
reader: &mut (impl tokio::io::AsyncRead + Unpin), reader: &mut (impl tokio::io::AsyncRead + Unpin),
@@ -318,8 +319,8 @@ pub(crate) async fn read_bridge_frame(
/// The exit chunk's JSON payload → the derived code /// The exit chunk's JSON payload → the derived code
/// (`{"type":"exit","code":N}`). Anything else on `STREAM_CTRL_OUT` /// (`{"type":"exit","code":N}`). Anything else on `STREAM_CTRL_OUT`
/// has no contract meaning — and an exit chunk that cannot parse has /// has no contract meaning, and an exit chunk that cannot parse has
/// no fidelity input — both map to `None` (no report; the plain /// no fidelity input; both map to `None` (no report; the plain
/// close posture). /// close posture).
fn exit_chunk_code(payload: &[u8]) -> Option<i32> { fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
ControlMessage::from_slice(payload) ControlMessage::from_slice(payload)
@@ -330,7 +331,7 @@ fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
}) })
} }
/// The per-channel bridge surface — handler state while a session row /// The per-channel bridge surface: handler state while a session row
/// serves (§the control rows). The handler's `window_change_request` /// serves (§the control rows). The handler's `window_change_request`
/// / `signal` hooks reach the spawned bridge **through this struct** /// / `signal` hooks reach the spawned bridge **through this struct**
/// (the requests arrive on the packet loop after the serving task /// (the requests arrive on the packet loop after the serving task
@@ -338,17 +339,17 @@ fn exit_chunk_code(payload: &[u8]) -> Option<i32> {
/// per-channel cancel signal `channel_close` fires. Packet-loop only /// per-channel cancel signal `channel_close` fires. Packet-loop only
/// (single-writer; no lock). /// (single-writer; no lock).
pub(crate) struct BridgeSurface { pub(crate) struct BridgeSurface {
/// The control-surface send half — the same bounded mpsc the /// The control-surface send half, the same bounded mpsc the
/// serving task's stdin pump feeds. /// serving task's stdin pump feeds.
pub(crate) sender: mpsc::Sender<Chunk>, pub(crate) sender: mpsc::Sender<Chunk>,
/// The per-channel cancel signal — `channel_close` fires it. The /// The per-channel cancel signal (`channel_close` fires it). The
/// serving task interprets the fire *or* the half's drop as the /// serving task interprets the fire *or* the half's drop as the
/// cancel. /// cancel.
pub(crate) cancel: watch::Sender<bool>, pub(crate) cancel: watch::Sender<bool>,
} }
impl BridgeSurface { impl BridgeSurface {
/// Forwards one control row into the bridge — `try_send`, never /// Forwards one control row into the bridge via `try_send`, never
/// awaited on the packet loop (§the control rows); a full or /// awaited on the packet loop (§the control rows); a full or
/// closed surface drops the event. /// closed surface drops the event.
pub(crate) fn forward_control(&self, message: &ControlMessage) -> bool { pub(crate) fn forward_control(&self, message: &ControlMessage) -> bool {
@@ -404,12 +405,12 @@ impl Teardown {
} }
} }
/// Reads the SSH half of the channel and feeds stdin chunks — the /// Reads the SSH half of the channel and feeds stdin chunks, the
/// SSH `data` → `Chunk::stdin` adapter (§the translation bridge). /// SSH `data` → `Chunk::stdin` adapter (§the translation bridge).
/// On EOF or a read error it sends **the zero-length stdin-chunk /// On EOF or a read error it sends **the zero-length stdin-chunk
/// sentinel** (the EOF translation decision: identical in effect to /// sentinel** (the EOF translation decision: identical in effect to
/// alktty's `ControlMessage::Eof` — the backend's stdin shuts while /// alktty's `ControlMessage::Eof`, with the backend's stdin shut
/// the session keeps pumping) and ends. /// while the session keeps pumping) and ends.
async fn stdin_pump( async fn stdin_pump(
ssh_read: impl tokio::io::AsyncRead + Unpin + 'static, ssh_read: impl tokio::io::AsyncRead + Unpin + 'static,
chunks: mpsc::Sender<Chunk>, chunks: mpsc::Sender<Chunk>,
@@ -450,9 +451,9 @@ async fn stdin_pump(
} }
/// Consumes the framed-input mpsc and writes alktty chunks into the /// Consumes the framed-input mpsc and writes alktty chunks into the
/// session's `client_recv` duplex half — the framing task. Ends after /// session's `client_recv` duplex half, the framing task. Ends after
/// the EOF sentinel is written (nothing rides the data path past /// the EOF sentinel is written (nothing rides the data path past
/// SSH EOF), on a mpsc close (every sender gone — the cancelled or /// SSH EOF), on a mpsc close (every sender gone, the cancelled or
/// completed session), or on a framing failure. /// completed session), or on a framing failure.
async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::DuplexStream) { async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::DuplexStream) {
let mut writer = ChunkWriter::new(writer); let mut writer = ChunkWriter::new(writer);
@@ -468,7 +469,7 @@ async fn framing_task(mut chunks: mpsc::Receiver<Chunk>, writer: tokio::io::Dupl
/// The exit report + close + lifecycle event, in the ADR-006 /// The exit report + close + lifecycle event, in the ADR-006
/// decision-4 order: **status before close, from the spawned task**. /// decision-4 order: **status before close, from the spawned task**.
/// A failed send means the session is gone — the lifecycle's one /// A failed send means the session is gone; the lifecycle's one
/// `warn` records it (the report that could not land). /// `warn` records it (the report that could not land).
async fn report_exit( async fn report_exit(
handle: &Handle, handle: &Handle,
@@ -501,7 +502,7 @@ async fn report_exit(
} }
} }
/// Fails the request and closes the channel from the serving path — /// Fails the request and closes the channel from the serving path,
/// the serving-path decline shape ([`crate::serve`]'s mechanics) with /// the serving-path decline shape ([`crate::serve`]'s mechanics) with
/// the lifecycle events and the given reason. Used for `no-identity`. /// the lifecycle events and the given reason. Used for `no-identity`.
async fn fail_and_close( async fn fail_and_close(
@@ -528,7 +529,7 @@ async fn fail_and_close(
/// [`alktty::drive_session_pre_negotiated`]; the alktty session task /// [`alktty::drive_session_pre_negotiated`]; the alktty session task
/// and the SSH→chunks pump are spawned here, and *this* future drives /// and the SSH→chunks pump are spawned here, and *this* future drives
/// the chunks→SSH direction inline so the exit report is the path's /// the chunks→SSH direction inline so the exit report is the path's
/// own final act (status before close — ADR-006 decision 4). The /// own final act (status before close, per ADR-006 decision 4). The
/// channel was taken by the dispatch; the spawned task owns the /// channel was taken by the dispatch; the spawned task owns the
/// stream and its report surface. /// stream and its report surface.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -688,14 +689,14 @@ async fn drive_chunks_to_ssh(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
/// - *Contract stamp:* the no-exit-chunk close family's frame /// - *Contract stamp:* the no-exit-chunk close family's frame
/// classification (`w4-tty-coverage` — the wire row in /// classification (`w4-tty-coverage`; the wire row in
/// `tests/tty_mapping.rs` lands the family's serving outcome): a /// `tests/tty_mapping.rs` lands the family's serving outcome): a
/// clean read-end classifies `Closed`, an oversized claimed /// clean read-end classifies `Closed`, an oversized claimed
/// length classifies `Break` — bounds-checked both directions, /// length classifies `Break` (bounds-checked both directions,
/// never an oversized read. `Break`'s serving arm is one body /// never an oversized read). `Break`'s serving arm is one body
/// with `Closed`'s; through alktty's own validated `ChunkWriter` /// with `Closed`'s; through alktty's own validated `ChunkWriter`
/// a `Break` frame is unreachable on the duplex (the drainer /// a `Break` frame is unreachable on the duplex (the drainer
/// rejects the oversized chunk locally and the read sees EOF — /// rejects the oversized chunk locally and the read sees EOF,
/// the wire row's construction), so the classification rows pin /// the wire row's construction), so the classification rows pin
/// the defensive arm at the frame layer. /// the defensive arm at the frame layer.
#[tokio::test] #[tokio::test]
+95 -95
View File
@@ -1,23 +1,23 @@
//! The `direct-tcpip` / `direct-streamlocal` mapping — the tunnel //! The `direct-tcpip` / `direct-streamlocal` mapping, the tunnel
//! branch's route-1 bridge (`w3-tunnel-direct`, ADR-006 decision 1): //! branch's route-1 bridge (`w3-tunnel-direct`, ADR-006 decision 1):
//! the adapter-side bridge POC-3 stage C proved, re-landed as contract //! the adapter-side bridge POC-3 stage C proved, re-landed as contract
//! against published alktunnels 0.4.1, with the forwarded serving //! against published alktunnels 0.4.1, with the forwarded serving
//! surface adopted to alktunnels 0.5.0's establisher shape (the //! surface adopted to alktunnels 0.5.0's establisher shape (the
//! review-003 W2 adoption — the `ForwardedGate` correlation gate). //! review-003 W2 adoption, the `ForwardedGate` correlation gate).
//! //!
//! ## The route-1 bridge (ADR-006 decision 1) //! ## The route-1 bridge (ADR-006 decision 1)
//! //!
//! alkssh holds the assembly-supplied `Arc<ChannelClient>` into the //! alkssh holds the assembly-supplied `Arc<ChannelClient>` into the
//! family fabric (the serve-config's `channel_client` — the wave-1 //! family fabric (the serve-config's `channel_client`, the wave-1
//! config field's consuming code). Per accepted `direct-tcpip` / //! config field's consuming code). Per accepted `direct-tcpip` /
//! `direct-streamlocal` open, the target/substrate are constructed //! `direct-streamlocal` open, the target/substrate are constructed
//! from the SSH open arguments (`direct-tcpip`(host, port) → //! from the SSH open arguments (`direct-tcpip`(host, port) →
//! `SubstrateAddr::HostPort` + `Substrate::Tcp`; //! `SubstrateAddr::HostPort` + `Substrate::Tcp`;
//! `direct-streamlocal`(socket path) → `SubstrateAddr::Path` + //! `direct-streamlocal`(socket path) → `SubstrateAddr::Path` +
//! `Substrate::Unix` — the mapping.md row's treatment) and the flow is //! `Substrate::Unix`, the mapping.md row's treatment) and the flow is
//! exactly the POC recipe: **`TunnelSession::open_direct(&client, //! exactly the POC recipe: **`TunnelSession::open_direct(&client,
//! target, substrate)` → accept only after the fabric open succeeds** //! target, substrate)` → accept only after the fabric open succeeds**
//! (`ChannelOpenHandle::accept` consumes the handle — the reject shape //! (`ChannelOpenHandle::accept` consumes the handle; the reject shape
//! is only possible pre-accept, which is what makes accept-after-open //! is only possible pre-accept, which is what makes accept-after-open
//! the only honest flow) → on success `take_halves()` pumped against //! the only honest flow) → on success `take_halves()` pumped against
//! the taken channel's stream ([`alkcall::channels::pump::pump_bidi`], //! the taken channel's stream ([`alkcall::channels::pump::pump_bidi`],
@@ -25,20 +25,20 @@
//! the spawn-and-return constraint. The open hook returns fast with //! the spawn-and-return constraint. The open hook returns fast with
//! the fabric open in flight. //! the fabric open in flight.
//! //!
//! **Fabric failure → the explicit `ConnectFailed` reject** — the //! **Fabric failure → the explicit `ConnectFailed` reject**; the
//! wave-2 seam stub's reason surviving as the real decline //! wave-2 seam stub's reason surviving as the real decline
//! ([`crate::dispatch::connect_failed_open_decline`]; the stub itself //! ([`crate::dispatch::connect_failed_open_decline`]; the stub itself
//! is retired). The channel is never established, so the SSH client //! is retired). The channel is never established, so the SSH client
//! observes a clean, channel-scoped open refusal (POC-1 §5 posture) //! observes a clean, channel-scoped open refusal (POC-1 §5 posture)
//! — vanilla clients retry-observe a down forward instead of a hang. //! : vanilla clients retry-observe a down forward instead of a hang.
//! //!
//! ## The `-R` listen flow (`w3-tunnel-listen` — the fabric listens, //! ## The `-R` listen flow (`w3-tunnel-listen`: the fabric listens,
//! the client dials) //! the client dials)
//! //!
//! The server direction of the route-1 bridge (`mapping.md` //! The server direction of the route-1 bridge (`mapping.md`
//! §Channel-type rows, the `forwarded-tcpip` row + §Global-request //! §Channel-type rows, the `forwarded-tcpip` row + §Global-request
//! rows). Per the family door semantics: the door owns nothing local //! rows). Per the family door semantics: the door owns nothing local,
//! — **the fabric listens**; per arrival the data flows back toward //! **the fabric listens**; per arrival the data flows back toward
//! the vanilla SSH client, which dials its own target. Three moving //! the vanilla SSH client, which dials its own target. Three moving
//! parts, all decided against alktunnels 0.4.1's actual surfaces with //! parts, all decided against alktunnels 0.4.1's actual surfaces with
//! POC-3 §C.4 as the template (the gate adoption rides 0.5.0's //! POC-3 §C.4 as the template (the gate adoption rides 0.5.0's
@@ -47,36 +47,36 @@
//! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`): //! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`):
//! the door maps the vanilla listen request family-ward as //! the door maps the vanilla listen request family-ward as
//! `TunnelParams { resource: <the wire bind endpoint string>, //! `TunnelParams { resource: <the wire bind endpoint string>,
//! substrate: Tcp/Unix }` — the **resource-naming convention //! substrate: Tcp/Unix }`; the **resource-naming convention
//! recorded** (the one open surface this flow's composition had): //! recorded** (the one open surface this flow's composition had):
//! ADR-001 keeps the wire field a *resource identifier*, and a //! ADR-001 keeps the wire field a *resource identifier*, and a
//! fabric serving vanilla `-R` opt into dynamic-address binding by //! fabric serving vanilla `-R` opt into dynamic-address binding by
//! its own establisher/bind-closure treating the mapped string as //! its own establisher/bind-closure treating the mapped string as
//! its bind endpoint (`bindfirst_establisher` + //! its bind endpoint (`bindfirst_establisher` +
//! `register_openable_with_establisher`, the alkcall ADR-049 //! `register_openable_with_establisher`, the alkcall ADR-049
//! establisher shape — who may bind what is the fabric's ACL + //! establisher shape: who may bind what is the fabric's ACL +
//! its bind closure, assembly policy at the family's provider, //! its bind closure, assembly policy at the family's provider,
//! never the door's). The dial is the published consumer surface — //! never the door's). The dial is the published consumer surface,
//! `TunnelListener::open(client, params)` on the held //! `TunnelListener::open(client, params)` on the held
//! `ChannelClient` — bounded by [`LISTEN_DIAL_TIMEOUT`]; the //! `ChannelClient`, bounded by [`LISTEN_DIAL_TIMEOUT`]; the
//! reply's `bound` + minted `listen` token (the alkcall ADR-049 //! reply's `bound` + minted `listen` token (the alkcall ADR-049
//! amendment-3 establisher fields) are read through //! amendment-3 establisher fields) are read through
//! `parse-listen-reply`-class accessors; **a reply without both is //! `parse-listen-reply`-class accessors; **a reply without both is
//! a declined listen with the fabric listener torn down //! a declined listen with the fabric listener torn down
//! immediately** (never a minted-but-uncorrelatable listen — a //! immediately** (never a minted-but-uncorrelatable listen; a
//! declined listen never left a half state). //! declined listen never left a half state).
//! 2. **The door-side arrival surface** — the composition decision //! 2. **The door-side arrival surface**, the composition decision
//! (the task's second `w3-seam-config` course correction): the //! (the task's second `w3-seam-config` course correction): the
//! fabric dials `tunnel/forwarded` *toward the door* per alktunnels' //! fabric dials `tunnel/forwarded` *toward the door* per alktunnels'
//! bind-first dispatch (`make_bindfirst_accept_handler` + //! bind-first dispatch (`make_bindfirst_accept_handler` +
//! `ForwardedOpener` — the producer calls the op on the channel-0 //! `ForwardedOpener`): the producer calls the op on the channel-0
//! call plane toward the listen opener, and the door reply carries //! call plane toward the listen opener, and the door reply carries
//! the door-allocated channel id the producer adopts; alkcall's //! the door-allocated channel id the producer adopts; alkcall's
//! odd/even split keeps the two open directions collision-free). //! odd/even split keeps the two open directions collision-free).
//! Serving that op needs the door's channel-0 serving loop — an //! Serving that op needs the door's channel-0 serving loop, an
//! alkcall `ChannelClient` built with //! alkcall `ChannelClient` built with
//! `from_connection_with_serving`, whose registry the assembly owns //! `from_connection_with_serving`, whose registry the assembly owns,
//! — so the serve-config carries [`crate::config::TunnelListenSeam`] //! so the serve-config carries [`crate::config::TunnelListenSeam`]
//! (the same `Arc<OperationRegistry>` the assembly wired); alkssh //! (the same `Arc<OperationRegistry>` the assembly wired); alkssh
//! registers the forwarded openable itself in //! registers the forwarded openable itself in
//! [`install_forwarded_surface`] (called from //! [`install_forwarded_surface`] (called from
@@ -85,7 +85,7 @@
//! client's manager + alktunnels' `tunnel_forwarded_spec()` + //! client's manager + alktunnels' `tunnel_forwarded_spec()` +
//! `make_forwarded_op_handler` over the crate's arrival pump, the //! `make_forwarded_op_handler` over the crate's arrival pump, the
//! wire-level ACL (`tunnel:forwarded` scope) and the per-connection //! wire-level ACL (`tunnel:forwarded` scope) and the per-connection
//! channel budget enforced inside alkcall's dispatch — registered //! channel budget enforced inside alkcall's dispatch, registered
//! in the **establisher variant** (alktunnels 0.5.0's //! in the **establisher variant** (alktunnels 0.5.0's
//! `register_tunnel_forwarded_openable_with_establisher`, the //! `register_tunnel_forwarded_openable_with_establisher`, the
//! review-003 W2 adoption): the router's correlation lookup rides //! review-003 W2 adoption): the router's correlation lookup rides
@@ -94,25 +94,25 @@
//! `unknown_resource`) before any channel is born; the handler's //! `unknown_resource`) before any channel is born; the handler's
//! re-lookup keeps the residual-race + //! re-lookup keeps the residual-race +
//! cap-admission teardown-at-birth fallback. //! cap-admission teardown-at-birth fallback.
//! 3. **The arrival pump** — the `forwarded-tcpip` data leg: the //! 3. **The arrival pump**, the `forwarded-tcpip` data leg: the
//! registered [`ForwardedHandler`]-closure correlates the //! registered [`ForwardedHandler`]-closure correlates the
//! arrival's `listen` token through [`ListenRouter`] (the route was //! arrival's `listen` token through [`ListenRouter`] (the route was
//! registered at listen acceptance with the owning session's //! registered at listen acceptance with the owning session's
//! handle + cap); a miss here is the gate-pass residual race — //! handle + cap); a miss here is the gate-pass residual race,
//! still **teardown-at-birth** (drop the fabric //! still **teardown-at-birth** (drop the fabric
//! half, never a half-open SSH channel). On a hit, per vanilla `-R` //! half, never a half-open SSH channel). On a hit, per vanilla `-R`
//! convention: the door opens a `forwarded-tcpip` channel *toward //! convention: the door opens a `forwarded-tcpip` channel *toward
//! the client* echoing **the listen request's own args** — the //! the client* echoing **the listen request's own args**, the
//! connected address as the wire `tcpip_forward` request carried it //! connected address as the wire `tcpip_forward` request carried it
//! verbatim (the OpenSSH client matches that exact string; for //! verbatim (the OpenSSH client matches that exact string; for
//! `port = 0` mints the *actually bound* port from the fabric's //! `port = 0` mints the *actually bound* port from the fabric's
//! `bound` reply — the sshd parity recorded in //! `bound` reply, the sshd parity recorded in
//! [`ListenRoute`]) — with the `TunnelForwardedParams.peer` arrival //! [`ListenRoute`]), with the `TunnelForwardedParams.peer` arrival
//! mapped to the originator fields (`HostPort` → address/port; //! mapped to the originator fields (`HostPort` → address/port;
//! `Path` → path + port 0 — the degenerate but typed mapping, //! `Path` → path + port 0, the degenerate but typed mapping,
//! unreachable for a TCP-minted listen). The accepted-or-rejected //! unreachable for a TCP-minted listen). The accepted-or-rejected
//! decision is the in-crate cap (the wave-2 gate's booked acceptor //! decision is the in-crate cap (the wave-2 gate's booked acceptor
//! vocabulary — the per-arrival admission; a client-side rejection //! vocabulary, the per-arrival admission; a client-side rejection
//! or a dead session releases the count, the established channel's //! or a dead session releases the count, the established channel's
//! release rides [`crate::handler`]'s `channel_close` like every //! release rides [`crate::handler`]'s `channel_close` like every
//! established channel). Accept → the fabric `BiStream` pumped //! established channel). Accept → the fabric `BiStream` pumped
@@ -125,7 +125,7 @@
//! //!
//! **The inline listen dial (recorded decision):** the //! **The inline listen dial (recorded decision):** the
//! spawn-and-return rule keeps *serving paths* out of the packet //! spawn-and-return rule keeps *serving paths* out of the packet
//! loop — but the `tcpip_forward` global request's reply is bound to //! loop, but the `tcpip_forward` global request's reply is bound to
//! the hook itself (russh answers the want-reply from the hook's //! the hook itself (russh answers the want-reply from the hook's
//! return + the `&mut u32` out-param; no handle surface responds to a //! return + the `&mut u32` out-param; no handle surface responds to a
//! global request from a spawned task at this pin), so the listen //! global request from a spawned task at this pin), so the listen
@@ -136,18 +136,18 @@
//! packet loop), and its stall is bounded: other channels of the //! packet loop), and its stall is bounded: other channels of the
//! connection pause only for [`LISTEN_DIAL_TIMEOUT`], and the //! connection pause only for [`LISTEN_DIAL_TIMEOUT`], and the
//! fabric-dial failure answers a denied listen. All serving below the //! fabric-dial failure answers a denied listen. All serving below the
//! reply (none — the listen returns) and all arrival paths stay //! reply (none, since the listen returns) and all arrival paths stay
//! spawned. //! spawned.
//! //!
//! ## Bounded bookkeeping (AGENTS.md convention 9) //! ## Bounded bookkeeping (AGENTS.md convention 9)
//! //!
//! - **The per-session listener map** (the session-handler's //! - **The per-session listener map** (the session-handler's
//! `listeners` — packet-loop-only writer, like the `channels`/ //! `listeners`, packet-loop-only writer, like the `channels`/
//! `ptys` stores) maps the minted token → the pending listen (wire //! `ptys` stores) maps the minted token → the pending listen (wire
//! args, bound port, the [`alktunnels::consumer::TunnelListener`] //! args, bound port, the [`alktunnels::consumer::TunnelListener`]
//! whose `close`/`Drop` perform the fabric teardown). Its bound: //! whose `close`/`Drop` perform the fabric teardown). Its bound:
//! the listen count shares the `Limits::max_concurrent_channels` //! the listen count shares the `Limits::max_concurrent_channels`
//! *ceiling* in a dedicated counter — a listen is at least one //! *ceiling* in a dedicated counter; a listen is at least one
//! future channel's potential, sessions minting unbounded listeners //! future channel's potential, sessions minting unbounded listeners
//! would mint an unbounded fabric-channel map on the shared leg. //! would mint an unbounded fabric-channel map on the shared leg.
//! - **The route map** ([`ListenRouter`], shared with the registered //! - **The route map** ([`ListenRouter`], shared with the registered
@@ -155,29 +155,29 @@
//! bounded by construction: entries are minted exactly at accepted //! bounded by construction: entries are minted exactly at accepted
//! listens (listener-cap gated), removed 1:1 at the cancel forms //! listens (listener-cap gated), removed 1:1 at the cancel forms
//! and at the owning session's teardown (the handler's `Drop` //! and at the owning session's teardown (the handler's `Drop`
//! removes its tokens — a stale route to a dead handle only ever //! removes its tokens; a stale route to a dead handle only ever
//! costs one failed open that returns teardown-at-birth, never a //! costs one failed open that returns teardown-at-birth, never a
//! half-open SSH channel). //! half-open SSH channel).
//! - **A cancelled / torn-down listen never half-serves**: the cancel //! - **A cancelled / torn-down listen never half-serves**: the cancel
//! path removes the route before any teardown (a racing arrival //! path removes the route before any teardown (a racing arrival
//! misses → teardown-at-birth), and `cancel_*` of an unknown //! misses → teardown-at-birth), and `cancel_*` of an unknown
//! (address, bound-port) pair answers `false` — the vanilla //! (address, bound-port) pair answers `false`, the vanilla
//! cancel contract (sshd parity: match by the raw wire string + //! cancel contract (sshd parity: match by the raw wire string +
//! the bound port). One recorded caveat: an arrival that *cloned* //! the bound port). One recorded caveat: an arrival that *cloned*
//! its route just before the cancel removes it still completes its //! its route just before the cancel removes it still completes its
//! open + pump (the forwarded channel is independent of the //! open + pump (the forwarded channel is independent of the
//! listener's teardown — the arrival is never half-open, but a //! listener's teardown; the arrival is never half-open, but a
//! cancelled listen can deliver one already-accepted connection; //! cancelled listen can deliver one already-accepted connection;
//! sshd parity). //! sshd parity).
//! //!
//! ## The runtime-Unix gate (OQ-11) //! ## The runtime-Unix gate (OQ-11)
//! //!
//! `direct-streamlocal` compiles wherever `tunnel` compiles (universal //! `direct-streamlocal` compiles wherever `tunnel` compiles (universal
//! compile — no per-platform feature fork); **serving is runtime-Unix- //! compile, no per-platform feature fork); **serving is runtime-Unix-
//! gated**: off-Unix a clean open-decline **before any fabric call** //! gated**: off-Unix a clean open-decline **before any fabric call**
//! (never a partial bridge), on-Unix the real bridge. The gate lives //! (never a partial bridge), on-Unix the real bridge. The gate lives
//! in the dispatch hook ([`crate::handler`]'s `direct-streamlocal` //! in the dispatch hook ([`crate::handler`]'s `direct-streamlocal`
//! row), before the cap admission — a platform-refused open consumes //! row), before the cap admission; a platform-refused open consumes
//! nothing. The gate rides the `streamlocal_forward` listen row the //! nothing. The gate rides the `streamlocal_forward` listen row the
//! same way (`w3-tunnel-listen`). //! same way (`w3-tunnel-listen`).
//! //!
@@ -185,25 +185,25 @@
//! //!
//! The resolved SSH identity is the auth bridge's ride-along in //! The resolved SSH identity is the auth bridge's ride-along in
//! handler state (`auth_publickey` stores; the row dispatches never //! handler state (`auth_publickey` stores; the row dispatches never
//! serve anonymously — the `no-identity` guard, unreachable on the //! serve anonymously, the `no-identity` guard, unreachable on the
//! wire because russh authorizes before channels exist, fail-closed //! wire because russh authorizes before channels exist, fail-closed
//! like the git/tty rows' `w2-auth-bridge` parked item discharged //! like the git/tty rows' `w2-auth-bridge` parked item discharged
//! there). The guard gates every tunnel open **at the door**; the //! there). The guard gates every tunnel open **at the door**; the
//! open that proceeds then rides the channels connection into the //! open that proceeds then rides the channels connection into the
//! fabric, whose own `AccessControl::check(peer_identity)` gates the //! fabric, whose own `AccessControl::check(peer_identity)` gates the
//! mapped `channels/tunnel/direct` open (alkcall CF-005/CF-006) — //! mapped `channels/tunnel/direct` open (alkcall CF-005/CF-006);
//! the composed-gate witness is the harness's identity rows. The //! the composed-gate witness is the harness's identity rows. The
//! `-R` leg composes the same gates on both of its directions: the //! `-R` leg composes the same gates on both of its directions: the
//! listen dial rides the fabric leg's identity (`tunnel:open` scope — //! listen dial rides the fabric leg's identity (`tunnel:open` scope,
//! the assembly's dial-identity composition, unchanged), and the //! the assembly's dial-identity composition, unchanged), and the
//! arrivals are gated by the door-side registry's ACL //! arrivals are gated by the door-side registry's ACL
//! (`tunnel:forwarded` scope, per arrival) exactly when the assembly's //! (`tunnel:forwarded` scope, per arrival) exactly when the assembly's
//! fabric leg carries it. //! fabric leg carries it.
//! //!
//! **`set_identity` mechanics (recorded — `w3-tunnel-direct`'s decided //! **`set_identity` mechanics (recorded: `w3-tunnel-direct`'s decided
//! point, carried unchanged):** the alkhub `hub_leg.rs` one-time-set //! point, carried unchanged):** the alkhub `hub_leg.rs` one-time-set
//! precedent (`channel0_conn.set_identity(identity)`) is a //! precedent (`channel0_conn.set_identity(identity)`) is a
//! **serving-side** set — the identity the fabric leg dials with, and //! **serving-side** set: the identity the fabric leg dials with, and
//! against which the fabric's ACL gates alkssh's mapped opens, is the //! against which the fabric's ACL gates alkssh's mapped opens, is the
//! assembly-supplied `ChannelClient`'s own dial identity (CF-005: set //! assembly-supplied `ChannelClient`'s own dial identity (CF-005: set
//! once at dial, propagated to channel 0 at `from_connection`); the //! once at dial, propagated to channel 0 at `from_connection`); the
@@ -246,10 +246,10 @@ use crate::{config, dispatch, observe};
pub(crate) type WireChannel = Channel<russh::server::Msg>; pub(crate) type WireChannel = Channel<russh::server::Msg>;
/// The spawned route-1 bridge (`w3-tunnel-direct`, the POC-3 §C /// The spawned route-1 bridge (`w3-tunnel-direct`, the POC-3 §C
/// recipe): the fabric open runs here — accept only after it succeeds, /// recipe): the fabric open runs here; accept only after it succeeds,
/// `take_halves()` pumped against the taken channel on completion, and /// `take_halves()` pumped against the taken channel on completion, and
/// on failure the explicit `ConnectFailed` reject **with the cap count /// on failure the explicit `ConnectFailed` reject **with the cap count
/// released** (the recorded release mechanism — the admitted open this /// released** (the recorded release mechanism: the admitted open this
/// task holds was never established, so no `channel_close` can ever /// task holds was never established, so no `channel_close` can ever
/// fire; the count recovers here, in the task that knows the failure, /// fire; the count recovers here, in the task that knows the failure,
/// exactly once: a success never releases). The channel closes on /// exactly once: a success never releases). The channel closes on
@@ -283,7 +283,7 @@ pub(crate) async fn serve_direct_row(
} }
/// The open hook's spawn step (ADR-006 decision 4): the handler clones /// The open hook's spawn step (ADR-006 decision 4): the handler clones
/// the cap `Arc` (the failure-path release's handle — the count was /// the cap `Arc` (the failure-path release's handle; the count was
/// admitted at the hook and is held across the fabric open) and hands /// admitted at the hook and is held across the fabric open) and hands
/// the rest over; the hook returns fast with the fabric open in /// the rest over; the hook returns fast with the fabric open in
/// flight. /// flight.
@@ -307,29 +307,29 @@ pub(crate) fn spawn_direct_row(
/// The wall-clock bound on the family-ward listen dial (`TunnelListener:: /// The wall-clock bound on the family-ward listen dial (`TunnelListener::
/// open`): the fabric's establisher is already bound by alkcall's own /// open`): the fabric's establisher is already bound by alkcall's own
/// establisher deadline (10 s, alkcall ADR-049 §2), so this matches it — /// establisher deadline (10 s, alkcall ADR-049 §2), so this matches it,
/// a fabric that never answers a listen dial cannot hold the packet /// a fabric that never answers a listen dial cannot hold the packet
/// loop longer than a bounded dial. /// loop longer than a bounded dial.
pub(crate) const LISTEN_DIAL_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const LISTEN_DIAL_TIMEOUT: Duration = Duration::from_secs(10);
/// One route in the listen-arrival bookkeeping — registered at listen /// One route in the listen-arrival bookkeeping, registered at listen
/// acceptance, consulted per arrival. The echo fields are the vanilla /// acceptance, consulted per arrival. The echo fields are the vanilla
/// `-R` convention decoded (sshd parity, recorded on [`ListenRouter`]): /// `-R` convention decoded (sshd parity, recorded on [`ListenRouter`]):
/// the connected address rides the wire `tcpip_forward` string /// the connected address rides the wire `tcpip_forward` string
/// **verbatim** (the OpenSSH client sent that exact string and matches /// **verbatim** (the OpenSSH client sent that exact string and matches
/// the open's address field against it exactly), the port is the /// the open's address field against it exactly), the port is the
/// **actually bound** one (the fabric's `bound` reply — the sshd /// **actually bound** one (the fabric's `bound` reply, the sshd
/// `get_local_port`-of-accepted-socket parity, which is what makes /// `get_local_port`-of-accepted-socket parity, which is what makes
/// `port = 0` mints round-trip), and the unix path rides the /// `port = 0` mints round-trip), and the unix path rides the
/// `streamlocal_forward` request verbatim. /// `streamlocal_forward` request verbatim.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct ListenRoute { pub(crate) struct ListenRoute {
/// The SSH args the arrivals' opens echo — keyed by the route's /// The SSH args the arrivals' opens echo, keyed by the route's
/// substrate (`Tcp`/`Udp` → the vanilla `-R` trio's first two /// substrate (`Tcp`/`Udp` → the vanilla `-R` trio's first two
/// fields; `Unix` → the `forwarded-streamlocal` path). /// fields; `Unix` → the `forwarded-streamlocal` path).
pub(crate) echo: RouteEcho, pub(crate) echo: RouteEcho,
pub(crate) substrate: Substrate, pub(crate) substrate: Substrate,
/// The owning session's packet-loop handle — the arrival's SSH /// The owning session's packet-loop handle: the arrival's SSH
/// channel opens through it; a session that died fails the open /// channel opens through it; a session that died fails the open
/// (the arrival's teardown-at-birth path). /// (the arrival's teardown-at-birth path).
pub(crate) handle: Arc<Handle>, pub(crate) handle: Arc<Handle>,
@@ -355,10 +355,10 @@ pub(crate) enum RouteEcho {
/// `Drop`). A mint on an already-held token is refused fail-closed /// `Drop`). A mint on an already-held token is refused fail-closed
/// ([`ListenRouter::register`], `w4-router-collision`). Arrivals /// ([`ListenRouter::register`], `w4-router-collision`). Arrivals
/// that find no route are the correlation-miss /// that find no route are the correlation-miss
/// class — teardown-at-birth, never a half-open SSH channel. /// class (teardown-at-birth, never a half-open SSH channel).
/// ///
/// The map is behind a `parking_lot::Mutex` because arrivals arrive on /// The map is behind a `parking_lot::Mutex` because arrivals arrive on
/// alkcall serving tasks while the packet loop mints/removes — the /// alkcall serving tasks while the packet loop mints/removes; the
/// held region is a HashMap op, never an await. /// held region is a HashMap op, never an await.
pub(crate) struct ListenRouter { pub(crate) struct ListenRouter {
routes: Mutex<HashMap<u32, ListenRoute>>, routes: Mutex<HashMap<u32, ListenRoute>>,
@@ -367,21 +367,21 @@ pub(crate) struct ListenRouter {
impl ListenRouter { impl ListenRouter {
/// Minted-entry registration (the listen-acceptance step), /// Minted-entry registration (the listen-acceptance step),
/// fail-closed on token collision (the wave-3 gate's booked item /// fail-closed on token collision (the wave-3 gate's booked item
/// 4, `w4-router-collision` — the same posture the same gate /// 4, `w4-router-collision`, the same posture the same gate
/// landed inline for a duplicate `tunnel/forwarded` /// landed inline for a duplicate `tunnel/forwarded`
/// registration): safety rests on the fabric's ADR-008 mint /// registration): safety rests on the fabric's ADR-008 mint
/// discipline upstream, and this defends in depth against a /// discipline upstream, and this defends in depth against a
/// collided mint — a `register` on an already-held token returns /// collided mint, a `register` on an already-held token returns
/// `Err` and **never replaces** the held route, so a colliding /// `Err` and **never replaces** the held route, so a colliding
/// mint can never steal a live adapter's arrival routes. The /// mint can never steal a live adapter's arrival routes. The
/// caller declines the listen (the fabric listener is then torn /// caller declines the listen (the fabric listener is then torn
/// down — a refused registration never left a half state). /// down; a refused registration never left a half state).
/// ///
/// Error surface (decided, `w4-router-collision`): the refusal /// Error surface (decided, `w4-router-collision`): the refusal
/// rides [`String`]-carried detail surfaced as the request-time /// rides [`String`]-carried detail surfaced as the request-time
/// **decline disposition** (`RequestDenied`, reason /// **decline disposition** (`RequestDenied`, reason
/// `token-collision`), not a distinct [`AdapterError`](crate::AdapterError) /// `token-collision`), not a distinct [`AdapterError`](crate::AdapterError)
/// variant — a global-request hook's `Err` would kill the whole /// variant, since a global-request hook's `Err` would kill the whole
/// russh session (russh 0.65 answers the decline from the hook's /// russh session (russh 0.65 answers the decline from the hook's
/// `Ok(false)`, and `Err` propagates out of the packet loop), and /// `Ok(false)`, and `Err` propagates out of the packet loop), and
/// the taxonomy's rule holds that wire decline reasons are never /// the taxonomy's rule holds that wire decline reasons are never
@@ -393,7 +393,7 @@ impl ListenRouter {
let mut routes = self.routes.lock(); let mut routes = self.routes.lock();
if routes.contains_key(&token) { if routes.contains_key(&token) {
return Err(format!( return Err(format!(
"listen token {token} is already routed — a collided \ "listen token {token} is already routed; a collided \
mint may not replace a live adapter's route" mint may not replace a live adapter's route"
)); ));
} }
@@ -401,7 +401,7 @@ impl ListenRouter {
Ok(()) Ok(())
} }
/// The per-arrival lookup — cloned out, never taken: one route /// The per-arrival lookup, cloned out, never taken: one route
/// serves unboundedly many arrivals; only the cancel forms and /// serves unboundedly many arrivals; only the cancel forms and
/// session teardown remove. /// session teardown remove.
pub(crate) fn route_for(&self, token: u32) -> Option<ListenRoute> { pub(crate) fn route_for(&self, token: u32) -> Option<ListenRoute> {
@@ -409,7 +409,7 @@ impl ListenRouter {
} }
/// The cancel-form removal (1:1 into the family listener's /// The cancel-form removal (1:1 into the family listener's
/// teardown): removes first, so a racing arrival already misses — /// teardown): removes first, so a racing arrival already misses,
/// teardown-at-birth, never a post-cancel half-serve. /// teardown-at-birth, never a post-cancel half-serve.
pub(crate) fn cancel(&self, token: &u32) -> bool { pub(crate) fn cancel(&self, token: &u32) -> bool {
self.routes.lock().remove(token).is_some() self.routes.lock().remove(token).is_some()
@@ -417,7 +417,7 @@ impl ListenRouter {
/// The owning session's teardown (the handler's `Drop`): remove /// The owning session's teardown (the handler's `Drop`): remove
/// the session's tokens; any listener objects left drop with the /// the session's tokens; any listener objects left drop with the
/// handler (the fabric-side reap — see `PendingListen`). /// handler (the fabric-side reap, see `PendingListen`).
pub(crate) fn remove_tokens(&self, tokens: &[u32]) { pub(crate) fn remove_tokens(&self, tokens: &[u32]) {
let mut routes = self.routes.lock(); let mut routes = self.routes.lock();
for token in tokens { for token in tokens {
@@ -438,24 +438,24 @@ impl Default for ListenRouter {
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both /// [`crate::SshAdapter::new`] when the fabric leg + seam are both
/// present): builds the router, registers alktunnels' forwarded /// present): builds the router, registers alktunnels' forwarded
/// openable into the seam's shared registry over a `ChannelCore` /// openable into the seam's shared registry over a `ChannelCore`
/// wrapping a clone of the held `ChannelClient`'s manager — the /// wrapping a clone of the held `ChannelClient`'s manager, the
/// alkcall shape whose wrapper performs the door-side arrival's /// alkcall shape whose wrapper performs the door-side arrival's
/// channel allocation, ACL (`tunnel:forwarded`, per call) and budget /// channel allocation, ACL (`tunnel:forwarded`, per call) and budget
/// (alkcall's secure default per-identity policy, `256`, plus the /// (alkcall's secure default per-identity policy, `256`, plus the
/// `DEFAULT_MAX_CHANNELS` per-connection bound — alkssh composes no /// `DEFAULT_MAX_CHANNELS` per-connection bound; alkssh composes no
/// deployment policy; an assembly wanting a different per-identity /// deployment policy; an assembly wanting a different per-identity
/// cap shapes it through its own `ServingConfig`/`registry` /// cap shapes it through its own `ServingConfig`/`registry`
/// composition upstream) — and spawns alktunnels' forward-op handler /// composition upstream), and spawns alktunnels' forward-op handler
/// over the crate's arrival pump. /// over the crate's arrival pump.
/// ///
/// The registration shape is the **establisher variant** /// The registration shape is the **establisher variant**
/// (`register_tunnel_forwarded_openable_with_establisher` — alktunnels /// (`register_tunnel_forwarded_openable_with_establisher`, alktunnels
/// 0.5.0, review 003 W2's adoption, ADR-008 Amendment 2's recommended /// 0.5.0, review 003 W2's adoption, ADR-008 Amendment 2's recommended
/// form): the router's correlation lookup rides a [`ForwardedGate`] /// form): the router's correlation lookup rides a [`ForwardedGate`]
/// capturing the router, so a stale/missing `listen` token fails the /// capturing the router, so a stale/missing `listen` token fails the
/// open at alkcall's call plane as a typed `channel:open_failed` /// open at alkcall's call plane as a typed `channel:open_failed`
/// (`details.reason == "unknown_resource"`) **before any channel is /// (`details.reason == "unknown_resource"`) **before any channel is
/// born** — the phantom-channel class (call-plane success, then /// born**; the phantom-channel class (call-plane success, then
/// handler-side decline) is closed at the open; door-side arrival /// handler-side decline) is closed at the open; door-side arrival
/// correlations that pass the gate but lose the narrow gate-pass→ /// correlations that pass the gate but lose the narrow gate-pass→
/// handler-run race, plus every cap-admission decline, still take the /// handler-run race, plus every cap-admission decline, still take the
@@ -466,14 +466,14 @@ impl Default for ListenRouter {
/// ///
/// The registration is /// The registration is
/// one-per-op-id-per-registry: alkcall's `register` silently /// one-per-op-id-per-registry: alkcall's `register` silently
/// replaces an existing entry, so the door **pre-checks the op** — /// replaces an existing entry, so the door **pre-checks the op**:
/// a `tunnel/forwarded` registration that already exists (e.g. a /// a `tunnel/forwarded` registration that already exists (e.g. a
/// second adapter over the same shared registry) is a failed /// second adapter over the same shared registry) is a failed
/// construction (fail-closed — never a route to a handler the door /// construction (fail-closed; never a route to a handler the door
/// does not own). /// does not own).
/// ///
/// On success the router rides back into the serve-config (the /// On success the router rides back into the serve-config (the
/// session handlers read it) — `Ok` with the router installed, `Err` /// session handlers read it): `Ok` with the router installed, `Err`
/// is the caller's constructor failure. /// is the caller's constructor failure.
pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Result<(), String> { pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Result<(), String> {
let Some(seam) = serve.tunnel_listen_seam.as_mut() else { let Some(seam) = serve.tunnel_listen_seam.as_mut() else {
@@ -490,8 +490,8 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
Ok(()) Ok(())
} else { } else {
Err(TunnelEstablishError::UnknownResource(format!( Err(TunnelEstablishError::UnknownResource(format!(
"no accepted listen routes the arrival's token {} \ "no accepted listen routes the arrival's token {}, \
— the forwarding session is gone or was never minted \ the forwarding session is gone or was never minted \
(alkssh: stale/missing `-R` token)", (alkssh: stale/missing `-R` token)",
params.listen params.listen
))) )))
@@ -515,7 +515,7 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
{ {
return Err(format!( return Err(format!(
"the tunnel/forwarded op ({}) is already registered in the \ "the tunnel/forwarded op ({}) is already registered in the \
seam's registry — a conflicting registration would silently \ seam's registry; a conflicting registration would silently \
replace the door's arrival route", replace the door's arrival route",
alktunnels::params::OP_TUNNEL_FORWARDED alktunnels::params::OP_TUNNEL_FORWARDED
)); ));
@@ -532,34 +532,34 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
Ok(()) Ok(())
} }
/// The registered arrival pump — the `forwarded-tcpip` data leg /// The registered arrival pump, the `forwarded-tcpip` data leg
/// (mapping.md's row, "hand the channel duplex to the handler"): the /// (mapping.md's row, "hand the channel duplex to the handler"): the
/// alkcall openable registration hands every fabric arrival here with /// alkcall openable registration hands every fabric arrival here with
/// the parsed [`TunnelForwardedParams`] and the arrival's `BiStream`; /// the parsed [`TunnelForwardedParams`] and the arrival's `BiStream`;
/// the flow is the recorded shape: /// the flow is the recorded shape:
/// ///
/// 1. **Correlation** — the route for the arrival's minted token; /// 1. **Correlation**: the route for the arrival's minted token;
/// a miss is teardown-at-birth (drop the fabric half; the alkcall /// a miss is teardown-at-birth (drop the fabric half; the alkcall
/// wrapper's awaited join ends, the channel reaps). The gate /// wrapper's awaited join ends, the channel reaps). The gate
/// (the establisher's call-plane correlation) misses a stale /// (the establisher's call-plane correlation) misses a stale
/// token before any channel is born; this handler-side re-lookup /// token before any channel is born; this handler-side re-lookup
/// is the narrow gate-pass→handler-run **residual race** — the /// is the narrow gate-pass→handler-run **residual race**, the
/// same miss class, past the open — and the never-established cap /// same miss class, past the open, and the never-established cap
/// declines below take the same path. /// declines below take the same path.
/// 2. **The in-crate cap admission** (bounded resources at the accept /// 2. **The in-crate cap admission** (bounded resources at the accept
/// surface — the wave-2 gate's booked acceptor vocabulary): no /// surface, the wave-2 gate's booked acceptor vocabulary): no
/// room → the fabric half drops, the arrival never opens an SSH /// room → the fabric half drops, the arrival never opens an SSH
/// channel. The count releases here on every never-established /// channel. The count releases here on every never-established
/// path; an established one releases at its close (the packet /// path; an established one releases at its close (the packet
/// loop's release site), the direct rows' disjoint-release shape. /// loop's release site), the direct rows' disjoint-release shape.
/// 3. **The SSH open toward the client** — the echoed listen args /// 3. **The SSH open toward the client**, the echoed listen args
/// (`forwarded-tcpip` for the tcp/udp substrates with the originator /// (`forwarded-tcpip` for the tcp/udp substrates with the originator
/// fields mapped from `TunnelForwardedParams.peer`; `forwarded- /// fields mapped from `TunnelForwardedParams.peer`; `forwarded-
/// streamlocal@openssh.com` with the echoed path for `Unix`) — per /// streamlocal@openssh.com` with the echoed path for `Unix`); per
/// the spawn-and-return constraint this runs on this spawned task; /// the spawn-and-return constraint this runs on this spawned task;
/// a rejected/dead-open releases the count and drops the fabric /// a rejected/dead-open releases the count and drops the fabric
/// half (never a half-open SSH channel). /// half (never a half-open SSH channel).
/// 4. **The pump** — the fabric `BiStream` ↔ the SSH channel duplex /// 4. **The pump**: the fabric `BiStream` ↔ the SSH channel duplex
/// (the route-1 bridge's primitive, reversed). /// (the route-1 bridge's primitive, reversed).
async fn pump_forwarded_arrival( async fn pump_forwarded_arrival(
router: Arc<ListenRouter>, router: Arc<ListenRouter>,
@@ -609,7 +609,7 @@ async fn pump_forwarded_arrival(
} }
(RouteEcho::Tcp { address, port }, SubstrateAddr::Path { path }) => { (RouteEcho::Tcp { address, port }, SubstrateAddr::Path { path }) => {
// The degenerate-but-typed mapping (a path-shaped peer on // The degenerate-but-typed mapping (a path-shaped peer on
// a tcp listen — unreachable from a well-behaved producer): // a tcp listen, unreachable from a well-behaved producer):
// the path rides the originator string, port 0. // the path rides the originator string, port 0.
route route
.handle .handle
@@ -638,12 +638,12 @@ async fn pump_forwarded_arrival(
} }
} }
/// A minted listen — the door-side outcome of a `tcpip_forward` / /// A minted listen, the door-side outcome of a `tcpip_forward` /
/// `streamlocal_forward` request's family-ward dial (the published /// `streamlocal_forward` request's family-ward dial (the published
/// `TunnelListener::open`, bounded by [`LISTEN_DIAL_TIMEOUT`], with /// `TunnelListener::open`, bounded by [`LISTEN_DIAL_TIMEOUT`], with
/// the reply's `bound` + `listen` establisher fields required: a /// the reply's `bound` + `listen` establisher fields required: a
/// producer that minted no correlation token or no bound report /// producer that minted no correlation token or no bound report
/// cannot serve — [`Result`]'s `Err` declines the SSH request before /// cannot serve; [`Result`]'s `Err` declines the SSH request before
/// anything registers). /// anything registers).
pub(crate) struct MintedListen { pub(crate) struct MintedListen {
pub(crate) listener: TunnelListener, pub(crate) listener: TunnelListener,
@@ -653,9 +653,9 @@ pub(crate) struct MintedListen {
/// The listen dial (the mapping.md §Global-request rows' family-ward /// The listen dial (the mapping.md §Global-request rows' family-ward
/// step): `TunnelParams { resource: <endpoint string>, substrate }` /// step): `TunnelParams { resource: <endpoint string>, substrate }`
/// — the wire-named bind endpoint as the resource, the recorded /// : the wire-named bind endpoint as the resource, the recorded
/// door-side naming convention whose fabric-side interpretation is /// door-side naming convention whose fabric-side interpretation is
/// the assembly's establisher — then the reply projection. Called /// the assembly's establisher; then the reply projection. Called
/// inline from the handler hook (see the recorded inline-decision in /// inline from the handler hook (see the recorded inline-decision in
/// the module doc). /// the module doc).
pub(crate) async fn open_listen( pub(crate) async fn open_listen(
@@ -690,7 +690,7 @@ pub(crate) async fn open_listen(
/// The bound port the listen request's out-param carries /// The bound port the listen request's out-param carries
/// (the `&mut u32` sshd parity): the fabric's bound report's port. /// (the `&mut u32` sshd parity): the fabric's bound report's port.
/// Unix listens have no port semantics — the tcp variants only. /// Unix listens have no port semantics; the tcp variants only.
pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> { pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
match bound { match bound {
SubstrateAddr::HostPort { port, .. } => Some(u32::from(*port)), SubstrateAddr::HostPort { port, .. } => Some(u32::from(*port)),
@@ -698,12 +698,12 @@ pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
} }
} }
/// The family-ward resource string for a listen request — the wire /// The family-ward resource string for a listen request: the wire
/// bind endpoint verbatim (`"localhost"` / `"0.0.0.0"` / explicit /// bind endpoint verbatim (`"localhost"` / `"0.0.0.0"` / explicit
/// host: the OpenSSH client has already normalized the no-address case /// host: the OpenSSH client has already normalized the no-address case
/// to `"localhost"` and `""` before this surface exists — sshd /// to `"localhost"` and `""` before this surface exists; sshd
/// `channel_rfwd_bind_host` parity), with the requested port for the /// `channel_rfwd_bind_host` parity), with the requested port for the
/// tcp substrates (`host:port`; port `0` stays `0` — the OS-picking is /// tcp substrates (`host:port`; port `0` stays `0`, since the OS-picking is
/// the fabric's bind step, whose bound report feeds the out-param). /// the fabric's bind step, whose bound report feeds the out-param).
pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option<u32>) -> String { pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option<u32>) -> String {
match substrate { match substrate {
@@ -715,9 +715,9 @@ pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option
/// The per-session pending listen (the packet-loop-only store, like /// The per-session pending listen (the packet-loop-only store, like
/// the `channels`/`ptys` stores): the minted token is the map's key; /// the `channels`/`ptys` stores): the minted token is the map's key;
/// the entry carries what the cancel forms match on (the wire endpoint /// the entry carries what the cancel forms match on (the wire endpoint
/// verbatim + the bound port — the sshd `c->path == host && /// verbatim + the bound port, the sshd `c->path == host &&
/// c->listening_port == port` parity) over the substrate (a unix /// c->listening_port == port` parity) over the substrate (a unix
/// entry's port is 0-carrying and never matched — the unix cancel /// entry's port is 0-carrying and never matched; the unix cancel
/// forms match on the path alone) and the session-owned /// forms match on the path alone) and the session-owned
/// [`TunnelListener`] whose close/Drop performs the family teardown. /// [`TunnelListener`] whose close/Drop performs the family teardown.
pub(crate) struct PendingListen { pub(crate) struct PendingListen {
@@ -727,16 +727,16 @@ pub(crate) struct PendingListen {
pub(crate) listener: TunnelListener, pub(crate) listener: TunnelListener,
} }
/// The listen count's dedicated bound — the same mechanism as /// The listen count's dedicated bound, the same mechanism as
/// [`ChannelCap`], instantiated per session next to it, against the /// [`ChannelCap`], instantiated per session next to it, against the
/// same `Limits::max_concurrent_channels` ceiling: listeners and /// same `Limits::max_concurrent_channels` ceiling: listeners and
/// channels stay independently bounded while their mix stays /// channels stay independently bounded while their mix stays
/// unconstrained (AGENTS.md convention 9 — no unbounded listener /// unconstrained (AGENTS.md convention 9: no unbounded listener
/// map). Admits are packet-loop-only (the listen hooks are the only /// map). Admits are packet-loop-only (the listen hooks are the only
/// mint site); releases fire at the cancel forms, the listen-dial /// mint site); releases fire at the cancel forms, the listen-dial
/// failures (admitted-but-never-minted — the route 1 bridge's /// failures (admitted-but-never-minted, the route 1 bridge's
/// release-site shape), and nowhere else (a minted listener's release /// release-site shape), and nowhere else (a minted listener's release
/// is... never — listeners hold until cancel or session teardown, so /// is... never: listeners hold until cancel or session teardown, so
/// the session's count dies with the handler, per the channels /// the session's count dies with the handler, per the channels
/// cap's same posture). /// cap's same posture).
pub(crate) type ListenerCap = ChannelCap; pub(crate) type ListenerCap = ChannelCap;
@@ -750,7 +750,7 @@ mod tests {
/// The arrival pump's handler-side miss (the narrow gate-pass→ /// The arrival pump's handler-side miss (the narrow gate-pass→
/// handler-run residual race the establisher shape retains) still /// handler-run residual race the establisher shape retains) still
/// teardown-at-births: a token with no route at handler time drops /// teardown-at-births: a token with no route at handler time drops
/// the fabric half — the adoptee reads EOF, never a half-served /// the fabric half; the adoptee reads EOF, never a half-served
/// arrival. /// arrival.
#[tokio::test] #[tokio::test]
async fn the_pump_residual_miss_teardown_at_births_the_fabric_half() { async fn the_pump_residual_miss_teardown_at_births_the_fabric_half() {
+23 -23
View File
@@ -1,20 +1,20 @@
//! The wave-2 auth-bridge contract tests (`w2-auth-bridge`) — the //! The wave-2 auth-bridge contract tests (`w2-auth-bridge`): the
//! ADR-004 dispositions at the handler level and over the wire. //! ADR-004 dispositions at the handler level and over the wire.
//! //!
//! Two vehicles, each for a structural reason: //! Two vehicles, each for a structural reason:
//! //!
//! - **Wire rows** (the adapter's tokio-duplex mount + russh's own //! - **Wire rows** (the adapter's tokio-duplex mount + russh's own
//! client, the `w2-server-mount` wire-smoke precedent) drive the full //! client, the `w2-server-mount` wire-smoke precedent) drive the full
//! `auth_publickey` hook end to end — the hook's //! `auth_publickey` hook end to end (the hook's
//! `VerifiedPublicKey`/`PublicKeyOffer` argument shapes are //! `VerifiedPublicKey`/`PublicKeyOffer` argument shapes are
//! `#[non_exhaustive]` (only russh constructs them, post-verification), //! `#[non_exhaustive]` (only russh constructs them, post-verification),
//! so wire traffic is the only way to observe the full hook — exactly //! so wire traffic is the only way to observe the full hook, exactly
//! where POC-1 observed it. These rows are the thin in-process //! where POC-1 observed it. These rows are the thin in-process
//! precursor of the dedicated `w2-loopback-harness` vehicle (which //! precursor of the dedicated `w2-loopback-harness` vehicle (which
//! records the transport choice and the helpers' vocabulary). //! records the transport choice and the helpers' vocabulary).
//! - **Direct-hook rows** (the wave-1 doubles against //! - **Direct-hook rows** (the wave-1 doubles against
//! [`alkssh::SessionHandler`] constructed directly) drive every hook //! [`alkssh::SessionHandler`] constructed directly) drive every hook
//! whose arguments are constructible outside russh — the four //! whose arguments are constructible outside russh: the four
//! non-publickey declines, and the bridge core //! non-publickey declines, and the bridge core
//! ([`alkssh::SessionHandler::resolve_bridge`], structured out of the //! ([`alkssh::SessionHandler::resolve_bridge`], structured out of the
//! `#[non_exhaustive]`-arg hook on purpose) with fixture keys + //! `#[non_exhaustive]`-arg hook on purpose) with fixture keys +
@@ -39,7 +39,7 @@ use russh::server::{Auth, Handler};
use alkssh::{AdapterError, Limits, ServeConfig, SessionHandler, SshAdapter, fingerprint}; use alkssh::{AdapterError, Limits, ServeConfig, SessionHandler, SshAdapter, fingerprint};
use alkssh::{HostKeySource, KeySourceError}; use alkssh::{HostKeySource, KeySourceError};
/// The client-side handler double for the wire rows — accept every /// The client-side handler double for the wire rows; it accepts every
/// host key (the test choice; the crate never embeds this posture). /// host key (the test choice; the crate never embeds this posture).
struct AcceptAllHostKeys; struct AcceptAllHostKeys;
@@ -55,7 +55,7 @@ impl russh::client::Handler for AcceptAllHostKeys {
} }
/// Mounts the real adapter stack over an in-process duplex pair and /// Mounts the real adapter stack over an in-process duplex pair and
/// connects a russh client to it — the wire rows' vehicle. /// connects a russh client to it, the wire rows' vehicle.
async fn mount( async fn mount(
provider: Arc<MemIdentityProvider>, provider: Arc<MemIdentityProvider>,
limits: Limits, limits: Limits,
@@ -109,7 +109,7 @@ fn registered_provider(fingerprints: &[&str]) -> Arc<MemIdentityProvider> {
} }
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint → /// - *Contract stamp:* ADR-004 decision 1 (fingerprint →
/// `resolve_from_fingerprint` → `Some` → `Auth::Accept` — the bridge /// `resolve_from_fingerprint` → `Some` → `Auth::Accept` (the bridge
/// accepts the identity's key with the arbitrary username attached). /// accepts the identity's key with the arbitrary username attached).
#[tokio::test] #[tokio::test]
async fn known_fingerprint_accepts_over_the_wire() { async fn known_fingerprint_accepts_over_the_wire() {
@@ -132,7 +132,7 @@ async fn known_fingerprint_accepts_over_the_wire() {
} }
/// - *Contract stamp:* ADR-004 decision 5 + OQ-03 (the username is not /// - *Contract stamp:* ADR-004 decision 5 + OQ-03 (the username is not
/// identity and carries no semantics — keys resolve independently of /// identity and carries no semantics; keys resolve independently of
/// the username string, POC-1's `ssh anything@host` observation). /// the username string, POC-1's `ssh anything@host` observation).
#[tokio::test] #[tokio::test]
async fn username_is_never_an_identity_input() { async fn username_is_never_an_identity_input() {
@@ -149,15 +149,15 @@ async fn username_is_never_an_identity_input() {
.expect("auth exchange completes over the wire"); .expect("auth exchange completes over the wire");
assert!( assert!(
result.success(), result.success(),
"every username authenticates identically — {username}: {result:?}" "every username authenticates identically: {username}: {result:?}"
); );
unmount(client, server_task).await; unmount(client, server_task).await;
} }
} }
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject — the /// - *Contract stamp:* ADR-004 decision 1 (`None` → reject; the
/// fail-closed path never creates a weakly authorized session) and /// fail-closed path never creates a weakly authorized session) and
/// the advertisement honesty (ADR-004 decision 4 — exactly /// the advertisement honesty (ADR-004 decision 4, exactly
/// `{publickey}` remains advertised on the decline). /// `{publickey}` remains advertised on the decline).
#[tokio::test] #[tokio::test]
async fn unknown_fingerprint_rejects_fail_closed() { async fn unknown_fingerprint_rejects_fail_closed() {
@@ -188,7 +188,7 @@ async fn unknown_fingerprint_rejects_fail_closed() {
} }
/// - *Contract stamp:* ADR-004 decision 1 (resolution happens at auth /// - *Contract stamp:* ADR-004 decision 1 (resolution happens at auth
/// time — a post-resolution revocation rejects the *next* attempt; /// time (a post-resolution revocation rejects the *next* attempt);
/// nothing is cached into a session credential). /// nothing is cached into a session credential).
#[tokio::test] #[tokio::test]
async fn resolved_fingerprint_can_be_revoked_between_attempts() { async fn resolved_fingerprint_can_be_revoked_between_attempts() {
@@ -220,7 +220,7 @@ async fn resolved_fingerprint_can_be_revoked_between_attempts() {
} }
/// - *Contract stamp:* ADR-004 decision 3 (`auth_none` is an explicit /// - *Contract stamp:* ADR-004 decision 3 (`auth_none` is an explicit
/// family-contract decline — non-advertised methods are never /// family-contract decline; non-advertised methods are never
/// *advertised*, but the disposition is every attempted method's own /// *advertised*, but the disposition is every attempted method's own
/// hook, not the advertisement's). /// hook, not the advertisement's).
#[tokio::test] #[tokio::test]
@@ -258,7 +258,7 @@ async fn auth_password_rejects_over_the_wire() {
} }
/// - *Contract stamp:* ADR-004 decision 3 + OQ-04 (the OpenSSH /// - *Contract stamp:* ADR-004 decision 3 + OQ-04 (the OpenSSH
/// certificate decline is the standing disposition — no CA-trust /// certificate decline is the standing disposition; no CA-trust
/// story exists in the family identity model). /// story exists in the family identity model).
#[tokio::test] #[tokio::test]
async fn auth_openssh_certificate_rejects_over_the_wire() { async fn auth_openssh_certificate_rejects_over_the_wire() {
@@ -286,7 +286,7 @@ fn handler_with(provider: Arc<MemIdentityProvider>) -> SessionHandler {
} }
/// - *Contract stamp:* ADR-004 decision 3 (each non-publickey method is /// - *Contract stamp:* ADR-004 decision 3 (each non-publickey method is
/// an *implemented* decline, never the inherited default — the /// an *implemented* decline, never the inherited default; the
/// dispositions are family contract, and each one is its own hook). /// dispositions are family contract, and each one is its own hook).
#[tokio::test] #[tokio::test]
async fn every_non_publickey_hook_rejects_explicitly() { async fn every_non_publickey_hook_rejects_explicitly() {
@@ -345,11 +345,11 @@ async fn bridge_core_resolves_registered_fingerprints() {
assert_eq!(resolved.id, "user-0", "the identity rides to the handler"); assert_eq!(resolved.id, "user-0", "the identity rides to the handler");
assert!( assert!(
handler.resolved_identity().is_none(), handler.resolved_identity().is_none(),
"resolve_bridge only resolves — the hook stores; an unwired bridge leaves no state" "resolve_bridge only resolves (the hook stores; an unwired bridge leaves no state)"
); );
} }
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject — the /// - *Contract stamp:* ADR-004 decision 1 (`None` → reject; the
/// unknown / revoked fingerprint never resolves; fail-closed, no /// unknown / revoked fingerprint never resolves; fail-closed, no
/// weakly-authorized session, no cached resolution). /// weakly-authorized session, no cached resolution).
#[tokio::test] #[tokio::test]
@@ -375,18 +375,18 @@ async fn bridge_core_rejects_unknown_and_revoked_fingerprints() {
provider.revoke(fixture("delta").fingerprint); provider.revoke(fixture("delta").fingerprint);
assert!( assert!(
handler.resolve_bridge(&key, &Algorithm::Ed25519).is_none(), handler.resolve_bridge(&key, &Algorithm::Ed25519).is_none(),
"the revocation takes — no cached resolution survives" "the revocation takes; no cached resolution survives"
); );
} }
/// - *Contract stamp:* ADR-002 decision 4 (ed25519-only: the /// - *Contract stamp:* ADR-002 decision 4 (ed25519-only: the
/// non-ed25519 decline is explicit-over-inherited and precedes any /// non-ed25519 decline is explicit-over-inherited and precedes any
/// resolution — a registered fingerprint under a non-ed25519 /// resolution: a registered fingerprint under a non-ed25519
/// algorithm still declines). The sk-variant and `Other` stand-ins /// algorithm still declines). The sk-variant and `Other` stand-ins
/// cover the scheme classes the in-tree build cannot even parse /// cover the scheme classes the in-tree build cannot even parse
/// (rsa/dsa are dropped from the dependency tree and ssh-key ships /// (rsa/dsa are dropped from the dependency tree and ssh-key ships
/// without ecdsa/dsa/rsa features here, so no wire row can drive a /// without ecdsa/dsa/rsa features here, so no wire row can drive a
/// non-ed25519 key at this pin — recorded in the task notes; the /// non-ed25519 key at this pin (recorded in the task notes); the
/// gate is the explicit-over-inherited posture for the bump). /// gate is the explicit-over-inherited posture for the bump).
#[tokio::test] #[tokio::test]
async fn bridge_core_gates_non_ed25519_algorithms_before_resolution() { async fn bridge_core_gates_non_ed25519_algorithms_before_resolution() {
@@ -419,7 +419,7 @@ async fn bridge_core_gates_non_ed25519_algorithms_before_resolution() {
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint the bridge /// - *Contract stamp:* ADR-004 decision 1 (the fingerprint the bridge
/// computes is byte-format identical to the independent-issuer /// computes is byte-format identical to the independent-issuer
/// registrations — the wave-1 parity vectors pin the string; this row /// registrations; the wave-1 parity vectors pin the string, and this row
/// pins that the bridge resolves against exactly those values). /// pins that the bridge resolves against exactly those values).
#[tokio::test] #[tokio::test]
async fn bridge_core_fingerprint_is_the_registered_value() { async fn bridge_core_fingerprint_is_the_registered_value() {
@@ -437,7 +437,7 @@ async fn bridge_core_fingerprint_is_the_registered_value() {
} }
/// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded /// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded
/// standard-alphabet base64 — the wave-1 record, re-asserted against /// standard-alphabet base64 (the wave-1 record), re-asserted against
/// every committed fixture's `ssh-keygen -lf` value). /// every committed fixture's `ssh-keygen -lf` value).
#[test] #[test]
fn every_fixture_fingerprint_format_stays_the_contract_invariant_shape() { fn every_fixture_fingerprint_format_stays_the_contract_invariant_shape() {
@@ -452,7 +452,7 @@ fn every_fixture_fingerprint_format_stays_the_contract_invariant_shape() {
} }
/// - *Contract stamp:* ADR-005 decision 6 (host-key loading is the /// - *Contract stamp:* ADR-005 decision 6 (host-key loading is the
/// construction's business, never the auth hooks' — the bridge /// construction's business, never the auth hooks'; the bridge
/// handler builds over an absent host-key source and never loads it, /// handler builds over an absent host-key source and never loads it,
/// and the absent shape still refuses fail-closed if it were). /// and the absent shape still refuses fail-closed if it were).
#[tokio::test] #[tokio::test]
+16 -16
View File
@@ -1,4 +1,4 @@
//! The `w2-bounded-resources` contract rows — the bounded-resources //! The `w2-bounded-resources` contract rows: the bounded-resources
//! policy enforced through the real wire via the loopback harness: the //! policy enforced through the real wire via the loopback harness: the
//! in-crate concurrent-channel cap at every `channel_open_*` accept //! in-crate concurrent-channel cap at every `channel_open_*` accept
//! path, its recovery on close, its `Limits` tunability, and the //! path, its recovery on close, its `Limits` tunability, and the
@@ -8,7 +8,7 @@
//! `channel_buffer_size`), the inactivity wall clock and the remaining //! `channel_buffer_size`), the inactivity wall clock and the remaining
//! auth-counting posture ride the config-mapping tests //! auth-counting posture ride the config-mapping tests
//! (`adapter.rs` `#[cfg(test)]` + the harness's own //! (`adapter.rs` `#[cfg(test)]` + the harness's own
//! `client_config_maps_the_limits_postures`) — recorded, not //! `client_config_maps_the_limits_postures`); recorded, not
//! re-proven on the wire here. //! re-proven on the wire here.
//! //!
//! The cap rows are feature-gated to the builds whose opens can be //! The cap rows are feature-gated to the builds whose opens can be
@@ -41,7 +41,7 @@ fn registered_provider(fingerprints: &[&str]) -> Arc<MemIdentityProvider> {
} }
/// Mounts an authenticated loopback session over the doubles with the /// Mounts an authenticated loopback session over the doubles with the
/// row's [`Limits`] — the bounded-resources rows' driver. /// row's [`Limits`], the bounded-resources rows' driver.
async fn authenticated_with(limits: Limits) -> LoopbackSession { async fn authenticated_with(limits: Limits) -> LoopbackSession {
let provider = registered_provider(&[fixture("alpha").fingerprint]); let provider = registered_provider(&[fixture("alpha").fingerprint]);
let adapter = SshAdapter::new(serve_config( let adapter = SshAdapter::new(serve_config(
@@ -67,7 +67,7 @@ async fn authenticated_with(limits: Limits) -> LoopbackSession {
session session
} }
/// Matches an open failure's SSH reason — the wire decline reason the /// Matches an open failure's SSH reason, the wire decline reason the
/// cap-gate rows assert. /// cap-gate rows assert.
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) { fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
assert!( assert!(
@@ -80,7 +80,7 @@ fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
} }
/// - *Contract stamp:* `overview.md` §Bounded-resources policy (the /// - *Contract stamp:* `overview.md` §Bounded-resources policy (the
/// auth-attempts row — the `max_auth_attempts` knob is behavioral, /// auth-attempts row; the `max_auth_attempts` knob is behavioral,
/// verified where cheap: the ceiling disconnects an exceeding /// verified where cheap: the ceiling disconnects an exceeding
/// client) + ADR-004 decision 6 (the mapping in `adapter.rs` makes /// client) + ADR-004 decision 6 (the mapping in `adapter.rs` makes
/// the knob the counting ceiling; within the ceiling, failures stay /// the knob the counting ceiling; within the ceiling, failures stay
@@ -91,7 +91,7 @@ fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
/// three unregistered keys. The first two rejections stay ordinary /// three unregistered keys. The first two rejections stay ordinary
/// failures with the advertisement intact; the third request past the /// failures with the advertisement intact; the third request past the
/// ceiling is answered with the server's /// ceiling is answered with the server's
/// `Disconnect::NoMoreAuthMethodsAvailable` — client-side, the /// `Disconnect::NoMoreAuthMethodsAvailable`. Client-side, the
/// in-flight attempt sees russh's synthesized `Failure` with the /// in-flight attempt sees russh's synthesized `Failure` with the
/// **empty** remaining-methods set (no reply ever came; the /// **empty** remaining-methods set (no reply ever came; the
/// advertisement is gone). The session ends cleanly server-side. /// advertisement is gone). The session ends cleanly server-side.
@@ -165,7 +165,7 @@ async fn auth_attempt_ceiling_disconnects_an_exceeding_client() {
); );
} }
Err(_) => { Err(_) => {
// The client loop already exited on the disconnect — the // The client loop already exited on the disconnect, the
// equally-valid torn-down shape (the send side is gone). // equally-valid torn-down shape (the send side is gone).
} }
} }
@@ -177,7 +177,7 @@ async fn auth_attempt_ceiling_disconnects_an_exceeding_client() {
.expect("the ceiling disconnect ends the session cleanly"); .expect("the ceiling disconnect ends the session cleanly");
} }
/// The remaining-methods set of an [`AuthResult`] — `None` on success /// The remaining-methods set of an [`AuthResult`]; `None` on success
/// (the within-ceiling vs past-ceiling observation shape). /// (the within-ceiling vs past-ceiling observation shape).
fn remaining_ads(result: &russh::client::AuthResult) -> Option<russh::MethodSet> { fn remaining_ads(result: &russh::client::AuthResult) -> Option<russh::MethodSet> {
match result { match result {
@@ -208,7 +208,7 @@ async fn concurrent_channel_cap_declines_and_recovers_over_the_wire() {
let err = open_session_channel(&session.client) let err = open_session_channel(&session.client)
.await .await
.expect_err("the third open is refused at the cap — never queued"); .expect_err("the third open is refused at the cap, never queued");
open_failure( open_failure(
err, err,
ChannelOpenFailure::AdministrativelyProhibited, ChannelOpenFailure::AdministrativelyProhibited,
@@ -219,13 +219,13 @@ async fn concurrent_channel_cap_declines_and_recovers_over_the_wire() {
// The close reply is not observable client-side at this pin: russh // The close reply is not observable client-side at this pin: russh
// removes a locally-closed channel from its map before the // removes a locally-closed channel from its map before the
// server's CHANNEL_CLOSE arrives back, so no message — and no // server's CHANNEL_CLOSE arrives back; no message, and no
// `None` — is ever delivered (the harness's `await_close` only // `None`, is ever delivered (the harness's `await_close` only
// observes *server-initiated* closes). The recovery observation is // observes *server-initiated* closes). The recovery observation is
// the open itself, and it is deterministic: close and open ride // the open itself, and it is deterministic: close and open ride
// the client loop's single FIFO event queue, they are written in // the client loop's single FIFO event queue, they are written in
// that order on the wire, and the server's packet loop processes // that order on the wire, and the server's packet loop processes
// them in order — the counted close releases before the recovery // them in order; the counted close releases before the recovery
// open is admitted. Had the release not happened, this open // open is admitted. Had the release not happened, this open
// declines and this expect fails. // declines and this expect fails.
let _third = open_session_channel(&session.client) let _third = open_session_channel(&session.client)
@@ -249,7 +249,7 @@ async fn zero_cap_refuses_every_open_and_poisons_nothing() {
for attempt in 1..=2 { for attempt in 1..=2 {
let err = open_session_channel(&session.client) let err = open_session_channel(&session.client)
.await .await
.expect_err("a zero cap refuses the very first open — no half-open, no queue"); .expect_err("a zero cap refuses the very first open: no half-open, no queue");
open_failure( open_failure(
err, err,
ChannelOpenFailure::AdministrativelyProhibited, ChannelOpenFailure::AdministrativelyProhibited,
@@ -262,8 +262,8 @@ async fn zero_cap_refuses_every_open_and_poisons_nothing() {
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded admission /// - *Contract stamp:* `w3-tunnel-direct`'s recorded admission
/// mechanics (decline-first-alloc-never applies to the cap budget /// mechanics (decline-first-alloc-never applies to the cap budget
/// too): the tunnel row's pre-admit classes — here the **absent /// too): the tunnel row's pre-admit classes (here the **absent
/// fabric handle** under the feature — answer before the admission /// fabric handle** under the feature) answer before the admission
/// is taken, so under an exhausted cap the absent-handle class /// is taken, so under an exhausted cap the absent-handle class
/// (`ConnectFailed`, the cannot-bridge wire class) wins over the cap /// (`ConnectFailed`, the cannot-bridge wire class) wins over the cap
/// reason. The cap class on a *configured* direct row rides /// reason. The cap class on a *configured* direct row rides
@@ -281,7 +281,7 @@ async fn the_tunnel_rows_pre_admit_class_answers_before_the_cap_gate() {
let _first = open_session_channel(&session.client) let _first = open_session_channel(&session.client)
.await .await
.expect("the first open admits — the cap is now exhausted"); .expect("the first open admits, so the cap is now exhausted");
let err = open_direct_tcpip(&session.client, "internal.example", 5432) let err = open_direct_tcpip(&session.client, "internal.example", 5432)
.await .await
+18 -18
View File
@@ -1,4 +1,4 @@
//! The dial surface's contract rows (`w4-dial`) — driven through the //! The dial surface's contract rows (`w4-dial`), driven through the
//! in-process fake `sshd` ([`sshd_double`](common::sshd_double), the //! in-process fake `sshd` ([`sshd_double`](common::sshd_double), the
//! client-direction vehicle) plus the crate-to-crate smoke against //! client-direction vehicle) plus the crate-to-crate smoke against
//! the alkssh server direction. The row/stamp discipline //! the alkssh server direction. The row/stamp discipline
@@ -21,7 +21,7 @@ use russh::{ChannelOpenFailure, Error};
use sshd_double::SshdDouble; use sshd_double::SshdDouble;
/// A vault-backed key source over the alpha fixture — the standard /// A vault-backed key source over the alpha fixture, the standard
/// custody posture (the fixture PEM sealed, decrypt-at-dial behind /// custody posture (the fixture PEM sealed, decrypt-at-dial behind
/// the production source). /// the production source).
fn alpha_key_source() -> Arc<dyn KeySource> { fn alpha_key_source() -> Arc<dyn KeySource> {
@@ -91,10 +91,10 @@ fn postured_key_source(posture: &'static str) -> (Arc<dyn KeySource>, KeySourceE
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// explicit-vs-drop boundary, the OQ-12 primitives-only posture) — /// explicit-vs-drop boundary, the OQ-12 primitives-only posture):
/// **drop is never the clean path**: dropping the (feature-free, /// **drop is never the clean path**. Dropping the (feature-free,
/// last) wire-stack handle ends the runner and the transport goes /// last) wire-stack handle ends the runner and the transport goes
/// down abruptly — the double's serving outcome is the error wire /// down abruptly; the double's serving outcome is the error wire
/// class, never the clean session end only `close()` produces (see /// class, never the clean session end only `close()` produces (see
/// `dial_success_lands_an_authenticated_connection`'s `Ok`). /// `dial_success_lands_an_authenticated_connection`'s `Ok`).
#[tokio::test] #[tokio::test]
@@ -117,14 +117,14 @@ async fn dropping_the_connection_is_an_abrupt_teardown_close_is_the_only_clean_p
let outcome = double.next_session_outcome().await; let outcome = double.next_session_outcome().await;
assert!( assert!(
matches!(outcome, Some(Err(_))), matches!(outcome, Some(Err(_))),
"the un-closed drop tears the peer's session down abruptly — \ "the un-closed drop tears the peer's session down abruptly; \
never the clean `Ok`, got {outcome:?}" never the clean `Ok`, got {outcome:?}"
); );
double.shutdown(); double.shutdown();
} }
/// - *Contract stamp:* the dial surface's success row: decrypt-at-dial /// - *Contract stamp:* the dial surface's success row: decrypt-at-dial
/// → the policy accept → the single publickey attempt — an /// → the policy accept → the single publickey attempt: an
/// authenticated connection, the dial data carried on its surface, /// authenticated connection, the dial data carried on its surface,
/// and the clean close the peer observes. /// and the clean close the peer observes.
#[tokio::test] #[tokio::test]
@@ -171,7 +171,7 @@ async fn dial_success_lands_an_authenticated_connection() {
} }
/// `127.0.0.1:9` is deliberately unbound in the no-dial rows: a dial /// `127.0.0.1:9` is deliberately unbound in the no-dial rows: a dial
/// that reached the socket would fail with a wire class — only a /// that reached the socket would fail with a wire class; only a
/// refused key load can produce the key-source classes here. /// refused key load can produce the key-source classes here.
/// ///
/// - *Contract stamp:* ADR-005 decision 2 (decrypt-at-*dial*) + /// - *Contract stamp:* ADR-005 decision 2 (decrypt-at-*dial*) +
@@ -200,7 +200,7 @@ async fn every_key_source_posture_is_a_no_dial_outcome() {
} }
/// - *Contract stamp:* OQ-05 resolution (`host_key.rs`): the crate's /// - *Contract stamp:* OQ-05 resolution (`host_key.rs`): the crate's
/// built-in default policy connects to nothing — the dial refuses /// built-in default policy connects to nothing; the dial refuses
/// as the decline class (`DialError::HostKeyRejected`), never a /// as the decline class (`DialError::HostKeyRejected`), never a
/// wire crash class, never a fallback. /// wire crash class, never a fallback.
#[tokio::test] #[tokio::test]
@@ -224,7 +224,7 @@ async fn the_crate_default_policy_connects_to_nothing() {
} }
/// - *Contract stamp:* OQ-05 resolution, case 1 (known beforehand): /// - *Contract stamp:* OQ-05 resolution, case 1 (known beforehand):
/// an assembly pin **hard-declines on mismatch** — the dial aborts; /// an assembly pin **hard-declines on mismatch** (the dial aborts);
/// the policy's own log carries the context (the presented key's /// the policy's own log carries the context (the presented key's
/// real fingerprint rode the policy; the crate never second-guesses). /// real fingerprint rode the policy; the crate never second-guesses).
#[tokio::test] #[tokio::test]
@@ -257,7 +257,7 @@ async fn pin_mismatch_hard_declines() {
} }
/// - *Contract stamp:* OQ-05 resolution, case 2 (unknown beforehand): /// - *Contract stamp:* OQ-05 resolution, case 2 (unknown beforehand):
/// a permissive assembly policy decides — the dial proceeds; the /// a permissive assembly policy decides and the dial proceeds; the
/// crate ships no accept-all of its own (the permissive posture /// crate ships no accept-all of its own (the permissive posture
/// was this row's explicit choice, `MemHostKeyPolicy:: /// was this row's explicit choice, `MemHostKeyPolicy::
/// with_default(true)`). /// with_default(true)`).
@@ -285,7 +285,7 @@ async fn case2_policy_decides_and_the_dial_proceeds() {
/// - *Contract stamp:* the publickey-only family contract: the /// - *Contract stamp:* the publickey-only family contract: the
/// remote end refusing the single attempt is the dial's /// remote end refusing the single attempt is the dial's
/// [`DialError::AuthRejected`] — no retry, no fallback method, the /// [`DialError::AuthRejected`]: no retry, no fallback method, the
/// attempt closed behind the refusal. /// attempt closed behind the refusal.
#[tokio::test] #[tokio::test]
async fn refused_authentication_is_the_dials_no_connection_outcome() { async fn refused_authentication_is_the_dials_no_connection_outcome() {
@@ -324,8 +324,8 @@ async fn malformed_address_refuses_at_the_dial() {
/// - *Contract stamp:* the bounded-config wiring (the task's /// - *Contract stamp:* the bounded-config wiring (the task's
/// acceptance): the dial's russh `Config` carries the /// acceptance): the dial's russh `Config` carries the
/// [`Limits`]-mapped knobs — the keepalive 15 s / 3 posture, the /// [`Limits`]-mapped knobs (the keepalive 15 s / 3 posture, the
/// always-set wall clock, the window/packet/buffer knobs verbatim — /// always-set wall clock, the window/packet/buffer knobs verbatim)
/// observed from outside the crate through the doc-hidden mapping. /// observed from outside the crate through the doc-hidden mapping.
#[test] #[test]
fn the_bounded_config_wiring_is_observable() { fn the_bounded_config_wiring_is_observable() {
@@ -341,7 +341,7 @@ fn the_bounded_config_wiring_is_observable() {
/// - *Contract stamp:* ADR-003 §4 (the client-side fail-closed /// - *Contract stamp:* ADR-003 §4 (the client-side fail-closed
/// acceptor): the dialed client's skeleton declines **every** /// acceptor): the dialed client's skeleton declines **every**
/// server-initiated open on the wire — the double's initiator /// server-initiated open on the wire (the double's initiator
/// observes each open as an unconfirmed refusal, never russh's /// observes each open as an unconfirmed refusal, never russh's
/// accept-and-drop default; the never-feature rows (x11, agent) /// accept-and-drop default; the never-feature rows (x11, agent)
/// decline too; the declines consulted no host-key policy; and the /// decline too; the declines consulted no host-key policy; and the
@@ -351,7 +351,7 @@ fn the_bounded_config_wiring_is_observable() {
/// (`should_accept_unknown_server_channel` has no wire row: russh's /// (`should_accept_unknown_server_channel` has no wire row: russh's
/// server surface exposes no initiator for an unknown type and the /// server surface exposes no initiator for an unknown type and the
/// `ChannelId` is deliberately unconstructible outside the library /// `ChannelId` is deliberately unconstructible outside the library
/// — the skeleton's explicit `false` is code-pinned, not /// while the skeleton's explicit `false` is code-pinned, not
/// row-pinned; recorded in the task.) /// row-pinned; recorded in the task.)
#[tokio::test] #[tokio::test]
async fn server_initiated_opens_decline_cleanly_on_the_wire() { async fn server_initiated_opens_decline_cleanly_on_the_wire() {
@@ -398,7 +398,7 @@ async fn server_initiated_opens_decline_cleanly_on_the_wire() {
assert_eq!( assert_eq!(
policy.decisions().len(), policy.decisions().len(),
1, 1,
"the declines consulted no host-key policy — one decision, the dial's" "the declines consulted no host-key policy: one decision, the dial's"
); );
let forwarded = event_capture::await_event(&[ let forwarded = event_capture::await_event(&[
@@ -440,7 +440,7 @@ async fn server_initiated_opens_decline_cleanly_on_the_wire() {
/// The smoke posture (`w4-dial`'s decided + recorded item): the /// The smoke posture (`w4-dial`'s decided + recorded item): the
/// alkssh client ↔ alkssh server in-process loopback over a real TCP /// alkssh client ↔ alkssh server in-process loopback over a real TCP
/// socket — the cheap real-stack smoke (both russh stacks, the full /// socket, the cheap real-stack smoke (both russh stacks, the full
/// auth bridge server-side, the full custody + policy chain /// auth bridge server-side, the full custody + policy chain
/// client-side). A real-`sshd` dial is wave-5 e2e territory. /// client-side). A real-`sshd` dial is wave-5 e2e territory.
/// ///
+33 -33
View File
@@ -1,10 +1,10 @@
//! The op rows' contract rows (`w4-op-rows`) — driven through the //! The op rows' contract rows (`w4-op-rows`), driven through the
//! in-process fake `sshd` grown with the op-row script //! in-process fake `sshd` grown with the op-row script
//! ([`sshd_double`](common::sshd_double), the client-direction //! ([`sshd_double`](common::sshd_double), the client-direction
//! vehicle): featured + working rows (exec exit-status, pty //! vehicle): featured + working rows (exec exit-status, pty
//! window-change riding the live pump, the direct-tcpip pump, the git //! window-change riding the live pump, the direct-tcpip pump, the git
//! exec round trip), the decline rows (server-rejected opens, the //! exec round trip), the decline rows (server-rejected opens, the
//! sftp placeholder, the off-Unix streamlocal twin — OQ-11), and the //! sftp placeholder, the off-Unix streamlocal twin (OQ-11), and the
//! connection-stays-usable row after each decline class. The //! connection-stays-usable row after each decline class. The
//! row/stamp discipline (`tests/common/mod.rs`'s doc) applies //! row/stamp discipline (`tests/common/mod.rs`'s doc) applies
//! verbatim. //! verbatim.
@@ -13,8 +13,8 @@
//! rows' cheap real-stack smoke is the alkssh-client-op-row ↔ //! rows' cheap real-stack smoke is the alkssh-client-op-row ↔
//! alkssh-server-direction loopback (both russh stacks; the client //! alkssh-server-direction loopback (both russh stacks; the client
//! exec row driven against the server direction's *real* session rows //! exec row driven against the server direction's *real* session rows
//! — the chunk bridge's two arms meeting over one live wire), gated //! and the chunk bridge's two arms meeting over one live wire), gated
//! on tooling like the wave-3 smokes — skipped-with-reason when //! on tooling like the wave-3 smokes (skipped-with-reason when
//! absent, never silently green. A real-vanilla-`sshd` dial is wave-5 //! absent, never silently green. A real-vanilla-`sshd` dial is wave-5
//! e2e territory explicitly (see the task's Notes). //! e2e territory explicitly (see the task's Notes).
@@ -37,7 +37,7 @@ use common::sshd_double::DblExit;
use common::sshd_double::{SshdDouble, SshdObserved, SshdScript, spawn_scripted}; use common::sshd_double::{SshdDouble, SshdObserved, SshdScript, spawn_scripted};
use common::{MemHostKeyPolicy, TestVault, fixture}; use common::{MemHostKeyPolicy, TestVault, fixture};
/// A vault-backed key source over the alpha fixture — the dial /// A vault-backed key source over the alpha fixture, the dial
/// posture (the fixture PEM sealed, decrypt-at-dial). /// posture (the fixture PEM sealed, decrypt-at-dial).
fn alpha_key_source() -> Arc<dyn alkssh::KeySource> { fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
let vault = TestVault::unlocked(); let vault = TestVault::unlocked();
@@ -45,7 +45,7 @@ fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
Arc::new(alkssh::VaultKeySource::new(vault.handle(), Some(blob))) Arc::new(alkssh::VaultKeySource::new(vault.handle(), Some(blob)))
} }
/// Dials a fresh double under the given script — one dial per row per /// Dials a fresh double under the given script (one dial per row per
/// double. Returns the connection, the double (its outcome channel /// double. Returns the connection, the double (its outcome channel
/// for the clean-close rows) and the double's observations. /// for the clean-close rows) and the double's observations.
async fn dial_scripted(script: SshdScript) -> (SshConnection, SshdDouble, SshdObserved) { async fn dial_scripted(script: SshdScript) -> (SshConnection, SshdDouble, SshdObserved) {
@@ -76,7 +76,7 @@ async fn dial_scripted_limits(
} }
/// The clean-close observable: the explicit close leaves the double's /// The clean-close observable: the explicit close leaves the double's
/// session `Ok` — the decline rows' "the connection stays usable" /// session `Ok`, the decline rows' "the connection stays usable"
/// final act. /// final act.
async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) { async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) {
assert!(!connection.is_closed()); assert!(!connection.is_closed());
@@ -102,12 +102,12 @@ async fn no_open_observed(double: &mut SshdDouble, within: &str) {
/// OQ-13's client mirror: the sftp op declines cleanly in **every** /// OQ-13's client mirror: the sftp op declines cleanly in **every**
/// build (the `sftp` feature ships in the build string but gates no /// build (the `sftp` feature ships in the build string but gates no
/// route — the featured-build variant below pins that half too, /// route; the featured-build variant below pins that half too,
/// `--all-features` running both). /// `--all-features` running both).
/// ///
/// - *Contract stamp:* OQ-13 (the decline placeholder is the shipped /// - *Contract stamp:* OQ-13 (the decline placeholder is the shipped
/// behavior, both directions) + `client.md` §op rows (row 5) + §What /// behavior, both directions) + `client.md` §op rows (row 5) + §What
/// the crate does *not* include — before any wire op: no session /// the crate does *not* include: before any wire op, no session
/// channel ever opens at the peer, no subsystem request ever rides, /// channel ever opens at the peer, no subsystem request ever rides,
/// and the connection stays usable (the clean close is the double's /// and the connection stays usable (the clean close is the double's
/// clean session end). /// clean session end).
@@ -135,7 +135,7 @@ async fn sftp_feature_on_the_placeholder_still_declines() {
let err = connection let err = connection
.sftp() .sftp()
.await .await
.expect_err("the feature never serves the route (OQ-13 — the placeholder ships)"); .expect_err("the feature never serves the route (OQ-13, the placeholder ships)");
assert!(matches!(err, OpError::SftpNotServed), "got {err:?}"); assert!(matches!(err, OpError::SftpNotServed), "got {err:?}");
assert!(observed.execs().is_empty()); assert!(observed.execs().is_empty());
no_open_observed(&mut double, "the featured placeholder's decline").await; no_open_observed(&mut double, "the featured placeholder's decline").await;
@@ -150,7 +150,7 @@ mod session_rows {
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
/// Frames one chunk by hand (`stream_type` byte + the big-endian /// Frames one chunk by hand (`stream_type` byte + the big-endian
/// `u32` length + payload — the published header format, read /// `u32` length + payload, the published header format read
/// back only through alktty's own [`ChunkReader`]). /// back only through alktty's own [`ChunkReader`]).
fn frame(stream_type: u8, payload: &[u8]) -> Vec<u8> { fn frame(stream_type: u8, payload: &[u8]) -> Vec<u8> {
let mut out = vec![stream_type]; let mut out = vec![stream_type];
@@ -159,14 +159,14 @@ mod session_rows {
out out
} }
/// Frames one control chunk — the family currency's own /// Frames one control chunk; the family currency's own
/// serializer builds the payload. /// serializer builds the payload.
async fn control_frame(message: &ControlMessage) -> Vec<u8> { async fn control_frame(message: &ControlMessage) -> Vec<u8> {
let payload = message.to_json().expect("the control serializes"); let payload = message.to_json().expect("the control serializes");
frame(STREAM_CTRL_IN, &payload) frame(STREAM_CTRL_IN, &payload)
} }
/// Drains the hub's chunk plane until the exit chunk or the end — /// Drains the hub's chunk plane until the exit chunk or the end;
/// returns (stdout, stderr, the exit code in-band, if any). /// returns (stdout, stderr, the exit code in-band, if any).
async fn read_to_exit(mut session: alkssh::RemoteSession) -> (Vec<u8>, Vec<u8>, Option<i32>) { async fn read_to_exit(mut session: alkssh::RemoteSession) -> (Vec<u8>, Vec<u8>, Option<i32>) {
let mut stdout = Vec::new(); let mut stdout = Vec::new();
@@ -206,7 +206,7 @@ mod session_rows {
} }
/// - *Contract stamp:* `client.md` §op rows (row 1, the exec /// - *Contract stamp:* `client.md` §op rows (row 1, the exec
/// translation + the family plane) — the wire carries the /// translation + the family plane): the wire carries the
/// family command verbatim (`exec()`, never `request_exec`); /// family command verbatim (`exec()`, never `request_exec`);
/// stdout/stderr come back as distinct chunks and the exit /// stdout/stderr come back as distinct chunks and the exit
/// status rides the family chunk in-band, last. /// status rides the family chunk in-band, last.
@@ -241,7 +241,7 @@ mod session_rows {
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
} }
/// - *Contract stamp:* POC-3 §D.3 (the Success/Failure semantics — /// - *Contract stamp:* POC-3 §D.3 (the Success/Failure semantics:
/// **the Failure reply surfaces on the channel queue's drain, /// **the Failure reply surfaces on the channel queue's drain,
/// not as `Err` from the request call**) → /// not as `Err` from the request call**) →
/// [`OpError::RequestRefused`]; the refused row closes its /// [`OpError::RequestRefused`]; the refused row closes its
@@ -271,7 +271,7 @@ mod session_rows {
/// (the server direction's pre-parse cap, `Limits:: /// (the server direction's pre-parse cap, `Limits::
/// max_exec_command_len`, mirrored at the command-construction /// max_exec_command_len`, mirrored at the command-construction
/// gate): a beyond-budget command declines **before any wire /// gate): a beyond-budget command declines **before any wire
/// op** — no channel, no request, bounded by construction. /// op**; no channel, no request, bounded by construction.
#[tokio::test] #[tokio::test]
async fn oversized_exec_declines_before_any_wire_op() { async fn oversized_exec_declines_before_any_wire_op() {
let limits = Limits { let limits = Limits {
@@ -295,7 +295,7 @@ mod session_rows {
} }
/// - *Contract stamp:* `client.md` §op rows (row 1, the pty/shell /// - *Contract stamp:* `client.md` §op rows (row 1, the pty/shell
/// translation) — the family [`TerminalParamsWire`] rides the /// translation): the family [`TerminalParamsWire`] rides the
/// wire `pty_request` verbatim (the reserved modes never ride: /// wire `pty_request` verbatim (the reserved modes never ride:
/// the double sees the params, and the wire request carries the /// the double sees the params, and the wire request carries the
/// empty terminal-modes set), and the shell request launches /// empty terminal-modes set), and the shell request launches
@@ -326,7 +326,7 @@ mod session_rows {
} }
/// - *Contract stamp:* the pipe-mode shell parity (mirroring the /// - *Contract stamp:* the pipe-mode shell parity (mirroring the
/// server direction's decided row — a pty-less shell is /// server direction's decided row: a pty-less shell is
/// legitimate vanilla and served, never declined): the /// legitimate vanilla and served, never declined): the
/// `params: None` shell carries **no** pty request. /// `params: None` shell carries **no** pty request.
#[tokio::test] #[tokio::test]
@@ -348,7 +348,7 @@ mod session_rows {
/// - *Contract stamp:* the pty control rows riding the **live /// - *Contract stamp:* the pty control rows riding the **live
/// pump** (the POC-3 §D.3 window-change/signal mechanics', /// pump** (the POC-3 §D.3 window-change/signal mechanics',
/// client arm): the session never `into_stream()`s, so the /// client arm): the session never `into_stream()`s, so the
/// fire-and-forget rows ride the write half mid-session — the /// fire-and-forget rows ride the write half mid-session; the
/// `CTRL_IN` `Resize` translates to the wire `window_change` /// `CTRL_IN` `Resize` translates to the wire `window_change`
/// and `Signal` to the wire `signal` (the double's /// and `Signal` to the wire `signal` (the double's
/// observations are the witnesses). /// observations are the witnesses).
@@ -418,7 +418,7 @@ mod session_rows {
} }
/// - *Contract stamp:* the family exit-currency's signal mirror /// - *Contract stamp:* the family exit-currency's signal mirror
/// (mapping.md's derivation contract, the client arm — consumed /// (mapping.md's derivation contract, the client arm, consumed
/// not re-derived): a wire `exit-signal("TERM")` reports the /// not re-derived): a wire `exit-signal("TERM")` reports the
/// family code `−15` in-band (rule 2's value, the table read /// family code `−15` in-band (rule 2's value, the table read
/// backwards; the derivation never invents). /// backwards; the derivation never invents).
@@ -447,8 +447,8 @@ mod session_rows {
} }
/// - *Contract stamp:* the never-half-capability posture (ADR-003 /// - *Contract stamp:* the never-half-capability posture (ADR-003
/// decision 3, client mirror) — a **refused pty refuses the /// decision 3, client mirror): a **refused pty refuses the
/// whole row**: the exec behind it never rides the wire (the /// whole row**; the exec behind it never rides the wire (the
/// double's exec observations stay empty). /// double's exec observations stay empty).
#[tokio::test] #[tokio::test]
async fn refused_pty_refuses_the_whole_row_never_half_serves() { async fn refused_pty_refuses_the_whole_row_never_half_serves() {
@@ -482,8 +482,8 @@ mod session_rows {
/// class, `client.md` §Correctness facts): the session open /// class, `client.md` §Correctness facts): the session open
/// refused (the scripted count) → `OpenDeclined(Prohibited)`, /// refused (the scripted count) → `OpenDeclined(Prohibited)`,
/// then a later launch on the *same connection* succeeds and /// then a later launch on the *same connection* succeeds and
/// pumps — the usable-after-decline posture — and everything /// pumps; that is the usable-after-decline posture, and
/// closes clean. /// everything closes clean.
#[tokio::test] #[tokio::test]
async fn server_rejected_open_declines_and_the_connection_stays_usable() { async fn server_rejected_open_declines_and_the_connection_stays_usable() {
let script = SshdScript { let script = SshdScript {
@@ -526,7 +526,7 @@ mod session_rows {
/// - *Contract stamp:* the task's smoke item (the §module docs' /// - *Contract stamp:* the task's smoke item (the §module docs'
/// decided posture; like the wave-3 smokes, skipped-with-reason /// decided posture; like the wave-3 smokes, skipped-with-reason
/// when the tooling is absent): the client exec row driven /// when the tooling is absent): the client exec row driven
/// against the alkssh *server direction*'s real session rows — /// against the alkssh *server direction*'s real session rows,
/// both russh stacks over one live wire, the chunk bridge's two /// both russh stacks over one live wire, the chunk bridge's two
/// arms meeting (the client's family plane consuming the /// arms meeting (the client's family plane consuming the
/// server's family plane), data and the exit-status /// server's family plane), data and the exit-status
@@ -539,7 +539,7 @@ mod session_rows {
.output() .output()
.is_err() .is_err()
{ {
eprintln!("SKIPPED (with reason): the op-row smoke — sh is unavailable"); eprintln!("SKIPPED (with reason): the op-row smoke, sh is unavailable");
return; return;
} }
@@ -622,8 +622,8 @@ mod tunnel_rows {
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// - *Contract stamp:* `client.md` §op rows (row 2, the typed /// - *Contract stamp:* `client.md` §op rows (row 2, the typed
/// `channel_open_direct_tcpip` open — the alksocks `DialFn` /// `channel_open_direct_tcpip` open; the alksocks `DialFn`
/// composes over this surface) + POC-3 §C.1's recipe reversed — /// composes over this surface) + POC-3 §C.1's recipe reversed:
/// the pump is the raw byte duplex, round-tripping, no channel /// the pump is the raw byte duplex, round-tripping, no channel
/// queue consumed anywhere between the open and the /// queue consumed anywhere between the open and the
/// `into_stream()` conversion. /// `into_stream()` conversion.
@@ -654,7 +654,7 @@ mod tunnel_rows {
} }
/// - *Contract stamp:* the server-rejected open's family mirror /// - *Contract stamp:* the server-rejected open's family mirror
/// (POC-1 §6 — the typed open resolves `Err` on the reject, /// (POC-1 §6: the typed open resolves `Err` on the reject,
/// family-mapped) + the connection-stays-usable row **per /// family-mapped) + the connection-stays-usable row **per
/// decline class**: after the `ConnectFailed` class the /// decline class**: after the `ConnectFailed` class the
/// streamlocal twin still opens and pumps through the same /// streamlocal twin still opens and pumps through the same
@@ -700,7 +700,7 @@ mod tunnel_rows {
} }
/// - *Contract stamp:* OQ-11 (the streamlocal twin's runtime-Unix /// - *Contract stamp:* OQ-11 (the streamlocal twin's runtime-Unix
/// gate, client arm — universal compile, clean off-Unix decline /// gate, client arm: universal compile, clean off-Unix decline
/// **before any wire op**): this row compiles only off-Unix; /// **before any wire op**): this row compiles only off-Unix;
/// the on-Unix builds ride the usable-after-decline row's /// the on-Unix builds ride the usable-after-decline row's
/// success half. /// success half.
@@ -729,7 +729,7 @@ mod git_rows {
/// writes (a placeholder pack-side payload for the pump row). /// writes (a placeholder pack-side payload for the pump row).
const ADVERT: &[u8] = b"0008version 2\n0000"; const ADVERT: &[u8] = b"0008version 2\n0000";
/// - *Contract stamp:* `client.md` §op rows (row 4 — the shared /// - *Contract stamp:* `client.md` §op rows (row 4; the shared
/// exec grammar's command, *reused, not forked*; the wire /// exec grammar's command, *reused, not forked*; the wire
/// carries `git-upload-pack '/owner/demo'` byte-verbatim) + the /// carries `git-upload-pack '/owner/demo'` byte-verbatim) + the
/// pack duplex pump + the exit-status consumption. /// pack duplex pump + the exit-status consumption.
@@ -793,7 +793,7 @@ mod git_rows {
} }
/// - *Contract stamp:* the per-length cap's client mirror (the /// - *Contract stamp:* the per-length cap's client mirror (the
/// shared grammar's verdict — `Limits:: /// shared grammar's verdict, `Limits::
/// max_exec_command_len`): before any wire op. /// max_exec_command_len`): before any wire op.
#[tokio::test] #[tokio::test]
async fn oversized_git_command_declines_before_any_wire_op() { async fn oversized_git_command_declines_before_any_wire_op() {
@@ -814,7 +814,7 @@ mod git_rows {
} }
/// - *Contract stamp:* the upstream's own refusal is the /// - *Contract stamp:* the upstream's own refusal is the
/// request-refusal class (the drain's `Failure` — POC-3 §D.3's /// request-refusal class (the drain's `Failure`, POC-3 §D.3's
/// git-row shape): declined cleanly, the connection intact. /// git-row shape): declined cleanly, the connection intact.
#[tokio::test] #[tokio::test]
async fn refused_git_exec_surfaces_the_request_refusal() { async fn refused_git_exec_surfaces_the_request_refusal() {
+41 -41
View File
@@ -1,5 +1,5 @@
//! The `-R` leg's contract rows (`w4-reverse-listen` + the listener //! The `-R` leg's contract rows (`w4-reverse-listen` + the listener
//! handles' lifecycle rows — `w4-lifecycle`) — driven through the //! handles' lifecycle rows, `w4-lifecycle`), driven through the
//! in-process fake `sshd` grown with the reverse-forward surfaces //! in-process fake `sshd` grown with the reverse-forward surfaces
//! ([`sshd_double`](common::sshd_double): the `tcpip-forward` / //! ([`sshd_double`](common::sshd_double): the `tcpip-forward` /
//! `streamlocal-forward` hooks that bind **real** sockets and open the //! `streamlocal-forward` hooks that bind **real** sockets and open the
@@ -7,7 +7,7 @@
//! the listen rows (featured + registered → served, the port-0 mint, //! the listen rows (featured + registered → served, the port-0 mint,
//! the cancels + the listener-cap ceiling), the acceptor-registry rows //! the cancels + the listener-cap ceiling), the acceptor-registry rows
//! (featured + unregistered → clean decline; late-install across a //! (featured + unregistered → clean decline; late-install across a
//! live connection — decline-before-install, serve-after; uninstall; //! live connection: decline-before-install, serve-after; uninstall;
//! the data-leg pump round trip), the off-Unix streamlocal rows //! the data-leg pump round trip), the off-Unix streamlocal rows
//! (OQ-11), and the connection-stays-usable row after every decline //! (OQ-11), and the connection-stays-usable row after every decline
//! class. The lifecycle rows pin the handles' posture: the actually- //! class. The lifecycle rows pin the handles' posture: the actually-
@@ -20,12 +20,12 @@
//! //!
//! **Per-build matrix note (recorded):** this file is the //! **Per-build matrix note (recorded):** this file is the
//! `tunnel`-featured build's rows (the only build whose surface can //! `tunnel`-featured build's rows (the only build whose surface can
//! issue the reverse rows at all — the unfeatured build's compile-time //! issue the reverse rows at all, the unfeatured build's compile-time
//! mirror); the wire decline rows for the *unfeatured* builds run in //! mirror); the wire decline rows for the *unfeatured* builds run in
//! every build through `tests/client_dial.rs`'s //! every build through `tests/client_dial.rs`'s
//! `server_initiated_opens_decline_cleanly_on_the_wire` (the //! `server_initiated_opens_decline_cleanly_on_the_wire` (the
//! decline-all skeleton the dial carries in a build without `tunnel` //! decline-all skeleton the dial carries in a build without `tunnel`
//! — no registry exists there, every hook declines). //! (no registry exists there, every hook declines).
#![cfg(feature = "tunnel")] #![cfg(feature = "tunnel")]
mod common; mod common;
@@ -42,7 +42,7 @@ use common::sshd_double::{DblListen, SshdDouble, SshdObserved, SshdScript, spawn
use common::{MemHostKeyPolicy, TestVault, fixture}; use common::{MemHostKeyPolicy, TestVault, fixture};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// A vault-backed key source over the alpha fixture — the dial /// A vault-backed key source over the alpha fixture, the dial
/// posture (the fixture PEM sealed, decrypt-at-dial). /// posture (the fixture PEM sealed, decrypt-at-dial).
fn alpha_key_source() -> Arc<dyn alkssh::KeySource> { fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
let vault = TestVault::unlocked(); let vault = TestVault::unlocked();
@@ -50,7 +50,7 @@ fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
Arc::new(alkssh::VaultKeySource::new(vault.handle(), Some(blob))) Arc::new(alkssh::VaultKeySource::new(vault.handle(), Some(blob)))
} }
/// Dials a fresh double under the given script — one dial per row. /// Dials a fresh double under the given script: one dial per row.
async fn dial_scripted_limits( async fn dial_scripted_limits(
script: SshdScript, script: SshdScript,
limits: alkssh::Limits, limits: alkssh::Limits,
@@ -76,7 +76,7 @@ async fn dial_scripted(script: SshdScript) -> (SshConnection, SshdDouble, SshdOb
} }
/// The clean-close observable: the explicit close leaves the double's /// The clean-close observable: the explicit close leaves the double's
/// session `Ok` — the "the connection stays usable" final act. /// session `Ok`, the "the connection stays usable" final act.
async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) { async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) {
assert!(!connection.is_closed()); assert!(!connection.is_closed());
connection.close().await; connection.close().await;
@@ -89,7 +89,7 @@ async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) {
} }
/// A free loopback port the row's listen asks for exactly (the probe /// A free loopback port the row's listen asks for exactly (the probe
/// binds, then frees — a tiny race, as everywhere a fixed port is /// binds, then frees (a tiny race, as everywhere a fixed port is
/// picked; the rows mostly mint dynamic ports instead). /// picked; the rows mostly mint dynamic ports instead).
async fn pick_port() -> u16 { async fn pick_port() -> u16 {
let probe = tokio::net::TcpListener::bind("127.0.0.1:0") let probe = tokio::net::TcpListener::bind("127.0.0.1:0")
@@ -98,7 +98,7 @@ async fn pick_port() -> u16 {
probe.local_addr().expect("addr").port() probe.local_addr().expect("addr").port()
} }
/// The byte round trip the acceptor double's echo pump runs — the /// The byte round trip the acceptor double's echo pump runs; the
/// data leg's plain duplex (the bytes back through the accepted /// data leg's plain duplex (the bytes back through the accepted
/// channel; the pump is the assembly's). /// channel; the pump is the assembly's).
async fn echo_pump(mut stream: alkssh::TunnelChannel) { async fn echo_pump(mut stream: alkssh::TunnelChannel) {
@@ -115,7 +115,7 @@ async fn echo_pump(mut stream: alkssh::TunnelChannel) {
} }
} }
/// What the acceptor double records per accepted open — the payload's /// What the acceptor double records per accepted open (the payload's
/// echo/originator fields (the duplex rides the type, never recorded). /// echo/originator fields (the duplex rides the type, never recorded).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
struct OpenRecord { struct OpenRecord {
@@ -125,7 +125,7 @@ struct OpenRecord {
originator_port: u32, originator_port: u32,
} }
/// The acceptor double — records the payload's fields and, when the /// The acceptor double: it records the payload's fields and, when the
/// row asks, pumps the duplex as an echo (the family handler shape: /// row asks, pumps the duplex as an echo (the family handler shape:
/// the assembly owns the pump). /// the assembly owns the pump).
#[derive(Clone)] #[derive(Clone)]
@@ -152,7 +152,7 @@ impl TcpipForwardedAcceptor for AcceptorDouble {
} }
} }
/// The streamlocal acceptor double — the unix slot's shape. /// The streamlocal acceptor double, the unix slot's shape.
#[derive(Clone)] #[derive(Clone)]
struct UnixAcceptorDouble { struct UnixAcceptorDouble {
opens: tokio::sync::mpsc::UnboundedSender<String>, opens: tokio::sync::mpsc::UnboundedSender<String>,
@@ -171,7 +171,7 @@ impl StreamlocalForwardedAcceptor for UnixAcceptorDouble {
} }
} }
/// The next accepted open's fields — the row's payload observation /// The next accepted open's fields, the row's payload observation
/// (bounded await: a wrong row must not hang). /// (bounded await: a wrong row must not hang).
async fn next_open( async fn next_open(
opens: &mut tokio::sync::mpsc::UnboundedReceiver<OpenRecord>, opens: &mut tokio::sync::mpsc::UnboundedReceiver<OpenRecord>,
@@ -184,8 +184,8 @@ async fn next_open(
} }
/// - *Contract stamp:* `client.md` §op rows row 4 (the `-R` leg's /// - *Contract stamp:* `client.md` §op rows row 4 (the `-R` leg's
/// listen + ingest row) + the module docs' listen-gate record — /// listen + ingest row) + the module docs' listen-gate record:
/// featured + registered → served: the /// featured + registered → served; the the
/// `reverse_listen` request rides the wire (the double's /// `reverse_listen` request rides the wire (the double's
/// `tcpip-forward` hook observes it verbatim), the actually-bound /// `tcpip-forward` hook observes it verbatim), the actually-bound
/// port returns, and the **data leg** round-trips: a connection to /// port returns, and the **data leg** round-trips: a connection to
@@ -219,7 +219,7 @@ async fn reverse_listen_serves_and_the_data_leg_pumps() {
assert_eq!( assert_eq!(
listener.endpoint().bound_port(), listener.endpoint().bound_port(),
Some(port), Some(port),
"an explicit port binds exactly that port — the handle carries it" "an explicit port binds exactly that port; the handle carries it"
); );
let mut conn = let mut conn =
@@ -255,7 +255,7 @@ async fn reverse_listen_serves_and_the_data_leg_pumps() {
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// bound-port spec) — `port = 0` mint: the peer picks and the /// bound-port spec) the `port = 0` mint: the peer picks and the
/// surfaced number is the actually-bound one (the row connects to /// surfaced number is the actually-bound one (the row connects to
/// it; the double's listener is the real socket, so the mint is the /// it; the double's listener is the real socket, so the mint is the
/// real shape sshd's `get_local_port` parity rides). /// real shape sshd's `get_local_port` parity rides).
@@ -306,7 +306,7 @@ async fn port_zero_mint_surfaces_the_actually_bound_port() {
} }
/// - *Contract stamp:* ADR-003 §4's issuing-side mirror + the module /// - *Contract stamp:* ADR-003 §4's issuing-side mirror + the module
/// docs' listen-gate record — a reverse listen with no registered /// docs' listen-gate record: a reverse listen with no registered
/// acceptor declines **before any wire op** (`NoAcceptor`; the /// acceptor declines **before any wire op** (`NoAcceptor`; the
/// half-capability posture: a bound-but-unserveable listener is /// half-capability posture: a bound-but-unserveable listener is
/// never asked for), and the connection stays usable. /// never asked for), and the connection stays usable.
@@ -327,13 +327,13 @@ async fn reverse_listen_without_an_acceptor_declines_before_any_wire_op() {
); );
assert!( assert!(
observed.listens().is_empty(), observed.listens().is_empty(),
"nothing rode the wire — the gate answered before the request" "nothing rode the wire; the gate answered before the request"
); );
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
} }
/// - *Contract stamp:* ADR-003 §4 — featured + unregistered → clean /// - *Contract stamp:* ADR-003 §4: featured + unregistered → clean
/// decline: a server-initiated `forwarded-tcpip` open on a /// decline: a server-initiated `forwarded-tcpip` open on a
/// live connection with an empty registry answers /// live connection with an empty registry answers
/// `AdministrativelyProhibited` (never russh's accept-and-drop /// `AdministrativelyProhibited` (never russh's accept-and-drop
@@ -381,8 +381,8 @@ async fn unregistered_forwarded_open_declines_cleanly() {
} }
/// - *Contract stamp:* the late-install dynamic (reverse-runner fact /// - *Contract stamp:* the late-install dynamic (reverse-runner fact
/// 2 — consumers turn capabilities on/off per connection) + the /// 2, consumers turn capabilities on/off per connection) + the
/// arrival-time gate — the same live connection: the pre-install /// arrival-time gate, the same live connection: the pre-install
/// arrival declines, the post-install arrival serves (never /// arrival declines, the post-install arrival serves (never
/// buffered, never half-served), and the acceptor double's late /// buffered, never half-served), and the acceptor double's late
/// install is the registry's whole surface. /// install is the registry's whole surface.
@@ -444,7 +444,7 @@ async fn late_install_serves_after_a_pre_install_arrival_declined() {
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
} }
/// - *Contract stamp:* the late-install toggle's off half — the /// - *Contract stamp:* the late-install toggle's off half: the
/// uninstall takes the capability off mid-connection: the /// uninstall takes the capability off mid-connection: the
/// post-uninstall arrival declines at arrival time (fail-closed; /// post-uninstall arrival declines at arrival time (fail-closed;
/// the already-accepted open's channel is the acceptor's business). /// the already-accepted open's channel is the acceptor's business).
@@ -501,7 +501,7 @@ async fn uninstall_takes_the_capability_off_before_a_later_arrival() {
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (every /// - *Contract stamp:* `client.md` §Lifecycle primitives (every
/// listener explicitly cancellable) + the double's sshd parity — /// listener explicitly cancellable) + the double's sshd parity:
/// the cancel tears the real listener down (the bound port /// the cancel tears the real listener down (the bound port
/// reconnects as refused), a cancel of an unknown listener is the /// reconnects as refused), a cancel of an unknown listener is the
/// peer's refusal (`ListenRefused`), and the connection stays /// peer's refusal (`ListenRefused`), and the connection stays
@@ -557,7 +557,7 @@ async fn cancel_tears_the_forward_down_and_unknown_cancels_refuse() {
} }
assert!( assert!(
refused, refused,
"the forward is torn down — the bound port refuses a reconnect" "the forward is torn down; the bound port refuses a reconnect"
); );
let again = connection.reverse_listen_cancel("127.0.0.1", port).await; let again = connection.reverse_listen_cancel("127.0.0.1", port).await;
@@ -572,16 +572,16 @@ async fn cancel_tears_the_forward_down_and_unknown_cancels_refuse() {
.await, .await,
Err(OpError::ListenRefused) Err(OpError::ListenRefused)
), ),
"no match by endpoint either — the raw-string parity holds" "no match by endpoint either; the raw-string parity holds"
); );
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// explicit-vs-drop boundary, `w4-lifecycle`) — **no drop-timing /// explicit-vs-drop boundary, `w4-lifecycle`): **no drop-timing
/// wire state**: dropping the listener handle without a cancel /// wire state**. Dropping the listener handle without a cancel
/// frees nothing — the peer's listen keeps serving (the arrival /// frees nothing; the peer's listen keeps serving (the arrival
/// still hands off through the registered acceptor); only the /// still hands off through the registered acceptor); only the
/// explicit cancel tears the forward down (the bound port then /// explicit cancel tears the forward down (the bound port then
/// refuses a reconnect). The `ReverseListener` carries no `Drop` /// refuses a reconnect). The `ReverseListener` carries no `Drop`
@@ -662,11 +662,11 @@ async fn dropping_the_listener_handle_leaves_no_drop_timing_wire_state() {
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// cancel-release relation) + the cap relation with the /// cancel-release relation) + the cap relation with the
/// dedicated listener counter — the handle's `cancel()` is the /// dedicated listener counter: the handle's `cancel()` is the
/// confirmed teardown **which releases the listener count**: beyond /// confirmed teardown **which releases the listener count**: beyond
/// the ceiling the listens refuse pre-wire, the handle's cancel /// the ceiling the listens refuse pre-wire, the handle's cancel
/// (not the stateless form) recovers the budget, the next listen /// (not the stateless form) recovers the budget, the next listen
/// admits again. The handle is consumed by its cancel — the one /// admits again. The handle is consumed by its cancel: the one
/// cancel through it; a second cancel rides the connection row and /// cancel through it; a second cancel rides the connection row and
/// refuses (the unknown-listener wire answer, no release). /// refuses (the unknown-listener wire answer, no release).
#[tokio::test] #[tokio::test]
@@ -733,7 +733,7 @@ async fn the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budg
.map(|(_, _, port)| *port) .map(|(_, _, port)| *port)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![first, third], vec![first, third],
"each handle's cancel rode the wire — one confirmed cancel per \ "each handle's cancel rode the wire; one confirmed cancel per \
admitted listener" admitted listener"
); );
@@ -742,7 +742,7 @@ async fn the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budg
connection.reverse_listen_cancel("127.0.0.1", third).await, connection.reverse_listen_cancel("127.0.0.1", third).await,
Err(OpError::ListenRefused) Err(OpError::ListenRefused)
), ),
"the stateless form's repeat is the unknown-listener refusal — \ "the stateless form's repeat is the unknown-listener refusal; \
the confirmed release ran exactly once (the budgets never \ the confirmed release ran exactly once (the budgets never \
manufactured out of a repeated cancel)" manufactured out of a repeated cancel)"
); );
@@ -751,12 +751,12 @@ async fn the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budg
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the row's /// - *Contract stamp:* `client.md` §Lifecycle primitives (the row's
/// recorded contract) + the teardown-mid-connection posture — the /// recorded contract) + the teardown-mid-connection posture: the
/// cancel lands **while a data leg is live**: the confirmed cancel /// cancel lands **while a data leg is live**: the confirmed cancel
/// tears the forward down (the bound port refuses a reconnect), the /// tears the forward down (the bound port refuses a reconnect), the
/// connection stays usable (the `cancel` row's channel-scoped /// connection stays usable (the `cancel` row's channel-scoped
/// honesty) and ends clean despite the live leg (the peer observes /// honesty) and ends clean despite the live leg (the peer observes
/// the clean session end — no half-open channels). /// the clean session end, no half-open channels).
#[tokio::test] #[tokio::test]
async fn cancel_mid_data_leg_tears_the_forward_down_mid_connection() { async fn cancel_mid_data_leg_tears_the_forward_down_mid_connection() {
let scripted = SshdScript { let scripted = SshdScript {
@@ -814,14 +814,14 @@ async fn cancel_mid_data_leg_tears_the_forward_down_mid_connection() {
} }
assert!( assert!(
refused, refused,
"the forward is torn down while the leg was live — no more arrivals" "the forward is torn down while the leg was live; no more arrivals"
); );
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
} }
/// - *Contract stamp:* the module docs' recorded cap relation (the /// - *Contract stamp:* the module docs' recorded cap relation (the
/// server-side listener-counter posture mirrored) — the dedicated /// server-side listener-counter posture mirrored): the dedicated
/// listener count against the `max_concurrent_channels` ceiling: /// listener count against the `max_concurrent_channels` ceiling:
/// beyond it the listen declines **before any wire op** /// beyond it the listen declines **before any wire op**
/// (`ListenCapExceeded`), and a confirmed cancel recovers the /// (`ListenCapExceeded`), and a confirmed cancel recovers the
@@ -886,7 +886,7 @@ async fn the_listener_ceiling_bounds_the_listen_map_and_recovers_on_cancel() {
} }
/// - *Contract stamp:* the module docs' refuse-never-replace posture /// - *Contract stamp:* the module docs' refuse-never-replace posture
/// through the connection accessor — the second install is declined /// through the connection accessor: the second install is declined
/// (`AcceptorHeld`), the first acceptor stays served, and the /// (`AcceptorHeld`), the first acceptor stays served, and the
/// uninstall/uninstall-again cycle reports honestly. /// uninstall/uninstall-again cycle reports honestly.
#[tokio::test] #[tokio::test]
@@ -1002,7 +1002,7 @@ async fn streamlocal_listen_serves_arrivals_and_cancels_through_the_unix_gate()
let reconnect = tokio::net::UnixStream::connect(&socket_path).await; let reconnect = tokio::net::UnixStream::connect(&socket_path).await;
assert!( assert!(
reconnect.is_err(), reconnect.is_err(),
"the unix forward is torn down — the socket path refuses a reconnect, got {reconnect:?}" "the unix forward is torn down; the socket path refuses a reconnect, got {reconnect:?}"
); );
assert!( assert!(
matches!( matches!(
@@ -1017,7 +1017,7 @@ async fn streamlocal_listen_serves_arrivals_and_cancels_through_the_unix_gate()
} }
/// - *Contract stamp:* OQ-11 (the runtime-Unix gate; universal /// - *Contract stamp:* OQ-11 (the runtime-Unix gate; universal
/// compile, clean off-Unix decline) + the module docs' gate record — /// compile, clean off-Unix decline) + the module docs' gate record:
/// off-Unix the unix listen declines **before any wire op** /// off-Unix the unix listen declines **before any wire op**
/// (`UnixUnavailable`; the platform-refused request consumes /// (`UnixUnavailable`; the platform-refused request consumes
/// nothing). /// nothing).
@@ -1044,7 +1044,7 @@ async fn streamlocal_declines_off_unix_before_any_wire_op() {
); );
assert!( assert!(
observed.listens().is_empty(), observed.listens().is_empty(),
"nothing rode the wire — the gate answered before the request" "nothing rode the wire; the gate answered before the request"
); );
expect_clean_close(connection, double).await; expect_clean_close(connection, double).await;
+11 -11
View File
@@ -5,13 +5,13 @@
//! module) against. The recorded test convention (see //! module) against. The recorded test convention (see
//! `tests/observe.rs`'s module docs) applies verbatim: no //! `tests/observe.rs`'s module docs) applies verbatim: no
//! `tracing-subscriber` dependency (the hand-rolled shape needs none), //! `tracing-subscriber` dependency (the hand-rolled shape needs none),
//! and the **global** default is the only capture shape that works — //! and the **global** default is the only capture shape that works
//! the server side runs on spawned tasks, so a thread-local default //! (the server side runs on spawned tasks, so a thread-local default
//! never sees their events. //! never sees their events.
//! //!
//! Rows install once per binary with [`install`], then match events by //! Rows install once per binary with [`install`], then match events by
//! field pairs ([`await_event`]) — parallel rows never order against //! field pairs ([`await_event`], so parallel rows never order against
//! each other — and pin the decline event's exact vocabulary with //! each other) and pin the decline event's exact vocabulary with
//! [`assert_request_decline`] (the exact-field-set assertion is the //! [`assert_request_decline`] (the exact-field-set assertion is the
//! safety posture: no dynamic extra fields exist). //! safety posture: no dynamic extra fields exist).
@@ -28,7 +28,7 @@ pub struct Captured {
} }
impl Captured { impl Captured {
/// The field's value by name — the rows' matching input. /// The field's value by name, the rows' matching input.
pub fn field(&self, name: &str) -> Option<&str> { pub fn field(&self, name: &str) -> Option<&str> {
self.fields self.fields
.iter() .iter()
@@ -89,7 +89,7 @@ impl tracing::Subscriber for GlobalCapture {
} }
} }
/// The installed capture — one per binary (the [`OnceLock`] makes the /// The installed capture, one per binary (the [`OnceLock`] makes the
/// exactly-once shape explicit). /// exactly-once shape explicit).
fn capture() -> &'static Arc<GlobalCapture> { fn capture() -> &'static Arc<GlobalCapture> {
static CAPTURE: OnceLock<Arc<GlobalCapture>> = OnceLock::new(); static CAPTURE: OnceLock<Arc<GlobalCapture>> = OnceLock::new();
@@ -108,7 +108,7 @@ pub fn install() {
let _ = capture(); let _ = capture();
} }
/// The non-blocking peek — `Some` when a matching event is already on /// The non-blocking peek; `Some` when a matching event is already on
/// record, `None` otherwise. The e2e rows poll this alongside their /// record, `None` otherwise. The e2e rows poll this alongside their
/// bounded curls (the awaiting rows' `await_event` stays fixed-window; /// bounded curls (the awaiting rows' `await_event` stays fixed-window;
/// a slow CI host turns the wait-then-window shape into a flake, the /// a slow CI host turns the wait-then-window shape into a flake, the
@@ -125,7 +125,7 @@ pub fn captured_with(want: &[(&str, &str)]) -> Option<Captured> {
} }
/// Waits until an event matching every field pair appears, then /// Waits until an event matching every field pair appears, then
/// returns it — the spawned-task surfaces land their events /// returns it (the spawned-task surfaces land their events
/// asynchronously (the deadline loop is the observe-suite convention). /// asynchronously (the deadline loop is the observe-suite convention).
pub async fn await_event(want: &[(&str, &str)]) -> Captured { pub async fn await_event(want: &[(&str, &str)]) -> Captured {
let capture = capture(); let capture = capture();
@@ -143,7 +143,7 @@ pub async fn await_event(want: &[(&str, &str)]) -> Captured {
} }
/// The expected `feature_build` value, derived from `cfg!` /// The expected `feature_build` value, derived from `cfg!`
/// independently of the crate's match — divergence breaks this loudly. /// independently of the crate's match; divergence breaks this loudly.
pub fn expected_build() -> &'static str { pub fn expected_build() -> &'static str {
match ( match (
cfg!(feature = "session"), cfg!(feature = "session"),
@@ -171,7 +171,7 @@ pub fn expected_build() -> &'static str {
} }
/// The request-decline vocabulary assertion: a captured request-level /// The request-decline vocabulary assertion: a captured request-level
/// decline carries exactly the vocabulary (sorted-name-set comparison — /// decline carries exactly the vocabulary (a sorted-name-set comparison,
/// nothing more; the dynamic-free shape is the safety posture), the /// nothing more; the dynamic-free shape is the safety posture), the
/// build self-describing, the message shape, and the `debug` level (a /// build self-describing, the message shape, and the `debug` level (a
/// decline is the door's normal, honest posture). `exec_route` present /// decline is the door's normal, honest posture). `exec_route` present
@@ -179,7 +179,7 @@ pub fn expected_build() -> &'static str {
pub fn assert_request_decline(event: &Captured, exec_route: bool) { pub fn assert_request_decline(event: &Captured, exec_route: bool) {
assert_eq!( assert_eq!(
event.level, "DEBUG", event.level, "DEBUG",
"a decline emits at debug — the vocabulary's level posture" "a decline emits at debug, the vocabulary's level posture"
); );
assert_eq!( assert_eq!(
event.field("feature_build"), event.field("feature_build"),
+11 -11
View File
@@ -1,21 +1,21 @@
//! alkssh's git-seam doubles (`w3-git-mapping`) — the hand-rolled //! alkssh's git-seam doubles (`w3-git-mapping`): the hand-rolled
//! in-memory alkgit backend quartet, shaped per //! in-memory alkgit backend quartet, shaped per
//! `alkgit/src/backend.rs:164-254`'s noop impls (the shapes that file //! `alkgit/src/backend.rs:164-254`'s noop impls (the shapes that file
//! itself pins for dyn compatibility), with recording accessors the //! itself pins for dyn compatibility), with recording accessors the
//! rows assert through. alkgit's own `Mem*` constructions are //! rows assert through. alkgit's own `Mem*` constructions are
//! test-private to that crate — this module hand-rolls the minimal //! test-private to that crate, so this module hand-rolls the minimal
//! surface the git rows need, never a sibling-checkout path, never a //! surface the git rows need, never a sibling-checkout path, never a
//! copy of alkgit's doubles' *code*. //! copy of alkgit's doubles' *code*.
//! //!
//! State/accessor split: the doubles are configurable per row (repo //! State/accessor split: the doubles are configurable per row (repo
//! records with visibility + grants; advertised ref lines) and //! records with visibility + grants; advertised ref lines) and
//! record every seam call the serving path makes (resolutions, //! record every seam call the serving path makes (resolutions,
//! applied CAS updates, prepared pushes' updates, generated packs) — //! applied CAS updates, prepared pushes' updates, generated packs);
//! the dispatch fidelity (which id was resolved, which updates were //! the dispatch fidelity (which id was resolved, which updates were
//! applied) is observable without alkgit's own test surface. //! applied) is observable without alkgit's own test surface.
//! //!
//! Feature gating: this module is `git`-gated at the include site //! Feature gating: this module is `git`-gated at the include site
//! (`tests/common/mod.rs`) — a non-git build compiles none of it, and //! (`tests/common/mod.rs`); a non-git build compiles none of it, and
//! alkgit types never appear outside. //! alkgit types never appear outside.
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
@@ -33,13 +33,13 @@ use async_trait::async_trait;
use gix_hash::ObjectId; use gix_hash::ObjectId;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
/// A test oid — deterministic bytes (the row-assertable hex shape). /// A test oid, deterministic bytes (the row-assertable hex shape).
pub fn oid(byte: u8) -> ObjectId { pub fn oid(byte: u8) -> ObjectId {
ObjectId::from([byte; 20]) ObjectId::from([byte; 20])
} }
/// One repo the registry double serves: grants keyed by the resolved /// One repo the registry double serves: grants keyed by the resolved
/// identity id (alkgit's `authorize` input — the identity-ride /// identity id (alkgit's `authorize` input, the identity-ride
/// witness), the advertised ref set the refs double lists. /// witness), the advertised ref set the refs double lists.
pub struct RepoSetup { pub struct RepoSetup {
pub repo_id: String, pub repo_id: String,
@@ -116,7 +116,7 @@ pub struct GeneratedPack {
/// One per-repo serve gate (the `w4-git-exit-close` row's /// One per-repo serve gate (the `w4-git-exit-close` row's
/// serve-timing control): the double's `GitRefs::list_refs` for the /// serve-timing control): the double's `GitRefs::list_refs` for the
/// gated repo id pends on it until [`MemGate::release`] — the serving /// gated repo id pends on it until [`MemGate::release`]; the serving
/// task stalls at that call with its channel's reader parked, so the /// task stalls at that call with its channel's reader parked, so the
/// row schedules the serve's later stages (the answer flood, the /// row schedules the serve's later stages (the answer flood, the
/// serve tail) against a prepared wire state. Race-safe by /// serve tail) against a prepared wire state. Race-safe by
@@ -152,7 +152,7 @@ impl MemGate {
} }
} }
/// The in-memory quartet: one struct, four trait impls — the /// The in-memory quartet: one struct, four trait impls; the
/// [`GitSeam`](alkssh::GitSeam) takes a separate `Arc` per backend /// [`GitSeam`](alkssh::GitSeam) takes a separate `Arc` per backend
/// trait and the rows share this state. /// trait and the rows share this state.
#[derive(Default)] #[derive(Default)]
@@ -199,7 +199,7 @@ impl MemGit {
self.generated.lock().clone() self.generated.lock().clone()
} }
/// Installs a serve gate for one repo id — [`GitRefs::list_refs`] /// Installs a serve gate for one repo id, so [`GitRefs::list_refs`]
/// calls for it pend until the returned gate's `release`. The /// calls for it pend until the returned gate's `release`. The
/// rows use the gate to prepare wire state (saturated queues, /// rows use the gate to prepare wire state (saturated queues,
/// blocked clients) for the serve's later stages. /// blocked clients) for the serve's later stages.
@@ -212,7 +212,7 @@ impl MemGit {
} }
} }
/// The record for a resolved id — the RepoRecord the seam hands the /// The record for a resolved id: the RepoRecord the seam hands the
/// backend traits. /// backend traits.
fn record(id: &str, repos: &RepoRegistry) -> Result<RepoRecord, RegistryError> { fn record(id: &str, repos: &RepoRegistry) -> Result<RepoRecord, RegistryError> {
match repos.get(id) { match repos.get(id) {
@@ -325,7 +325,7 @@ impl GitPackIngest for MemGit {
} }
/// Assembles the recording quartet into a /// Assembles the recording quartet into a
/// [`GitSeam`](alkssh::GitSeam) — one shared `Arc<MemGit>`, the /// [`GitSeam`](alkssh::GitSeam) taking one shared `Arc<MemGit>`, the
/// default alkgit budget, the push-options gate off (the v1 default). /// default alkgit budget, the push-options gate off (the v1 default).
pub fn git_seam(mem: &Arc<MemGit>) -> alkssh::GitSeam { pub fn git_seam(mem: &Arc<MemGit>) -> alkssh::GitSeam {
alkssh::GitSeam { alkssh::GitSeam {
+50 -50
View File
@@ -1,4 +1,4 @@
//! alkssh's loopback harness (`w2-loopback-harness`) — the in-process //! alkssh's loopback harness (`w2-loopback-harness`), the in-process
//! client/server pair that is *the* contract-test vehicle for the //! client/server pair that is *the* contract-test vehicle for the
//! crate: the wave-2 decline matrix, every wave-3 mapping's contract //! crate: the wave-2 decline matrix, every wave-3 mapping's contract
//! tests, and the client-direction smokes all drive the crate through //! tests, and the client-direction smokes all drive the crate through
@@ -8,7 +8,7 @@
//! the server-side [`SshAdapter::handle`](alkssh::SshAdapter::handle) //! the server-side [`SshAdapter::handle`](alkssh::SshAdapter::handle)
//! mount runs over a **tokio duplex pair** ([`tokio::io::duplex`], //! mount runs over a **tokio duplex pair** ([`tokio::io::duplex`],
//! [`DUPLEX_CAPACITY`] bytes each direction), and the client side //! [`DUPLEX_CAPACITY`] bytes each direction), and the client side
//! connects with [`russh::client::connect_stream`] — russh's real //! connects with [`russh::client::connect_stream`]; russh's real
//! codec, kex and transport run over the pair verbatim, so the wire //! codec, kex and transport run over the pair verbatim, so the wire
//! path is fully exercised. The one thing it deliberately does not //! path is fully exercised. The one thing it deliberately does not
//! carry is OS-socket semantics; TCP loopback (`127.0.0.1:0`) was the //! carry is OS-socket semantics; TCP loopback (`127.0.0.1:0`) was the
@@ -16,13 +16,13 @@
//! lifecycle, loopback timing, accept-loop flakiness under slow CI) //! lifecycle, loopback timing, accept-loop flakiness under slow CI)
//! buys only what the real-tool smokes of the mapping waves and the //! buys only what the real-tool smokes of the mapping waves and the
//! e2e wave already cover cheaply. The duplex pair is deterministic, //! e2e wave already cover cheaply. The duplex pair is deterministic,
//! needs no port, and cannot collide — the no-TCP-port-dependency //! needs no port, and cannot collide, the no-TCP-port-dependency
//! shape the acceptance criteria name. //! shape the acceptance criteria name.
//! //!
//! **Placement decision (recorded, the `w2-loopback-harness` //! **Placement decision (recorded, the `w2-loopback-harness`
//! counterpart of the wave-1 doubles' location decision):** the //! counterpart of the wave-1 doubles' location decision):** the
//! harness lives as a sibling module of the wave-1 doubles, under //! harness lives as a sibling module of the wave-1 doubles, under
//! `tests/common/`, reached with the same `mod common;` convention — //! `tests/common/`, reached with the same `mod common;` convention:
//! test surface, test-versioned freely, never a public API export //! test surface, test-versioned freely, never a public API export
//! (no re-export from `lib.rs`, no feature gate). Unlike the doubles //! (no re-export from `lib.rs`, no feature gate). Unlike the doubles
//! it is a *vehicle* convention, not just a seam: later waves' //! it is a *vehicle* convention, not just a seam: later waves'
@@ -37,7 +37,7 @@
//! `check_server_key` hook ([`HarnessClientHandler`]), and the //! `check_server_key` hook ([`HarnessClientHandler`]), and the
//! keepalive / wall-clock posture from [`alkssh::Limits`] //! keepalive / wall-clock posture from [`alkssh::Limits`]
//! ([`client_config`]). The permissive posture for routine rows is //! ([`client_config`]). The permissive posture for routine rows is
//! the double's `with_default(true)` — an explicit test choice; the //! the double's `with_default(true)`, an explicit test choice; the
//! accept-all posture exists only here, never as a crate built-in, //! accept-all posture exists only here, never as a crate built-in,
//! and the fail-closed rows construct `with_default(false)`. //! and the fail-closed rows construct `with_default(false)`.
//! //!
@@ -46,11 +46,11 @@
//! [`LOOPBACK_HOST`] and [`LOOPBACK_PORT`]; host-key policy rows pin //! [`LOOPBACK_HOST`] and [`LOOPBACK_PORT`]; host-key policy rows pin
//! rules against those values verbatim. The presented key's //! rules against those values verbatim. The presented key's
//! fingerprint is the crate's single fingerprint path //! fingerprint is the crate's single fingerprint path
//! ([`alkssh::fingerprint`]) — exactly the decision input the wave-4 //! ([`alkssh::fingerprint`]), exactly the decision input the wave-4
//! dial wiring will compute at russh's own hook, so this bridge is //! dial wiring will compute at russh's own hook, so this bridge is
//! that wiring's rehearsal. //! that wiring's rehearsal.
//! //!
//! Helpers stay thin — the russh client API is the vocabulary; these //! Helpers stay thin (the russh client API is the vocabulary); these
//! remove boilerplate (config assembly, mount/spawn, fixture-key //! remove boilerplate (config assembly, mount/spawn, fixture-key
//! plumbing, drain loops), never semantics: every observation is a //! plumbing, drain loops), never semantics: every observation is a
//! russh-verbatim shape ([`AuthResult`], [`Error`], //! russh-verbatim shape ([`AuthResult`], [`Error`],
@@ -60,7 +60,7 @@
//! landed the auth/session/exec/shell/subsystem vocabulary, and //! landed the auth/session/exec/shell/subsystem vocabulary, and
//! `w2-dispatch-decline-core` extended it with the decline-matrix //! `w2-dispatch-decline-core` extended it with the decline-matrix
//! vocabulary (the tunnel/x11 opens, the pty/env/x11/agent-forward //! vocabulary (the tunnel/x11 opens, the pty/env/x11/agent-forward
//! requests, the listen requests) — its recorded expectation that the //! requests, the listen requests); its recorded expectation that the
//! "session-request and exit-status/close helpers are exercised by //! "session-request and exit-status/close helpers are exercised by
//! construction this wave ... their live rows arrive with //! construction this wave ... their live rows arrive with
//! `w2-dispatch-decline-core`" is discharged by exactly that //! `w2-dispatch-decline-core`" is discharged by exactly that
@@ -86,20 +86,20 @@ use alkssh::{
use super::{FixtureKey, MemHostKeyPolicy, MemHostKeySource}; use super::{FixtureKey, MemHostKeyPolicy, MemHostKeySource};
/// The host label the loopback presents to the host-key policy — a /// The host label the loopback presents to the host-key policy: a
/// duplex pair has no endpoint, so every decision is labeled with /// duplex pair has no endpoint, so every decision is labeled with
/// this constant (see the module docs §Host identity). /// this constant (see the module docs §Host identity).
pub const LOOPBACK_HOST: &str = "loopback"; pub const LOOPBACK_HOST: &str = "loopback";
/// The port label for the loopback's presented host key — `0`, the /// The port label for the loopback's presented host key (`0`), the
/// no-port marker: the transport has no TCP port by design. /// no-port marker: the transport has no TCP port by design.
pub const LOOPBACK_PORT: u16 = 0; pub const LOOPBACK_PORT: u16 = 0;
/// Each duplex direction's buffer — the `w2-server-mount` wire-smoke /// Each duplex direction's buffer, the `w2-server-mount` wire-smoke
/// precedent's capacity, kept as the harness convention. /// precedent's capacity, kept as the harness convention.
pub const DUPLEX_CAPACITY: usize = 65_536; pub const DUPLEX_CAPACITY: usize = 65_536;
/// The harness's russh client config — the keepalive / wall-clock /// The harness's russh client config; the keepalive / wall-clock
/// posture from [`Limits`]: `keepalive_interval` / `keepalive_max` /// posture from [`Limits`]: `keepalive_interval` / `keepalive_max`
/// mapped verbatim, `inactivity_timeout` carried as always-set (the /// mapped verbatim, `inactivity_timeout` carried as always-set (the
/// crate never constructs a session with no wall clock, /// crate never constructs a session with no wall clock,
@@ -115,12 +115,12 @@ pub fn client_config(limits: Limits) -> Arc<russh::client::Config> {
} }
/// One server-initiated forwarded open the rows observe at the /// One server-initiated forwarded open the rows observe at the
/// harness client (`w3-tunnel-listen`'s accept-hook harness row — the /// harness client (`w3-tunnel-listen`'s accept-hook harness row; the
/// `-R` data leg arrives here). The `Channel` only carries data after /// `-R` data leg arrives here). The `Channel` only carries data after
/// the row accepts (the harness hook answers the open itself). /// the row accepts (the harness hook answers the open itself).
#[derive(Debug)] #[derive(Debug)]
pub enum ForwardedOpen { pub enum ForwardedOpen {
/// The `forwarded-tcpip` row — the echo args + originator the door /// The `forwarded-tcpip` row (the echo args + originator the door
/// opened with, verbatim as the wire carried them. /// opened with, verbatim as the wire carried them.
Tcpip { Tcpip {
connected_address: String, connected_address: String,
@@ -137,11 +137,11 @@ pub enum ForwardedOpen {
}, },
} }
/// The harness client's acceptor — what a vanilla client's matching /// The harness client's acceptor, what a vanilla client's matching
/// `-R` listener stands-in as: when installed, server-initiated /// `-R` listener stands-in as: when installed, server-initiated
/// forwarded opens are accepted and handed to the row through the /// forwarded opens are accepted and handed to the row through the
/// channel the acceptor was built with; **no acceptor installed → the /// channel the acceptor was built with; **no acceptor installed → the
/// open declines cleanly** (`AdministrativelyProhibited` — the /// open declines cleanly** (`AdministrativelyProhibited`; the
/// family's client-side contract, ADR-003 symmetry: a vanillla stand- /// family's client-side contract, ADR-003 symmetry: a vanillla stand-
/// in with no matching forward never accept-and-drops). /// in with no matching forward never accept-and-drops).
pub struct ForwardedAcceptor { pub struct ForwardedAcceptor {
@@ -166,7 +166,7 @@ impl ForwardedAcceptor {
/// The client-direction data gate (`w4-git-exit-close`'s row /// The client-direction data gate (`w4-git-exit-close`'s row
/// vehicle): armed on a channel id, the harness client's packet loop /// vehicle): armed on a channel id, the harness client's packet loop
/// pends inside its `data` hook for that channel's packets (the hook /// pends inside its `data` hook for that channel's packets (the hook
/// is awaited inline in russh's client loop) — the server's outbound /// is awaited inline in russh's client loop); the server's outbound
/// writes for the channel back up through the transport's duplex pair /// writes for the channel back up through the transport's duplex pair
/// into the server's message queue until [`DataGate::release`]. The /// into the server's message queue until [`DataGate::release`]. The
/// held counter (`DataGate::held_packets`) lets the row observe that /// held counter (`DataGate::held_packets`) lets the row observe that
@@ -179,7 +179,7 @@ pub struct DataGate {
} }
impl DataGate { impl DataGate {
/// Builds the inert gate — arm it with [`DataGate::arm`] once the /// Builds the inert gate; arm it with [`DataGate::arm`] once the
/// row has the channel id to gate. /// row has the channel id to gate.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -199,14 +199,14 @@ impl DataGate {
*self.release.lock() = Some(release); *self.release.lock() = Some(release);
} }
/// How many data packets have hit the armed gate so far — the /// How many data packets have hit the armed gate so far, the
/// row's freeze signal (count `>= 1` means the client's packet /// row's freeze signal (count `>= 1` means the client's packet
/// loop is pended inside the hook). /// loop is pended inside the hook).
pub fn held_packets(&self) -> usize { pub fn held_packets(&self) -> usize {
self.held.load(Ordering::Acquire) self.held.load(Ordering::Acquire)
} }
/// Releases the gate — the client's packet loop resumes and the /// Releases the gate; the client's packet loop resumes and the
/// backed-up server queues drain. /// backed-up server queues drain.
pub fn release(&self) { pub fn release(&self) {
*self.target.lock() = None; *self.target.lock() = None;
@@ -227,7 +227,7 @@ impl DataGate {
} }
} }
/// The harness's russh client handler — the [`MemHostKeyPolicy`] /// The harness's russh client handler, the [`MemHostKeyPolicy`]
/// decision double bridged into russh's `check_server_key` hook, with /// decision double bridged into russh's `check_server_key` hook, with
/// the decision input exactly as the wave-4 dial wiring will shape it /// the decision input exactly as the wave-4 dial wiring will shape it
/// ([`PresentedHostKey`] over the crate's fingerprint path), plus the /// ([`PresentedHostKey`] over the crate's fingerprint path), plus the
@@ -241,9 +241,9 @@ pub struct HarnessClientHandler {
} }
impl HarnessClientHandler { impl HarnessClientHandler {
/// Builds the handler over a policy double — `with_default(true)` /// Builds the handler over a policy double: `with_default(true)`
/// for the routine permissive posture, `with_default(false)` (or a /// for the routine permissive posture, `with_default(false)` (or a
/// pinned rule) for the fail-closed rows — with the forwarded /// pinned rule) for the fail-closed rows; with the forwarded
/// acceptor absent (opens decline cleanly; install one with /// acceptor absent (opens decline cleanly; install one with
/// [`HarnessClientHandler::with_forwarded_acceptor`]). /// [`HarnessClientHandler::with_forwarded_acceptor`]).
pub fn new(policy: Arc<MemHostKeyPolicy>) -> Self { pub fn new(policy: Arc<MemHostKeyPolicy>) -> Self {
@@ -279,7 +279,7 @@ impl russh::client::Handler for HarnessClientHandler {
/// The data gate's hook: when the gate is armed on this channel id /// The data gate's hook: when the gate is armed on this channel id
/// and not yet released, the client's packet loop pends inside it /// and not yet released, the client's packet loop pends inside it
/// (the hook is awaited inline) — the server's outbound writes /// (the hook is awaited inline), the server's outbound writes
/// back up through the transport into its message queue until the /// back up through the transport into its message queue until the
/// row releases. Inert otherwise; also the block counter the row /// row releases. Inert otherwise; also the block counter the row
/// polls to know the freeze is real. /// polls to know the freeze is real.
@@ -373,7 +373,7 @@ impl russh::client::Handler for HarnessClientHandler {
} }
} }
/// Assembles the server side from the wave-1 doubles — a /// Assembles the server side from the wave-1 doubles: a
/// [`ServeConfig`] over the given identity provider and fixture host /// [`ServeConfig`] over the given identity provider and fixture host
/// keys, with the session limits applied. /// keys, with the session limits applied.
pub fn serve_config( pub fn serve_config(
@@ -389,7 +389,7 @@ pub fn serve_config(
serve serve
} }
/// Constructs the adapter from the doubles — [`serve_config`] with the /// Constructs the adapter from the doubles ([`serve_config`] with the
/// default [`Limits`]; the decrypt-at-construction failure paths are /// default [`Limits`]; the decrypt-at-construction failure paths are
/// the construction tests' business (`w2-server-mount`), so a harness /// the construction tests' business (`w2-server-mount`), so a harness
/// row's construction failing is a bug, not an observation. /// row's construction failing is a bug, not an observation.
@@ -405,9 +405,9 @@ pub async fn adapter(
/// One driven loopback session: the mounted server task plus the /// One driven loopback session: the mounted server task plus the
/// connected client handle and its policy double. /// connected client handle and its policy double.
pub struct LoopbackSession { pub struct LoopbackSession {
/// The connected russh client — the helpers' driver. /// The connected russh client, the helpers' driver.
pub client: Handle<HarnessClientHandler>, pub client: Handle<HarnessClientHandler>,
/// The policy double the connection's host-key decision rode — /// The policy double the connection's host-key decision rode,
/// decision-log inspection for the rows. /// decision-log inspection for the rows.
pub host_key_policy: Arc<MemHostKeyPolicy>, pub host_key_policy: Arc<MemHostKeyPolicy>,
server_task: tokio::task::JoinHandle<Result<(), AdapterError>>, server_task: tokio::task::JoinHandle<Result<(), AdapterError>>,
@@ -416,7 +416,7 @@ pub struct LoopbackSession {
impl LoopbackSession { impl LoopbackSession {
/// Ends one loopback row cleanly: disconnects the client, waits /// Ends one loopback row cleanly: disconnects the client, waits
/// for the server task, and requires a clean session end on both /// for the server task, and requires a clean session end on both
/// sides — the harness's every-row termination posture. /// sides, the harness's every-row termination posture.
pub async fn unmount(self) { pub async fn unmount(self) {
self.client self.client
.disconnect(Disconnect::ByApplication, "row done", "en") .disconnect(Disconnect::ByApplication, "row done", "en")
@@ -432,7 +432,7 @@ impl LoopbackSession {
/// Drives one loopback session: mounts the adapter over a duplex pair /// Drives one loopback session: mounts the adapter over a duplex pair
/// (spawned to completion, the assembly's accept-loop shape), connects /// (spawned to completion, the assembly's accept-loop shape), connects
/// a harness client against it and returns the pair. The adapter is /// a harness client against it and returns the pair. The adapter is
/// consumed (the spawned task owns it) — build a fresh one per row; /// consumed (the spawned task owns it), so build a fresh one per row;
/// the doubles make that cheap. /// the doubles make that cheap.
/// ///
/// Fallible at the connect step on purpose: a fail-closed host-key /// Fallible at the connect step on purpose: a fail-closed host-key
@@ -446,7 +446,7 @@ pub async fn drive(
drive_with_acceptor(adapter, host_key_policy, limits, None).await drive_with_acceptor(adapter, host_key_policy, limits, None).await
} }
/// [`drive`] with the client-side forwarded acceptor installed — the /// [`drive`] with the client-side forwarded acceptor installed, the
/// `w3-tunnel-listen` rows' vehicle (the harness handler's accept /// `w3-tunnel-listen` rows' vehicle (the harness handler's accept
/// hook consumes the installed acceptor; `None` = the clean-decline /// hook consumes the installed acceptor; `None` = the clean-decline
/// posture [`drive`] carries). /// posture [`drive`] carries).
@@ -459,7 +459,7 @@ pub async fn drive_with_acceptor(
mount_and_connect(adapter, host_key_policy, limits, acceptor, None).await mount_and_connect(adapter, host_key_policy, limits, acceptor, None).await
} }
/// [`drive`] with the client-direction data gate installed — the /// [`drive`] with the client-direction data gate installed, the
/// `w4-git-exit-close` row's vehicle. /// `w4-git-exit-close` row's vehicle.
pub async fn drive_with_data_gate( pub async fn drive_with_data_gate(
adapter: SshAdapter, adapter: SshAdapter,
@@ -498,7 +498,7 @@ async fn mount_and_connect(
}) })
} }
/// Authenticates one fixture key — the auth helper: the fixture /// Authenticates one fixture key, the auth helper: the fixture
/// plumbing (`Arc` + `PrivateKeyWithHashAlg`) removed; the /// plumbing (`Arc` + `PrivateKeyWithHashAlg`) removed; the
/// [`AuthResult`] comes back verbatim for the success / failure-shape /// [`AuthResult`] comes back verbatim for the success / failure-shape
/// rows to match on. /// rows to match on.
@@ -515,7 +515,7 @@ pub async fn authenticate_publickey(
} }
/// The failure shape's advertisement half: the remaining-methods set /// The failure shape's advertisement half: the remaining-methods set
/// carried by an [`AuthResult::Failure`] — `None` when the result is /// carried by an [`AuthResult::Failure`]; `None` when the result is
/// a success (the rows' success/failure observation shape). /// a success (the rows' success/failure observation shape).
pub fn remaining_methods(result: &AuthResult) -> Option<MethodSet> { pub fn remaining_methods(result: &AuthResult) -> Option<MethodSet> {
match result { match result {
@@ -526,7 +526,7 @@ pub fn remaining_methods(result: &AuthResult) -> Option<MethodSet> {
} }
} }
/// Opens a session channel — the open helper. Success is a confirmed /// Opens a session channel, the open helper. Success is a confirmed
/// [`Channel`]; the open-failure shape is the `Err` verbatim /// [`Channel`]; the open-failure shape is the `Err` verbatim
/// ([`Error::ChannelOpenFailure`] carries the SSH decline reason the /// ([`Error::ChannelOpenFailure`] carries the SSH decline reason the
/// decline-matrix rows match on). /// decline-matrix rows match on).
@@ -536,7 +536,7 @@ pub async fn open_session_channel(
client.channel_open_session().await client.channel_open_session().await
} }
/// Opens a `direct-tcpip` channel — the tunnel open helper (`ssh -L` /// Opens a `direct-tcpip` channel, the tunnel open helper (`ssh -L`
/// / `ssh -D` traffic). The open-failure shape is the `Err` verbatim, /// / `ssh -D` traffic). The open-failure shape is the `Err` verbatim,
/// the decline-matrix rows' reason surface. /// the decline-matrix rows' reason surface.
pub async fn open_direct_tcpip( pub async fn open_direct_tcpip(
@@ -549,7 +549,7 @@ pub async fn open_direct_tcpip(
.await .await
} }
/// Opens a `direct-streamlocal` channel — the unix-socket forward /// Opens a `direct-streamlocal` channel, the unix-socket forward
/// helper. The open-failure shape is the `Err` verbatim. /// helper. The open-failure shape is the `Err` verbatim.
pub async fn open_direct_streamlocal( pub async fn open_direct_streamlocal(
client: &Handle<HarnessClientHandler>, client: &Handle<HarnessClientHandler>,
@@ -558,7 +558,7 @@ pub async fn open_direct_streamlocal(
client.channel_open_direct_streamlocal(socket_path).await client.channel_open_direct_streamlocal(socket_path).await
} }
/// Opens an x11 channel — the never-feature open helper (ADR-003 §5). /// Opens an x11 channel, the never-feature open helper (ADR-003 §5).
/// The open-failure shape is the `Err` verbatim. /// The open-failure shape is the `Err` verbatim.
pub async fn open_x11_channel( pub async fn open_x11_channel(
client: &Handle<HarnessClientHandler>, client: &Handle<HarnessClientHandler>,
@@ -570,7 +570,7 @@ pub async fn open_x11_channel(
.await .await
} }
/// Sends an exec request over a channel (`want_reply` set — the /// Sends an exec request over a channel (`want_reply` set; the
/// success/failure shape rides [`ChannelMsg::Success`]/[`ChannelMsg::Failure`] /// success/failure shape rides [`ChannelMsg::Success`]/[`ChannelMsg::Failure`]
/// through [`next_message`]). /// through [`next_message`]).
pub async fn request_exec(channel: &Channel<Msg>, command: &str) -> Result<(), Error> { pub async fn request_exec(channel: &Channel<Msg>, command: &str) -> Result<(), Error> {
@@ -587,19 +587,19 @@ pub async fn request_subsystem(channel: &Channel<Msg>, name: &str) -> Result<(),
channel.request_subsystem(true, name).await channel.request_subsystem(true, name).await
} }
/// Sends a pty request over a channel (`want_reply` set — the pty row /// Sends a pty request over a channel (`want_reply` set; the pty row
/// rides the standard success/failure observation). /// rides the standard success/failure observation).
pub async fn request_pty(channel: &Channel<Msg>) -> Result<(), Error> { pub async fn request_pty(channel: &Channel<Msg>) -> Result<(), Error> {
channel.request_pty(true, "xterm", 80, 24, 0, 0, &[]).await channel.request_pty(true, "xterm", 80, 24, 0, 0, &[]).await
} }
/// Sends an env request over a channel (`want_reply` set — the OQ-09 /// Sends an env request over a channel (`want_reply` set; the OQ-09
/// unconditional-decline rows' observation). /// unconditional-decline rows' observation).
pub async fn request_env(channel: &Channel<Msg>, name: &str, value: &str) -> Result<(), Error> { pub async fn request_env(channel: &Channel<Msg>, name: &str, value: &str) -> Result<(), Error> {
channel.set_env(true, name, value).await channel.set_env(true, name, value).await
} }
/// Sends an x11-req request over a channel (`want_reply` set — the /// Sends an x11-req request over a channel (`want_reply` set; the
/// never-feature request rows). /// never-feature request rows).
pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> { pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> {
channel channel
@@ -607,16 +607,16 @@ pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> {
.await .await
} }
/// Sends an agent-forwarding request over a channel (`want_reply` set /// Sends an agent-forwarding request over a channel (`want_reply`
/// — the never-feature request rows; ADR-003 §5). /// set, the never-feature request rows; ADR-003 §5).
pub async fn request_agent_forward(channel: &Channel<Msg>) -> Result<(), Error> { pub async fn request_agent_forward(channel: &Channel<Msg>) -> Result<(), Error> {
channel.agent_forward(true).await channel.agent_forward(true).await
} }
/// Requests a `tcpip-forward` listen (`-R` ingress) — the /// Requests a `tcpip-forward` listen (`-R` ingress); the
/// success/failure shape is the result verbatim /// success/failure shape is the result verbatim
/// ([`Error::RequestDenied`] on a declined listen; a `port = 0` /// ([`Error::RequestDenied`] on a declined listen; a `port = 0`
/// request's actually-bound number rides the success value — sshd /// request's actually-bound number rides the success value, sshd
/// parity). /// parity).
pub async fn tcpip_forward( pub async fn tcpip_forward(
client: &Handle<HarnessClientHandler>, client: &Handle<HarnessClientHandler>,
@@ -651,7 +651,7 @@ pub async fn cancel_streamlocal_forward(
client.cancel_streamlocal_forward(socket_path).await client.cancel_streamlocal_forward(socket_path).await
} }
/// Drains the acceptor's event channel — the row's observation shape /// Drains the acceptor's event channel, the row's observation shape
/// (`None` once the acceptor's sender went, i.e. after the session's /// (`None` once the acceptor's sender went, i.e. after the session's
/// end). /// end).
pub async fn next_forwarded_open( pub async fn next_forwarded_open(
@@ -660,13 +660,13 @@ pub async fn next_forwarded_open(
receiver.recv().await receiver.recv().await
} }
/// Observes the next channel message — `None` after the channel has /// Observes the next channel message; `None` after the channel has
/// closed (russh's EOF-of-conversation shape). /// closed (russh's EOF-of-conversation shape).
pub async fn next_message(channel: &mut Channel<Msg>) -> Option<ChannelMsg> { pub async fn next_message(channel: &mut Channel<Msg>) -> Option<ChannelMsg> {
channel.wait().await channel.wait().await
} }
/// Awaits the channel's exit status — drains interim messages until /// Awaits the channel's exit status, draining interim messages until
/// [`ChannelMsg::ExitStatus`] (the wave-3 derivation contract's /// [`ChannelMsg::ExitStatus`] (the wave-3 derivation contract's
/// observation shape) or the channel's end, which is `None`. /// observation shape) or the channel's end, which is `None`.
pub async fn await_exit_status(channel: &mut Channel<Msg>) -> Option<u32> { pub async fn await_exit_status(channel: &mut Channel<Msg>) -> Option<u32> {
@@ -679,7 +679,7 @@ pub async fn await_exit_status(channel: &mut Channel<Msg>) -> Option<u32> {
} }
} }
/// Awaits the channel's close — drains interim messages until /// Awaits the channel's close, draining interim messages until
/// [`ChannelMsg::Close`] (observed: `true`) or the channel's end /// [`ChannelMsg::Close`] (observed: `true`) or the channel's end
/// (`false`). /// (`false`).
pub async fn await_close(channel: &mut Channel<Msg>) -> bool { pub async fn await_close(channel: &mut Channel<Msg>) -> bool {
+46 -45
View File
@@ -1,4 +1,4 @@
//! alkssh's wave-1 test doubles — the in-memory seams every later //! alkssh's wave-1 test doubles, the in-memory seams every later
//! wave's contract tests run against (the alkgit `tests/common/` //! wave's contract tests run against (the alkgit `tests/common/`
//! convention, adapted): an in-memory `IdentityProvider` //! convention, adapted): an in-memory `IdentityProvider`
//! ([`MemIdentityProvider`]), the in-memory vault seam ([`TestVault`]), //! ([`MemIdentityProvider`]), the in-memory vault seam ([`TestVault`]),
@@ -6,8 +6,8 @@
//! ed25519 test-key fixtures ([`FIXTURE_KEYS`]). //! ed25519 test-key fixtures ([`FIXTURE_KEYS`]).
//! //!
//! **Location decision (recorded, the OQ-03 freeze-inventory //! **Location decision (recorded, the OQ-03 freeze-inventory
//! discipline):** the doubles live *only* here — a shared module under //! discipline):** the doubles live *only* here (a shared module under
//! `tests/` reached with `mod common;` — and never in the public API: //! `tests/` reached with `mod common;`) and never in the public API:
//! no re-export from `lib.rs`, no feature gate, nothing doubles-related //! no re-export from `lib.rs`, no feature gate, nothing doubles-related
//! reachable from the crate root. The doubles' shapes are test //! reachable from the crate root. The doubles' shapes are test
//! surface, test-versioned freely; the only `src/`-side consumers are //! surface, test-versioned freely; the only `src/`-side consumers are
@@ -15,7 +15,7 @@
//! `include_str!`, never a public export. //! `include_str!`, never a public export.
//! //!
//! **Vault double decision (recorded, flagged for the wave-1 gate):** //! **Vault double decision (recorded, flagged for the wave-1 gate):**
//! alkvault 0.1 ships an in-memory construction — //! alkvault 0.1 ships an in-memory construction:
//! `VaultServiceHandle::new()` is an in-process vault that //! `VaultServiceHandle::new()` is an in-process vault that
//! `unlock_new` arms in-memory. The double is therefore a *wrapper* //! `unlock_new` arms in-memory. The double is therefore a *wrapper*
//! ([`TestVault`]) around that construction, not a local re-impl //! ([`TestVault`]) around that construction, not a local re-impl
@@ -26,7 +26,7 @@
//! //!
//! **Fixture decision (recorded):** committed static OpenSSH-PEM test //! **Fixture decision (recorded):** committed static OpenSSH-PEM test
//! keys, minted by `ssh-keygen` at vector-generation time (the //! keys, minted by `ssh-keygen` at vector-generation time (the
//! independent issuer), never generated at test time — determinism and //! independent issuer), never generated at test time; determinism and
//! no tooling dependency. The committed private halves are throwaway //! no tooling dependency. The committed private halves are throwaway
//! fixtures, not secrets: minted for this suite only, never registered //! fixtures, not secrets: minted for this suite only, never registered
//! anywhere real, never deployed. Each key's fingerprint is recorded //! anywhere real, never deployed. Each key's fingerprint is recorded
@@ -36,43 +36,43 @@
//! asserts parity against them. //! asserts parity against them.
//! //!
//! Each test binary's `mod common;` compiles the whole shared module //! Each test binary's `mod common;` compiles the whole shared module
//! and no single binary exercises every double — dead_code is allowed //! and no single binary exercises every double, so dead_code is allowed
//! module-wide so one binary's suite never fails the next's. //! module-wide so one binary's suite never fails the next's.
//! //!
//! The row/stamp discipline (adapted from the sibling crates): test //! The row/stamp discipline (adapted from the sibling crates): test
//! functions are *rows* — the name states the property, not the //! functions are *rows* (the name states the property, not the
//! caller — and each carries a doc-comment list item beginning //! caller), and each carries a doc-comment list item beginning
//! **`Contract stamp:`** citing the pinning spec §/ADR § (never a bare //! **`Contract stamp:`** citing the pinning spec §/ADR § (never a bare
//! wave or date). The marker is greppable: //! wave or date). The marker is greppable:
//! `grep -rn "Contract stamp:" tests/` is the inventory. //! `grep -rn "Contract stamp:" tests/` is the inventory.
#![allow(dead_code)] #![allow(dead_code)]
/// The loopback harness — *the* contract-test vehicle (its own /// The loopback harness, *the* contract-test vehicle (its own
/// location/transport decisions live in its module docs: /// location/transport decisions live in its module docs:
/// `tests/common/harness.rs`). /// `tests/common/harness.rs`).
pub mod harness; pub mod harness;
/// The in-process fake `sshd` (`w4-dial`) — the *client-direction* /// The in-process fake `sshd` (`w4-dial`), the *client-direction*
/// contract vehicle (the dial surface, the wave-4 client chain; its /// contract vehicle (the dial surface, the wave-4 client chain; its
/// own placement/shape decisions live in the module docs: /// own placement/shape decisions live in the module docs:
/// `tests/common/sshd_double.rs`). /// `tests/common/sshd_double.rs`).
pub mod sshd_double; pub mod sshd_double;
/// The git-seam doubles (`w3-git-mapping`, `git`-gated — alkssh's /// The git-seam doubles (`w3-git-mapping`, `git`-gated: alkssh's
/// hand-rolled minimal alkgit quartet per `alkgit/src/backend.rs`'s /// hand-rolled minimal alkgit quartet per `alkgit/src/backend.rs`'s
/// noop shapes; the module's own doc records the decisions: /// noop shapes; the module's own doc records the decisions:
/// `tests/common/git_doubles.rs`). /// `tests/common/git_doubles.rs`).
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub mod git_doubles; pub mod git_doubles;
/// The session-seam doubles (`w3-tty-session`, `session`-gated — the /// The session-seam doubles (`w3-tty-session`, `session`-gated; the
/// hand-rolled TtyBackend shim + ownership witness; the module's own /// hand-rolled TtyBackend shim + ownership witness; the module's own
/// doc records the decisions: `tests/common/tty_doubles.rs`). /// doc records the decisions: `tests/common/tty_doubles.rs`).
#[cfg(feature = "session")] #[cfg(feature = "session")]
pub mod tty_doubles; pub mod tty_doubles;
/// The decline-row tracing capture (`w4-channel-not-held-rows`) — the /// The decline-row tracing capture (`w4-channel-not-held-rows`); the
/// mapping binaries' decline-class rows assert the event vocabulary /// mapping binaries' decline-class rows assert the event vocabulary
/// through it (the module's own doc records the convention: /// through it (the module's own doc records the convention:
/// `tests/common/event_capture.rs`). /// `tests/common/event_capture.rs`).
@@ -92,7 +92,8 @@ use alkssh::{HostKeyPolicy, HostKeySource, KeySourceError, PresentedHostKey};
/// (`tests/vectors/<name>.pub`), and the fingerprint recorded /// (`tests/vectors/<name>.pub`), and the fingerprint recorded
/// byte-verbatim from `ssh-keygen -lf` (`tests/vectors/fingerprints.txt`). /// byte-verbatim from `ssh-keygen -lf` (`tests/vectors/fingerprints.txt`).
/// ///
/// The `pem`/`blob`/`fingerprint` values are committed constants — throwaway /// The `pem`/`blob`/`fingerprint` values are committed constants,
/// throwaway
/// fixture material, never registered or deployed anywhere real. /// fixture material, never registered or deployed anywhere real.
#[derive(Debug)] #[derive(Debug)]
pub struct FixtureKey { pub struct FixtureKey {
@@ -101,10 +102,10 @@ pub struct FixtureKey {
/// The OpenSSH private-key PEM, included verbatim from the /// The OpenSSH private-key PEM, included verbatim from the
/// committed fixture file. /// committed fixture file.
pub pem: &'static str, pub pem: &'static str,
/// The public key's base64 blob — the OpenSSH one-line encoding, /// The public key's base64 blob: the OpenSSH one-line encoding,
/// `ssh-ed25519 <blob>` stripped of the key-type prefix and comment. /// `ssh-ed25519 <blob>` stripped of the key-type prefix and comment.
pub blob: &'static str, pub blob: &'static str,
/// The fingerprint recorded from `ssh-keygen -lf` — the /// The fingerprint recorded from `ssh-keygen -lf`, the
/// independent-issuer value every comparison in the suite asserts /// independent-issuer value every comparison in the suite asserts
/// parity against (ADR-004 decision 1's contract invariant). /// parity against (ADR-004 decision 1's contract invariant).
pub fingerprint: &'static str, pub fingerprint: &'static str,
@@ -116,7 +117,7 @@ impl FixtureKey {
parse_public_key_base64(self.blob).expect("committed fixture blob parses") parse_public_key_base64(self.blob).expect("committed fixture blob parses")
} }
/// The decoded private key — `decode_secret_key(pem, None)`, the /// The decoded private key, `decode_secret_key(pem, None)`, the
/// ADR-005 decision 2 conversion (the fixtures' inner format is /// ADR-005 decision 2 conversion (the fixtures' inner format is
/// exactly the at-rest contract: unencrypted OpenSSH PEM). /// exactly the at-rest contract: unencrypted OpenSSH PEM).
pub fn private_key(&self) -> PrivateKey { pub fn private_key(&self) -> PrivateKey {
@@ -155,7 +156,7 @@ pub const FIXTURE_KEYS: &[FixtureKey] = &[
}, },
]; ];
/// Looks a fixture up by name — `fixture("alpha")` is the canonical /// Looks a fixture up by name; `fixture("alpha")` is the canonical
/// client-dial key. /// client-dial key.
pub fn fixture(name: &str) -> &'static FixtureKey { pub fn fixture(name: &str) -> &'static FixtureKey {
FIXTURE_KEYS FIXTURE_KEYS
@@ -165,7 +166,7 @@ pub fn fixture(name: &str) -> &'static FixtureKey {
} }
/// Builds an [`Identity`] for registration (`id` + scopes; the /// Builds an [`Identity`] for registration (`id` + scopes; the
/// `resources` map is empty — the wire layer only needs the resolved /// `resources` map is empty; the wire layer only needs the resolved
/// identity, richer ACL shapes are the assembly's). /// identity, richer ACL shapes are the assembly's).
pub fn identity(id: &str, scopes: &[&str]) -> Identity { pub fn identity(id: &str, scopes: &[&str]) -> Identity {
Identity { Identity {
@@ -175,7 +176,7 @@ pub fn identity(id: &str, scopes: &[&str]) -> Identity {
} }
} }
/// The in-memory [`IdentityProvider`] — registered fingerprints → /// The in-memory [`IdentityProvider`]: registered fingerprints →
/// [`Identity`] resolutions over alkcall's trait verbatim. /// [`Identity`] resolutions over alkcall's trait verbatim.
/// ///
/// The double the wave-2 auth bridge and dispatch tests resolve /// The double the wave-2 auth bridge and dispatch tests resolve
@@ -187,7 +188,7 @@ pub struct MemIdentityProvider {
} }
impl MemIdentityProvider { impl MemIdentityProvider {
/// An empty registry — nothing resolves until registered /// An empty registry; nothing resolves until registered
/// (fail-closed: unregistered fingerprints decline). /// (fail-closed: unregistered fingerprints decline).
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -202,12 +203,12 @@ impl MemIdentityProvider {
.insert(fingerprint.to_string(), identity); .insert(fingerprint.to_string(), identity);
} }
/// Removes a registration — the post-resolution-revoke shape. /// Removes a registration, the post-resolution-revoke shape.
pub fn revoke(&self, fingerprint: &str) { pub fn revoke(&self, fingerprint: &str) {
self.identities.write().remove(fingerprint); self.identities.write().remove(fingerprint);
} }
/// The registered fingerprints — inspection for test assertions. /// The registered fingerprints, inspection for test assertions.
pub fn registered_fingerprints(&self) -> Vec<String> { pub fn registered_fingerprints(&self) -> Vec<String> {
let mut keys = self let mut keys = self
.identities .identities
@@ -227,37 +228,37 @@ impl Default for MemIdentityProvider {
} }
impl IdentityProvider for MemIdentityProvider { impl IdentityProvider for MemIdentityProvider {
/// The fingerprint path — ADR-004 decision 1's single resolution /// The fingerprint path, ADR-004 decision 1's single resolution
/// shape: exact string equality against registered values. /// shape: exact string equality against registered values.
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> { fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
self.identities.read().get(fingerprint).cloned() self.identities.read().get(fingerprint).cloned()
} }
/// The token path resolves nothing in the SSH direction — /// The token path resolves nothing in the SSH direction,
/// ADR-004's dispositions are fingerprint-only on this surface /// since ADR-004's dispositions are fingerprint-only on this surface
/// (`auth-bridge.md`); the double never invents a token shape. /// (`auth-bridge.md`); the double never invents a token shape.
fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> { fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
None None
} }
} }
/// The in-memory vault seam — a wrapper over alkvault 0.1's own /// The in-memory vault seam, a wrapper over alkvault 0.1's own
/// in-memory construction ([`VaultServiceHandle`], unlocked at /// in-memory construction ([`VaultServiceHandle`], unlocked at
/// construction via `unlock_new`). /// construction via `unlock_new`).
/// ///
/// The locked/absent/tampered/mismatch failure postures (POC-2 §3) are /// The locked/absent/tampered/mismatch failure postures (POC-2 §3) are
/// exercised *through the real production seams* the source traits /// exercised *through the real production seams* the source traits
/// call, against this handle — the double exists so tests never touch /// call, against this handle; the double exists so tests never touch
/// vault lifecycle decisions themselves: [`TestVault::unlocked`] arms /// vault lifecycle decisions themselves: [`TestVault::unlocked`] arms
/// it, `lock` closes it, and the sources keep refusing, never /// it, `lock` closes it, and the sources keep refusing, never
/// retrying (the crate never unlocks — ADR-005's separation). /// retrying (the crate never unlocks, per ADR-005's separation).
#[derive(Clone)] #[derive(Clone)]
pub struct TestVault { pub struct TestVault {
handle: VaultServiceHandle, handle: VaultServiceHandle,
} }
impl TestVault { impl TestVault {
/// A freshly armed in-memory vault — unlocked, ready to encrypt. /// A freshly armed in-memory vault, unlocked and ready to encrypt.
pub fn unlocked() -> Self { pub fn unlocked() -> Self {
let handle = VaultServiceHandle::new(); let handle = VaultServiceHandle::new();
handle handle
@@ -266,13 +267,13 @@ impl TestVault {
Self { handle } Self { handle }
} }
/// The vault handle the production sources consume — clones share /// The vault handle the production sources consume; clones share
/// the same vault state (locking through one is visible to all). /// the same vault state (locking through one is visible to all).
pub fn handle(&self) -> VaultServiceHandle { pub fn handle(&self) -> VaultServiceHandle {
self.handle.clone() self.handle.clone()
} }
/// Seals one fixture key's PEM into an at-rest record — the /// Seals one fixture key's PEM into an at-rest record (the
/// ADR-005 inner-format contract (unencrypted OpenSSH PEM in an /// ADR-005 inner-format contract (unencrypted OpenSSH PEM in an
/// `EncryptedData` record). /// `EncryptedData` record).
pub fn seal(&self, key: &FixtureKey) -> EncryptedData { pub fn seal(&self, key: &FixtureKey) -> EncryptedData {
@@ -287,7 +288,7 @@ impl TestVault {
.expect("in-memory vault encrypts the plaintext") .expect("in-memory vault encrypts the plaintext")
} }
/// Locks the vault — the locked-vault posture (POC-2 §3, the /// Locks the vault, the locked-vault posture (POC-2 §3, the
/// `KeySourceError::Locked` refusal class). /// `KeySourceError::Locked` refusal class).
pub fn lock(&self) { pub fn lock(&self) {
self.handle.lock(); self.handle.lock();
@@ -299,14 +300,14 @@ impl TestVault {
} }
} }
/// The in-memory [`HostKeySource`] double — a static decrypted key set /// The in-memory [`HostKeySource`] double, a static decrypted key set
/// (wave-2 addition): the wire-shaped rows construct a real /// (wave-2 addition): the wire-shaped rows construct a real
/// `SshAdapter`, whose construction wants a non-empty host-key set, and /// `SshAdapter`, whose construction wants a non-empty host-key set, and
/// the direct-hook rows want a source that is never loaded (the handler /// the direct-hook rows want a source that is never loaded (the handler
/// construction loads nothing — host keys are the russh `Config`'s /// construction loads nothing; host keys are the russh `Config`'s
/// business, built in [`alkssh::SshAdapter::new`]). /// business, built in [`alkssh::SshAdapter::new`]).
/// ///
/// `absent()` is the empty-set shape — its load refuses with /// `absent()` is the empty-set shape; its load refuses with
/// [`KeySourceError::Absent`] (the fail-closed contract), so any row /// [`KeySourceError::Absent`] (the fail-closed contract), so any row
/// that accidentally loads an absent double fails loudly. /// that accidentally loads an absent double fails loudly.
pub struct MemHostKeySource { pub struct MemHostKeySource {
@@ -319,7 +320,7 @@ impl MemHostKeySource {
Self { keys: Vec::new() } Self { keys: Vec::new() }
} }
/// A static decrypted set from the fixture keys — the wire rows' /// A static decrypted set from the fixture keys, the wire rows'
/// host-key set (e.g. `["bravo"]`, the rotation-pair member). /// host-key set (e.g. `["bravo"]`, the rotation-pair member).
pub fn from_fixtures(keys: &[&'static FixtureKey]) -> Self { pub fn from_fixtures(keys: &[&'static FixtureKey]) -> Self {
Self { Self {
@@ -339,12 +340,12 @@ impl HostKeySource for MemHostKeySource {
} }
} }
/// The in-memory [`HostKeyPolicy`] — a configurable verdict double with /// The in-memory [`HostKeyPolicy`], a configurable verdict double with
/// an inspectable decision log, for the wave-4 dial tests (and the /// an inspectable decision log, for the wave-4 dial tests (and the
/// case-1/case-2 rows of every host-key contract test). /// case-1/case-2 rows of every host-key contract test).
/// ///
/// Verdicts: a rules list of exact `(host, fingerprint)` matches — /// Verdicts: a rules list of exact `(host, fingerprint)` matches;
/// last matching rule wins — falling back to a configurable /// the last matching rule wins, falling back to a configurable
/// default verdict for unlisted pairs. The default of the /// default verdict for unlisted pairs. The default of the
/// **default-constructed double is reject** (the crate's fail-closed /// **default-constructed double is reject** (the crate's fail-closed
/// posture, `OQ-05`); permissive verdicts are the test's explicit /// posture, `OQ-05`); permissive verdicts are the test's explicit
@@ -363,7 +364,7 @@ struct Rule {
accept: bool, accept: bool,
} }
/// One recorded host-key decision — the presented key and the verdict /// One recorded host-key decision: the presented key and the verdict
/// the policy returned. /// the policy returned.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decision { pub struct Decision {
@@ -380,7 +381,7 @@ impl MemHostKeyPolicy {
Self::with_default(false) Self::with_default(false)
} }
/// A decision double with an explicit default verdict — the /// A decision double with an explicit default verdict (the
/// case-2 permissive posture (`accepting` doubles) is the test's /// case-2 permissive posture (`accepting` doubles) is the test's
/// explicit choice, never a default. /// explicit choice, never a default.
pub fn with_default(default_verdict: bool) -> Self { pub fn with_default(default_verdict: bool) -> Self {
@@ -401,13 +402,13 @@ impl MemHostKeyPolicy {
}); });
} }
/// Pins an accept rule for one `(host, fingerprint)` — the case-1 /// Pins an accept rule for one `(host, fingerprint)`, the case-1
/// trust-anchor shape (hard accept on exact match). /// trust-anchor shape (hard accept on exact match).
pub fn pin(&self, host: &str, fingerprint: &str) { pub fn pin(&self, host: &str, fingerprint: &str) {
self.rule(host, fingerprint, true); self.rule(host, fingerprint, true);
} }
/// The decision log — every [`Decision`] in call order. /// The decision log, every [`Decision`] in call order.
pub fn decisions(&self) -> Vec<Decision> { pub fn decisions(&self) -> Vec<Decision> {
self.log.lock().clone() self.log.lock().clone()
} }
@@ -440,7 +441,7 @@ impl HostKeyPolicy for MemHostKeyPolicy {
/// The leg-2 rows' tracing visibility (`w3-tunnel-listen`): a minimal /// The leg-2 rows' tracing visibility (`w3-tunnel-listen`): a minimal
/// hand-rolled global [`tracing::Subscriber`] printing every event's /// hand-rolled global [`tracing::Subscriber`] printing every event's
/// field pairs to stderr — the same hand-rolled convention the /// field pairs to stderr, the same hand-rolled convention the
/// crate-side vocabulary rows use (no `tracing-subscriber` /// crate-side vocabulary rows use (no `tracing-subscriber`
/// dependency; events fire from spawned tasks on both sides of the /// dependency; events fire from spawned tasks on both sides of the
/// fabric, so the global subscriber is the only capture shape that /// fabric, so the global subscriber is the only capture shape that
+54 -54
View File
@@ -1,9 +1,9 @@
//! The in-process fake `sshd` (`w4-dial`) — the **client-direction** //! The in-process fake `sshd` (`w4-dial`), the **client-direction**
//! contract-test vehicle: a hand-rolled russh server handler standing //! contract-test vehicle: a hand-rolled russh server handler standing
//! in for the vanilla `sshd` every wave-4 dial row is pointed at (the //! in for the vanilla `sshd` every wave-4 dial row is pointed at (the
//! reverse-runner shape). The alkssh loopback harness //! reverse-runner shape). The alkssh loopback harness
//! ([`super::harness`]) drives the *server* direction and is not the //! ([`super::harness`]) drives the *server* direction and is not the
//! vehicle here — the client dials *vanilla* servers, so this double //! vehicle here (the client dials *vanilla* servers, so this double
//! is a plain russh server (no alkssh in its serving path), on a real //! is a plain russh server (no alkssh in its serving path), on a real
//! loopback TCP listener: a dial target is an address the dial //! loopback TCP listener: a dial target is an address the dial
//! surface resolves and connects to, which a duplex pair has no //! surface resolves and connects to, which a duplex pair has no
@@ -11,7 +11,7 @@
//! //!
//! **Shape decisions (recorded, the task's placement item):** //! **Shape decisions (recorded, the task's placement item):**
//! //!
//! - **Where**: sibling of the wave-1 doubles under `tests/common/` — //! - **Where**: sibling of the wave-1 doubles under `tests/common/`;
//! the module-placing convention (test surface, test-versioned //! the module-placing convention (test surface, test-versioned
//! freely, no public API export, no feature gate; the client //! freely, no public API export, no feature gate; the client
//! handler skeleton it exercises compiles in every build). //! handler skeleton it exercises compiles in every build).
@@ -20,45 +20,45 @@
//! completion on its own task, and loops (a row never re-dials the //! completion on its own task, and loops (a row never re-dials the
//! same double). //! same double).
//! - **Identity**: the double resolves fingerprints against an //! - **Identity**: the double resolves fingerprints against an
//! injected accept set ([`SshdAccepts`], exact-string equality — //! injected accept set ([`SshdAccepts`]; exact-string equality,
//! the family's only resolution shape, ADR-004 decision 1, the //! the family's only resolution shape, ADR-004 decision 1, the
//! vanilla-parity stand-in for a real `sshd`'s //! vanilla-parity stand-in for a real `sshd`'s
//! `authorized_keys`). Every other auth hook is an explicit reject, //! `authorized_keys`). Every other auth hook is an explicit reject,
//! never an inherited default (the ADR-004 decision 3 hygiene). //! never an inherited default (the ADR-004 decision 3 hygiene).
//! - **Host keys**: the fixture host-key pair rides the //! - **Host keys**: the fixture host-key pair rides the
//! `russh::server::Config`; the presented fingerprint is the //! `russh::server::Config`; the presented fingerprint is the
//! crate's single fingerprint path value — the rows' pin rules //! crate's single fingerprint path value (the rows' pin rules match on
//! match on `fixture("bravo")`'s recorded value. //! match on `fixture("bravo")`'s recorded value.
//! - **The session handle**: the double's `auth_succeeded` hook hands //! - **The session handle**: the double's `auth_succeeded` hook hands
//! the per-connection [`russh::server::Handle`] to the row through //! the per-connection [`russh::server::Handle`] to the row through
//! a one-shot carrier ([`SshdDouble::session_handle`]) — the row's //! a one-shot carrier ([`SshdDouble::session_handle`]), the row's
//! initiator for the server-initiated-open decline rows (server → //! initiator for the server-initiated-open decline rows (server →
//! client `channel_open_*`; the dialed client's skeleton declines; //! client `channel_open_*`; the dialed client's skeleton declines;
//! the server's open call observes //! the server's open call observes
//! `Err(ChannelOpenFailure(..))` — the wire rows' observable). //! `Err(ChannelOpenFailure(..))`, the wire rows' observable).
//! - **The session handle**: the double's `auth_succeeded` hook hands //! - **The session handle**: the double's `auth_succeeded` hook hands
//! the per-connection [`russh::server::Handle`] to the row through //! the per-connection [`russh::server::Handle`] to the row through
//! a one-shot carrier ([`SshdDouble::session_handle`]) — the row's //! a one-shot carrier ([`SshdDouble::session_handle`]), the row's
//! initiator for the server-initiated-open decline rows (server → //! initiator for the server-initiated-open decline rows (server →
//! client `channel_open_*`; the dialed client's skeleton declines; //! client `channel_open_*`; the dialed client's skeleton declines;
//! the server's open call observes //! the server's open call observes
//! `Err(ChannelOpenFailure(..))` — the wire rows' observable). //! `Err(ChannelOpenFailure(..))`, the wire rows' observable).
//! - **The acceptor stub**: rows that need a served session channel //! - **The acceptor stub**: rows that need a served session channel
//! (the stays-usable-after-declines row here; the op-row vehicles //! (the stays-usable-after-declines row here; the op-row vehicles
//! later) arm the double to accept client session opens and hand //! later) arm the double to accept client session opens and hand
//! the channel out ([`SshdDouble::next_opened_session`]). //! the channel out ([`SshdDouble::next_opened_session`]).
//! - **The op-row script** (`w4-op-rows`): the double's //! - **The op-row script** (`w4-op-rows`): the double's
//! request/open surfaces grow a small script — the session //! request/open surfaces grow a small script: the session
//! requests' reply (success/failure), the scripted stdout/stderr //! requests' reply (success/failure), the scripted stdout/stderr
//! data + exit report a launched program would produce, and the //! data + exit report a launched program would produce, and the
//! direct opens' echo-or-reject behavior — plus an observation //! direct opens' echo-or-reject behavior, plus an observation
//! surface (the recorded commands/pty params/window //! surface (the recorded commands/pty params/window
//! changes/signals the client rows assert the wire verbatim //! changes/signals the client rows assert the wire verbatim
//! against). The double stays vanilla-parity: a hand-rolled //! against). The double stays vanilla-parity: a hand-rolled
//! responder standing in for `sshd`, nothing alkssh-typed inside. //! responder standing in for `sshd`, nothing alkssh-typed inside.
//! //!
//! - **The `-R` server half** (`w4-reverse-listen`): the double grows //! - **The `-R` server half** (`w4-reverse-listen`): the double grows
//! the reverse-forward surfaces a vanilla `sshd` really serves — the //! the reverse-forward surfaces a vanilla `sshd` really serves: the
//! `tcpip-forward` / `streamlocal-forward` global-request hooks //! `tcpip-forward` / `streamlocal-forward` global-request hooks
//! (scripted accept/deny; an accept **binds a real socket** in //! (scripted accept/deny; an accept **binds a real socket** in
//! sshd's role, so a `port = 0` request's minted bound port is the //! sshd's role, so a `port = 0` request's minted bound port is the
@@ -68,10 +68,10 @@
//! toward the client echoing the listen's own args + the //! toward the client echoing the listen's own args + the
//! connection's peer as the originator, then the bytes pumped //! connection's peer as the originator, then the bytes pumped
//! bidirectionally; a client-declined open closes the accepted //! bidirectionally; a client-declined open closes the accepted
//! connection — vanilla parity), and the cancel forms removing the //! connection, vanilla parity), and the cancel forms removing the
//! matching entry (tearing the real listener down; an unknown //! matching entry (tearing the real listener down; an unknown
//! listener denies, sshd parity). The bookkeeping is per-connection //! listener denies, sshd parity). The bookkeeping is per-connection
//! handler state and bounded by the scripted listens alone — the //! handler state and bounded by the scripted listens alone (the
//! double is a scripted stand-in, not a real server: at most one //! double is a scripted stand-in, not a real server: at most one
//! forward per scripted accept is what the rows drive. //! forward per scripted accept is what the rows drive.
@@ -85,13 +85,13 @@ use russh::keys::PrivateKey;
use russh::server::{Auth, Handle, Msg, PublicKeyOffer, Response, Session, VerifiedPublicKey}; use russh::server::{Auth, Handle, Msg, PublicKeyOffer, Response, Session, VerifiedPublicKey};
use russh::{Channel, ChannelOpenFailure, MethodKind, MethodSet}; use russh::{Channel, ChannelOpenFailure, MethodKind, MethodSet};
/// The double's scripted exit report — what a launched program's /// The double's scripted exit report (what a launched program's
/// death would produce (`exit-status` / `exit-signal`). /// death would produce: `exit-status` / `exit-signal`).
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum DblExit { pub enum DblExit {
/// `exit-status(N)`. /// `exit-status(N)`.
Status(u32), Status(u32),
/// `exit-signal(name)` — the wire's signal-name class. /// `exit-signal(name)`, the wire's signal-name class.
Signal(&'static str), Signal(&'static str),
} }
@@ -99,7 +99,7 @@ pub enum DblExit {
/// (`direct-tcpip` / `direct-streamlocal`). /// (`direct-tcpip` / `direct-streamlocal`).
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum DblDirect { pub enum DblDirect {
/// Reject the open — the scripted server-refusal class /// Reject the open: the scripted server-refusal class
/// (`ConnectFailed`, the vanilla sshd's dead-target shape). /// (`ConnectFailed`, the vanilla sshd's dead-target shape).
Reject, Reject,
/// Accept the open and echo-pump the channel (the pump rows' /// Accept the open and echo-pump the channel (the pump rows'
@@ -108,28 +108,28 @@ pub enum DblDirect {
} }
/// The double's scripted behavior for the client's reverse-forward /// The double's scripted behavior for the client's reverse-forward
/// listen requests (`tcpip-forward` / `streamlocal-forward@…) — the /// listen requests (`tcpip-forward` / `streamlocal-forward@…`), the
/// `-R` rows' scripted class (`w4-reverse-listen`). /// `-R` rows' scripted class (`w4-reverse-listen`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DblListen { pub enum DblListen {
/// Deny the listen — the vanilla sshd's refusal (the scripted /// Deny the listen; the vanilla sshd's refusal (the scripted
/// server-refusal class; the wire `RequestFailure` the client's /// server-refusal class; the wire `RequestFailure` the client's
/// op row mirrors). The script's default (fail-closed double). /// op row mirrors). The script's default (fail-closed double).
#[default] #[default]
Deny, Deny,
/// Accept the listen: the double binds a **real socket** in /// Accept the listen: the double binds a **real socket** in
/// sshd's role (the actually-bound port rides the reply's mint — /// sshd's role (the actually-bound port rides the reply's mint,
/// the port-0 row's real shape), records it for the cancel forms, /// the port-0 row's real shape), records it for the cancel forms,
/// and spawns the accept loop (per accepted connection: a /// and spawns the accept loop (per accepted connection: a
/// server-initiated `forwarded-*` open toward the client echoing /// server-initiated `forwarded-*` open toward the client echoing
/// the listen's own args + the connection's peer, then the bytes /// the listen's own args + the connection's peer, then the bytes
/// pumped bidirectionally; a client-declined open closes that /// pumped bidirectionally; a client-declined open closes that
/// accepted connection — vanilla parity). /// accepted connection, vanilla parity).
Accept, Accept,
} }
/// The fake `sshd`'s op-row script (the `w4-op-rows` request /// The fake `sshd`'s op-row script (the `w4-op-rows` request
/// surfaces) — what the double answers and, on a success, what the /// surfaces), what the double answers and, on a success, what the
/// scripted "program" produces. /// scripted "program" produces.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct SshdScript { pub struct SshdScript {
@@ -142,7 +142,7 @@ pub struct SshdScript {
pub stdout: &'static [u8], pub stdout: &'static [u8],
/// The channel data written as `extended_data(1)` stderr. /// The channel data written as `extended_data(1)` stderr.
pub stderr: &'static [u8], pub stderr: &'static [u8],
/// The exit report after the outputs (`None` = none — the /// The exit report after the outputs (`None` = none, the
/// no-report rows). /// no-report rows).
pub exit: Option<DblExit>, pub exit: Option<DblExit>,
/// The `direct-tcpip` opens' behavior. /// The `direct-tcpip` opens' behavior.
@@ -154,7 +154,7 @@ pub struct SshdScript {
/// The `streamlocal-forward` listen form's behavior (the unix /// The `streamlocal-forward` listen form's behavior (the unix
/// `-R` rows). /// `-R` rows).
pub unix_listen: DblListen, pub unix_listen: DblListen,
/// The counted refusal — the double declines the *first N* client /// The counted refusal; the double declines the *first N* client
/// session opens (per connection, armed by /// session opens (per connection, armed by
/// `accept_session_channels`): the stays-usable-after-a-decline /// `accept_session_channels`): the stays-usable-after-a-decline
/// rows' scripted shape (one connection, first open refused, the /// rows' scripted shape (one connection, first open refused, the
@@ -183,11 +183,11 @@ impl Default for SshdScript {
} }
/// One recorded global-request listen/cancel form: (the request /// One recorded global-request listen/cancel form: (the request
/// name, the wire endpoint string, the wire port) — the reverse-listen /// name, the wire endpoint string, the wire port); the reverse-listen
/// rows assert the listens the client ops put on the wire. /// rows assert the listens the client ops put on the wire.
pub type ListenRecord = (&'static str, String, u32); pub type ListenRecord = (&'static str, String, u32);
/// The double's per-connection observations — the wire rows assert /// The double's per-connection observations; the wire rows assert
/// the verbatim translations against these (what the client's op /// the verbatim translations against these (what the client's op
/// rows put on the wire). /// rows put on the wire).
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -201,11 +201,11 @@ pub struct SshdObserved {
cancels: Arc<Mutex<Vec<ListenRecord>>>, cancels: Arc<Mutex<Vec<ListenRecord>>>,
} }
/// One recorded pty request's translated shapes — (term, cols, rows, /// One recorded pty request's translated shapes: (term, cols, rows,
/// pixel_width, pixel_height). /// pixel_width, pixel_height).
pub type PtyRecord = (String, u32, u32, u32, u32); pub type PtyRecord = (String, u32, u32, u32, u32);
/// One recorded window change's shapes — (cols, rows, pixel_width, /// One recorded window change's shapes: (cols, rows, pixel_width,
/// pixel_height). /// pixel_height).
pub type WindowRecord = (u32, u32, u32, u32); pub type WindowRecord = (u32, u32, u32, u32);
@@ -234,13 +234,13 @@ impl SshdObserved {
self.windows.lock().clone() self.windows.lock().clone()
} }
/// The signals seen (russh's `Sig` debug names — "TERM", /// The signals seen (russh's `Sig` debug names, "TERM",
/// `Custom("..")`, …). /// `Custom("..")`, …).
pub fn signals(&self) -> Vec<String> { pub fn signals(&self) -> Vec<String> {
self.signals.lock().clone() self.signals.lock().clone()
} }
/// The global-request listen forms seen (name, endpoint, port — /// The global-request listen forms seen (name, endpoint, port;
/// the requested port, never the mint). /// the requested port, never the mint).
pub fn listens(&self) -> Vec<ListenRecord> { pub fn listens(&self) -> Vec<ListenRecord> {
self.listens.lock().clone() self.listens.lock().clone()
@@ -252,7 +252,7 @@ impl SshdObserved {
} }
} }
/// The double's scripted program serve — after a request's success /// The double's scripted program serve, after a request's success
/// reply: stdout, then `extended_data(1)` stderr, then the exit /// reply: stdout, then `extended_data(1)` stderr, then the exit
/// report, then the close (the vanilla sshd's exit-then-close /// report, then the close (the vanilla sshd's exit-then-close
/// discipline). /// discipline).
@@ -292,7 +292,7 @@ async fn scripted_program(handle: Handle, channel_id: russh::ChannelId, script:
let _ = handle.close(channel_id).await; let _ = handle.close(channel_id).await;
} }
/// The direct opens' echo pump — the accepted-typed-open shape's /// The direct opens' echo pump, the accepted-typed-open shape's
/// server half, POC-1 §7's byte round-trip. /// server half, POC-1 §7's byte round-trip.
async fn direct_echo(mut stream: russh::ChannelStream<Msg>) { async fn direct_echo(mut stream: russh::ChannelStream<Msg>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -311,9 +311,9 @@ async fn direct_echo(mut stream: russh::ChannelStream<Msg>) {
/// The `-R` accept loop per bound listener (the double's sshd role): /// The `-R` accept loop per bound listener (the double's sshd role):
/// per accepted connection a server-initiated `forwarded-*` open /// per accepted connection a server-initiated `forwarded-*` open
/// toward the client — the connected address echoes the listen /// toward the client; the connected address echoes the listen
/// request's wire string verbatim, the port the actually-bound one, /// request's wire string verbatim, the port the actually-bound one,
/// the originator the accepted connection's peer — then the bytes /// and the originator the accepted connection's peer; then the bytes
/// pumped bidirectionally between the SSH channel and the accepted /// pumped bidirectionally between the SSH channel and the accepted
/// socket. A client-declined open closes that accepted connection /// socket. A client-declined open closes that accepted connection
/// (vanilla parity: the listener stays, the data has nowhere to go). /// (vanilla parity: the listener stays, the data has nowhere to go).
@@ -347,7 +347,7 @@ async fn accept_forward_loop(
} }
/// The accepted forward connection's pump (its SSH channel ↔ its /// The accepted forward connection's pump (its SSH channel ↔ its
/// socket) — the data leg's byte plane. /// socket), the data leg's byte plane.
async fn forward_connection(channel: Channel<Msg>, mut conn: tokio::net::TcpStream) { async fn forward_connection(channel: Channel<Msg>, mut conn: tokio::net::TcpStream) {
let mut stream = channel.into_stream(); let mut stream = channel.into_stream();
let _ = tokio::io::copy_bidirectional(&mut stream, &mut conn).await; let _ = tokio::io::copy_bidirectional(&mut stream, &mut conn).await;
@@ -384,7 +384,7 @@ async fn unix_forward_connection(channel: Channel<Msg>, mut conn: tokio::net::Un
let _ = tokio::io::copy_bidirectional(&mut stream, &mut conn).await; let _ = tokio::io::copy_bidirectional(&mut stream, &mut conn).await;
} }
/// The fake `sshd`'s per-connection handler — accept-set identity /// The fake `sshd`'s per-connection handler: accept-set identity
/// resolution, explicit rejects everywhere, the `auth_succeeded` /// resolution, explicit rejects everywhere, the `auth_succeeded`
/// handle carrier and the session-open acceptor stub. /// handle carrier and the session-open acceptor stub.
struct SshdDoubleHandler { struct SshdDoubleHandler {
@@ -397,7 +397,7 @@ struct SshdDoubleHandler {
remaining_declines: std::sync::Arc<std::sync::atomic::AtomicUsize>, remaining_declines: std::sync::Arc<std::sync::atomic::AtomicUsize>,
/// The `-R` bookkeeping (per-connection, sshd's listener table): /// The `-R` bookkeeping (per-connection, sshd's listener table):
/// the key is the cancel-form's match key (the wire endpoint + the /// the key is the cancel-form's match key (the wire endpoint + the
/// bound port — a `port = 0` mint's cancel matches the bound one), /// bound port (a `port = 0` mint's cancel matches the bound one),
/// the value the live accept loop (cancel aborts it, dropping the /// the value the live accept loop (cancel aborts it, dropping the
/// real listener with it). /// real listener with it).
tcp_forwards: std::collections::HashMap<(String, u32), tokio::task::JoinHandle<()>>, tcp_forwards: std::collections::HashMap<(String, u32), tokio::task::JoinHandle<()>>,
@@ -425,7 +425,7 @@ impl russh::server::Handler for SshdDoubleHandler {
} }
/// Explicit accept-and-delegate (the probe hook consumes no auth /// Explicit accept-and-delegate (the probe hook consumes no auth
/// attempt — POC-1 §4); the real disposition is /// attempt, POC-1 §4); the real disposition is
/// [`Self::auth_publickey`]'s. /// [`Self::auth_publickey`]'s.
async fn auth_publickey_offered( async fn auth_publickey_offered(
&mut self, &mut self,
@@ -513,7 +513,7 @@ impl russh::server::Handler for SshdDoubleHandler {
} }
/// The `direct-tcpip` script: reject (the scripted /// The `direct-tcpip` script: reject (the scripted
/// server-refusal class, `ConnectFailed` — the vanilla sshd's /// server-refusal class `ConnectFailed`, the vanilla sshd's
/// dead-target shape) or accept + echo pump. /// dead-target shape) or accept + echo pump.
async fn channel_open_direct_tcpip( async fn channel_open_direct_tcpip(
&mut self, &mut self,
@@ -633,7 +633,7 @@ impl russh::server::Handler for SshdDoubleHandler {
Ok(()) Ok(())
} }
/// The `tcpip-forward` listen form: recorded, scripted — an accept /// The `tcpip-forward` listen form: recorded and scripted; an accept
/// binds a real `127.0.0.1` listener in sshd's role (a `port = 0` /// binds a real `127.0.0.1` listener in sshd's role (a `port = 0`
/// request's mint is the socket's real bound port, the out-param /// request's mint is the socket's real bound port, the out-param
/// parity), keys the listener by (wire address, bound port), and /// parity), keys the listener by (wire address, bound port), and
@@ -675,7 +675,7 @@ impl russh::server::Handler for SshdDoubleHandler {
/// The `cancel-tcpip-forward` form: matches by the raw wire /// The `cancel-tcpip-forward` form: matches by the raw wire
/// strings (the bound port for a mint); a hit removes the /// strings (the bound port for a mint); a hit removes the
/// listener (tearing the real socket down — the cancel row's /// listener (tearing the real socket down, the cancel row's
/// observable), an unknown listener denies (sshd parity). /// observable), an unknown listener denies (sshd parity).
async fn cancel_tcpip_forward( async fn cancel_tcpip_forward(
&mut self, &mut self,
@@ -700,7 +700,7 @@ impl russh::server::Handler for SshdDoubleHandler {
/// scripts, binds the real unix socket (stale file unlinked /// scripts, binds the real unix socket (stale file unlinked
/// first), and spawns the unix accept loop. Off-Unix the hook is /// first), and spawns the unix accept loop. Off-Unix the hook is
/// not implemented and falls to the trait's deny default (the /// not implemented and falls to the trait's deny default (the
/// vanilla Windows sshd parity) — the crate's own runtime-Unix /// vanilla Windows sshd parity); the crate's own runtime-Unix
/// gate mirrors the refusal client-side before the wire. /// gate mirrors the refusal client-side before the wire.
#[cfg(unix)] #[cfg(unix)]
async fn streamlocal_forward( async fn streamlocal_forward(
@@ -729,7 +729,7 @@ impl russh::server::Handler for SshdDoubleHandler {
} }
/// The `cancel-streamlocal-forward` form (matches by the path /// The `cancel-streamlocal-forward` form (matches by the path
/// alone — the unix cancel's sshd parity). /// alone, the unix cancel's sshd parity).
#[cfg(unix)] #[cfg(unix)]
async fn cancel_streamlocal_forward( async fn cancel_streamlocal_forward(
&mut self, &mut self,
@@ -753,8 +753,8 @@ impl russh::server::Handler for SshdDoubleHandler {
impl SshdDoubleHandler { impl SshdDoubleHandler {
/// The scripted request reply: success launches the scripted /// The scripted request reply: success launches the scripted
/// program through the session's handle (spawned — the packet /// program through the session's handle (spawned, so the packet
/// loop returns fast) *when a program is scripted at all* — an /// loop returns fast) *when a program is scripted at all*; an
/// output-less/no-exit script launches nothing (the interactive /// output-less/no-exit script launches nothing (the interactive
/// sessions stay open; close is the client's or the connection's); /// sessions stay open; close is the client's or the connection's);
/// the failure class answers `channel_failure`. /// the failure class answers `channel_failure`.
@@ -775,7 +775,7 @@ impl SshdDoubleHandler {
} }
} }
/// The fake `sshd`'s accepted-identity register — exact-string /// The fake `sshd`'s accepted-identity register, exact-string
/// fingerprints (the row registers the fixture dials with). /// fingerprints (the row registers the fixture dials with).
pub struct SshdAccepts { pub struct SshdAccepts {
inner: Arc<parking_lot::RwLock<Vec<String>>>, inner: Arc<parking_lot::RwLock<Vec<String>>>,
@@ -789,9 +789,9 @@ impl SshdAccepts {
} }
} }
/// One spawned fake `sshd` — the client-direction contract vehicle. /// One spawned fake `sshd`, the client-direction contract vehicle.
pub struct SshdDouble { pub struct SshdDouble {
/// The dial target, `<ip>:<port>` — the row's /// The dial target, `<ip>:<port>`, the row's
/// `DialConfig.address` value. /// `DialConfig.address` value.
pub address: String, pub address: String,
session_carrier: Option<tokio::sync::oneshot::Receiver<Handle>>, session_carrier: Option<tokio::sync::oneshot::Receiver<Handle>>,
@@ -806,7 +806,7 @@ pub struct SshdDouble {
/// `host_keys` rides the `russh::server::Config`; /// `host_keys` rides the `russh::server::Config`;
/// `accept_session_channels` arms the session-open acceptor stub. /// `accept_session_channels` arms the session-open acceptor stub.
/// Returns the double, its accept-set register (register before /// Returns the double, its accept-set register (register before
/// dialing — the wire's auth reads it at the dial), and the /// dialing, since the wire's auth reads it at the dial), and the
/// observation surface (see [`SshdObserved`]). /// observation surface (see [`SshdObserved`]).
pub async fn spawn_scripted( pub async fn spawn_scripted(
host_keys: Vec<PrivateKey>, host_keys: Vec<PrivateKey>,
@@ -884,7 +884,7 @@ pub async fn spawn_scripted(
} }
/// Spawns the double on `127.0.0.1:0` with the plain dial rows' /// Spawns the double on `127.0.0.1:0` with the plain dial rows'
/// default script — the `w4-dial` shape, kept: the double and its /// default script, the `w4-dial` shape kept: the double and its
/// accept-set register. /// accept-set register.
pub async fn spawn( pub async fn spawn(
host_keys: Vec<PrivateKey>, host_keys: Vec<PrivateKey>,
@@ -896,7 +896,7 @@ pub async fn spawn(
} }
impl SshdDouble { impl SshdDouble {
/// The dialed session's server-side handle — the initiator for /// The dialed session's server-side handle, the initiator for
/// the server-initiated-open rows. Awaits the `auth_succeeded` /// the server-initiated-open rows. Awaits the `auth_succeeded`
/// carrier; the dial has authenticated by the time a row calls /// carrier; the dial has authenticated by the time a row calls
/// this (the carrier's sender only fires inside the double's /// this (the carrier's sender only fires inside the double's
@@ -912,13 +912,13 @@ impl SshdDouble {
} }
/// The client-initiated session channels the acceptor stub /// The client-initiated session channels the acceptor stub
/// accepted — `None` after the session's end (the observation /// accepted; `None` after the session's end (the observation
/// shape's honest empty). /// shape's honest empty).
pub async fn next_opened_session(&mut self) -> Option<Channel<Msg>> { pub async fn next_opened_session(&mut self) -> Option<Channel<Msg>> {
self.opened_sessions.recv().await self.opened_sessions.recv().await
} }
/// The next per-connection serving outcome — the clean-close /// The next per-connection serving outcome, the clean-close
/// rows' observable (a clean `Disconnect::ByApplication` is the /// rows' observable (a clean `Disconnect::ByApplication` is the
/// sessions' `Ok`; an abrupt peer close is an `Err` wire class). /// sessions' `Ok`; an abrupt peer close is an `Err` wire class).
/// `None` after the double's loop ends. /// `None` after the double's loop ends.
+22 -22
View File
@@ -1,17 +1,17 @@
//! The `session`-seam test doubles (`w3-tty-session`): the alkssh-side //! The `session`-seam test doubles (`w3-tty-session`): the alkssh-side
//! **TtyBackend shim double** — alktty's `MockBackend`/`testing.rs` are //! **TtyBackend shim double**: alktty's `MockBackend`/`testing.rs` are
//! `pub(crate)` + `cfg(test)` and never importable, so the suite //! `pub(crate)` + `cfg(test)` and never importable, so the suite
//! hand-rolls the shim per the `TtyHandle` field shapes: a backend //! hand-rolls the shim per the `TtyHandle` field shapes: a backend
//! that records the `TtyParams` it was allocated, consumes stdin //! that records the `TtyParams` it was allocated, consumes stdin
//! (recording bytes, the EOF shutdown and the abort kill), emits //! (recording bytes, the EOF shutdown and the abort kill), emits
//! scripted stdout/stderr chunks and a scripted exit code, and //! scripted stdout/stderr chunks and a scripted exit code, and
//! surfaces a control handle recording resizes/signals. Plus the //! surfaces a control handle recording resizes/signals. Plus the
//! ownership witness double — the identity that reaches //! and the ownership witness double: the identity that reaches
//! `drive_session_pre_negotiated`'s ADR-050 ownership check is the //! `drive_session_pre_negotiated`'s ADR-050 ownership check is the
//! identity ride's live witness (the task's "identity-landing item"). //! identity ride's live witness (the task's "identity-landing item").
//! //!
//! Every stream/future here is test-scripted and instantly-completed //! Every stream/future here is test-scripted and instantly-completed
//! or never-completing — no real process, no real PTY (the real-tool //! or never-completing; no real process, no real PTY (the real-tool
//! smoke composes alktty's own `local` backend instead, dev-dep only). //! smoke composes alktty's own `local` backend instead, dev-dep only).
use std::collections::HashMap; use std::collections::HashMap;
@@ -28,7 +28,7 @@ use bytes::Bytes;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
/// The shim's shared observation surface — every row's assertions /// The shim's shared observation surface; every row's assertions
/// come off this (`Record TtyParams`, consume stdin/EOF/resizes/ /// come off this (`Record TtyParams`, consume stdin/EOF/resizes/
/// signals, the abort kill witness). /// signals, the abort kill witness).
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -38,20 +38,20 @@ pub struct TtyShimObservation {
/// Bytes written to the shim's stdin, concatenated per allocation. /// Bytes written to the shim's stdin, concatenated per allocation.
pub stdin: parking_lot::Mutex<Vec<u8>>, pub stdin: parking_lot::Mutex<Vec<u8>>,
/// Whether a stdin shutdown (the EOF translation's destination) /// Whether a stdin shutdown (the EOF translation's destination)
/// was observed — one flag per shutdown, but one allocation per /// was observed (one flag per shutdown, but one allocation per
/// row in practice. /// row in practice).
pub stdin_eof: parking_lot::Mutex<Vec<bool>>, pub stdin_eof: parking_lot::Mutex<Vec<bool>>,
/// Resizes from the control handle (`cols`, `rows`). /// Resizes from the control handle (`cols`, `rows`).
pub resizes: parking_lot::Mutex<Vec<(u16, u16)>>, pub resizes: parking_lot::Mutex<Vec<(u16, u16)>>,
/// Signals from the control handle — the name strings, verbatim. /// Signals from the control handle, the name strings verbatim.
pub signals: parking_lot::Mutex<Vec<String>>, pub signals: parking_lot::Mutex<Vec<String>>,
/// The abort kill witness: pushed once per `exit_code` future /// The abort kill witness: pushed once per `exit_code` future
/// dropped *unresolved* — alktty's kill-on-drop contract /// dropped *unresolved*; alktty's kill-on-drop contract
/// (ADR-056) observed through the shim. /// (ADR-056) observed through the shim.
pub kills: Arc<parking_lot::Mutex<Vec<bool>>>, pub kills: Arc<parking_lot::Mutex<Vec<bool>>>,
} }
/// The shim's scripted per-allocation behavior — what the shim emits /// The shim's scripted per-allocation behavior: what the shim emits
/// and what exit code it resolves with. /// and what exit code it resolves with.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ShimScript { pub struct ShimScript {
@@ -66,7 +66,7 @@ pub struct ShimScript {
pub exit: Option<i32>, pub exit: Option<i32>,
} }
/// The TtyBackend shim double — a hand-rolled minimal backend per the /// The TtyBackend shim double, a hand-rolled minimal backend per the
/// `TtyHandle` field shapes (see the module doc). Clone-able (the /// `TtyHandle` field shapes (see the module doc). Clone-able (the
/// observation + script are `Arc`s). /// observation + script are `Arc`s).
#[derive(Clone, Default)] #[derive(Clone, Default)]
@@ -133,7 +133,7 @@ impl TtyBackend for TtyShim {
}) })
} }
/// The ADR-050 check input — every allocation targets this shim /// The ADR-050 check input (every allocation targets this shim
/// resource, so the ownership witness row (the identity surface) /// resource, so the ownership witness row (the identity surface)
/// always evaluates. /// always evaluates.
fn resource_id(&self, _params: &TtyParams) -> Option<(&'static str, String)> { fn resource_id(&self, _params: &TtyParams) -> Option<(&'static str, String)> {
@@ -143,7 +143,7 @@ impl TtyBackend for TtyShim {
impl ShimScript { impl ShimScript {
/// Whether the allocation is PTY-mode (the TtyParams's terminal is /// Whether the allocation is PTY-mode (the TtyParams's terminal is
/// not the NegotiateRequest.tty this module speaks — the adapter's /// not the NegotiateRequest.tty this module speaks; the adapter's
/// `From<NegotiateRequest>` conversion is what the seam consumes; /// `From<NegotiateRequest>` conversion is what the seam consumes;
/// the shim sees the converted `TtyParams`). /// the shim sees the converted `TtyParams`).
fn terminal_is_pty(&self, params: &TtyParams) -> bool { fn terminal_is_pty(&self, params: &TtyParams) -> bool {
@@ -151,9 +151,9 @@ impl ShimScript {
} }
} }
/// A scripted stream — the chunks in order, then end. Instantly /// A scripted stream: the chunks in order, then end. Instantly
/// drained (the alktty pumps consume; the exit chunk arrives after — /// drained (the alktty pumps consume; the exit chunk arrives after,
/// the adapter's exit-chunk-is-last invariant orders everything). /// since the adapter's exit-chunk-is-last invariant orders everything).
fn scripted_stream(chunks: &[Vec<u8>]) -> Pin<Box<dyn tokio_stream::Stream<Item = Bytes> + Send>> { fn scripted_stream(chunks: &[Vec<u8>]) -> Pin<Box<dyn tokio_stream::Stream<Item = Bytes> + Send>> {
let (tx, rx) = tokio::sync::mpsc::channel(chunks.len().max(1) + 1); let (tx, rx) = tokio::sync::mpsc::channel(chunks.len().max(1) + 1);
for chunk in chunks { for chunk in chunks {
@@ -163,7 +163,7 @@ fn scripted_stream(chunks: &[Vec<u8>]) -> Pin<Box<dyn tokio_stream::Stream<Item
Box::pin(ReceiverStream::new(rx)) Box::pin(ReceiverStream::new(rx))
} }
/// The shim's stdin — an `AsyncWrite` recording bytes and the EOF /// The shim's stdin, an `AsyncWrite` recording bytes and the EOF
/// shutdown (`poll_shutdown` = the zero-length sentinel's effect at /// shutdown (`poll_shutdown` = the zero-length sentinel's effect at
/// the backend). /// the backend).
struct ShimStdin { struct ShimStdin {
@@ -239,7 +239,7 @@ impl Drop for GuardedExit {
} }
} }
/// The shim's control surface — records resizes and signals verbatim. /// The shim's control surface; it records resizes and signals verbatim.
struct ShimControl { struct ShimControl {
observation: Arc<TtyShimObservation>, observation: Arc<TtyShimObservation>,
} }
@@ -255,7 +255,7 @@ impl TtyControl for ShimControl {
} }
/// A shim-backed `SessionSeam` in the `session`-seam shape the /// A shim-backed `SessionSeam` in the `session`-seam shape the
/// adapters take — `backends` map holds the shim under `"shim"`, the /// adapters take: the `backends` map holds the shim under `"shim"`, the
/// selector names it, ownership unset (the ADR-050 check skips). /// selector names it, ownership unset (the ADR-050 check skips).
pub fn tty_seam(shim: &Arc<TtyShim>) -> alkssh::SessionSeam { pub fn tty_seam(shim: &Arc<TtyShim>) -> alkssh::SessionSeam {
let mut backends = HashMap::new(); let mut backends = HashMap::new();
@@ -270,12 +270,12 @@ pub fn tty_seam(shim: &Arc<TtyShim>) -> alkssh::SessionSeam {
/// The ownership witness double ([`alkcall::core::ownership:: /// The ownership witness double ([`alkcall::core::ownership::
/// OwnershipProvider`]): records every identity evaluation and rules /// OwnershipProvider`]): records every identity evaluation and rules
/// by its configured verdict (the deny shape produces the negotiation /// by its configured verdict (the deny shape produces the negotiation
/// error frame on the wire — the bridge's `negotiation-failed` /// error frame on the wire, the bridge's `negotiation-failed`
/// decline). /// decline).
#[derive(Debug)] #[derive(Debug)]
pub struct MemOwnership { pub struct MemOwnership {
pub owns: bool, pub owns: bool,
/// The identity ids that reached `owns` — the identity ride's /// The identity ids that reached `owns`, the identity ride's
/// live witness. /// live witness.
pub ids: parking_lot::Mutex<Vec<String>>, pub ids: parking_lot::Mutex<Vec<String>>,
} }
@@ -310,7 +310,7 @@ impl OwnershipProvider for MemOwnership {
} }
} }
/// A shim-backed seam with the ownership provider present — the /// A shim-backed seam with the ownership provider present, the
/// witness rows' builder. /// witness rows' builder.
pub fn tty_seam_with_ownership( pub fn tty_seam_with_ownership(
shim: &Arc<TtyShim>, shim: &Arc<TtyShim>,
@@ -325,7 +325,7 @@ pub fn tty_seam_with_ownership(
} }
} }
/// A seam whose selector names no backend in its own map — the /// A seam whose selector names no backend in its own map; it renders the
/// `unknown_backend` negotiation error frame, driven from config. /// `unknown_backend` negotiation error frame, driven from config.
pub fn misconfigured_seam() -> alkssh::SessionSeam { pub fn misconfigured_seam() -> alkssh::SessionSeam {
alkssh::SessionSeam { alkssh::SessionSeam {
+43 -43
View File
@@ -1,5 +1,5 @@
//! The wave-2 dispatch/decline-core contract tests //! The wave-2 dispatch/decline-core contract tests
//! (`w2-dispatch-decline-core`) — the skeleton rows of the decline //! (`w2-dispatch-decline-core`): the skeleton rows of the decline
//! matrix (`mapping.md` §Decline matrix), driven per feature build //! matrix (`mapping.md` §Decline matrix), driven per feature build
//! through the loopback harness. This task tests the rows it lands; //! through the loopback harness. This task tests the rows it lands;
//! the full per-build matrix suite is wave-5's. //! the full per-build matrix suite is wave-5's.
@@ -32,7 +32,7 @@ use russh::{Channel, ChannelOpenFailure, Error};
use alkssh::Limits; use alkssh::Limits;
/// Mounts an authenticated loopback session over the doubles — the /// Mounts an authenticated loopback session over the doubles, the
/// dispatch rows' driver: fixture `alpha` resolves through /// dispatch rows' driver: fixture `alpha` resolves through
/// `MemIdentityProvider`, host keys `bravo`, the permissive host-key /// `MemIdentityProvider`, host keys `bravo`, the permissive host-key
/// posture (the explicit test choice). /// posture (the explicit test choice).
@@ -58,7 +58,7 @@ async fn authenticated() -> LoopbackSession {
session session
} }
/// Matches an open failure's SSH reason — the wire decline reason the /// Matches an open failure's SSH reason, the wire decline reason the
/// open-gate rows assert. /// open-gate rows assert.
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) { fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
assert!( assert!(
@@ -71,7 +71,7 @@ fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
} }
/// The request-level decline observation: the channel answers Failure. /// The request-level decline observation: the channel answers Failure.
/// A capability decline's channel stays open afterwards — the POC-1 §5 /// A capability decline's channel stays open afterwards, the POC-1 §5
/// session-survives-declines posture the channel-alive rows lean on. /// session-survives-declines posture the channel-alive rows lean on.
async fn expect_failure(channel: &mut Channel<Msg>, context: &str) { async fn expect_failure(channel: &mut Channel<Msg>, context: &str) {
match next_message(channel).await { match next_message(channel).await {
@@ -81,7 +81,7 @@ async fn expect_failure(channel: &mut Channel<Msg>, context: &str) {
} }
/// The serving-path (family-seam stub) decline observation: Failure /// The serving-path (family-seam stub) decline observation: Failure
/// then Close — the vanilla-clean shape for a row whose serving path /// then Close, the vanilla-clean shape for a row whose serving path
/// would own the channel. /// would own the channel.
async fn expect_failure_then_close(channel: &mut Channel<Msg>, context: &str) { async fn expect_failure_then_close(channel: &mut Channel<Msg>, context: &str) {
expect_failure(channel, context).await; expect_failure(channel, context).await;
@@ -91,7 +91,7 @@ async fn expect_failure_then_close(channel: &mut Channel<Msg>, context: &str) {
); );
} }
/// Proves a declined channel is still usable — a follow-up request /// Proves a declined channel is still usable: a follow-up request
/// reaches the dispatch again instead of the channel being dead. /// reaches the dispatch again instead of the channel being dead.
async fn expect_channel_alive(channel: &mut Channel<Msg>) { async fn expect_channel_alive(channel: &mut Channel<Msg>) {
common::harness::request_subsystem(channel, "sftp") common::harness::request_subsystem(channel, "sftp")
@@ -101,10 +101,10 @@ async fn expect_channel_alive(channel: &mut Channel<Msg>) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared rows — run in every feature build. // Shared rows; run in every feature build.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature — declined /// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature, declined
/// in every build, never feature-gated). /// in every build, never feature-gated).
#[tokio::test] #[tokio::test]
async fn x11_channel_opens_decline_in_every_build() { async fn x11_channel_opens_decline_in_every_build() {
@@ -122,7 +122,7 @@ async fn x11_channel_opens_decline_in_every_build() {
/// - *Contract stamp:* mapping.md §Global-request rows (the `-R` /// - *Contract stamp:* mapping.md §Global-request rows (the `-R`
/// listen flow is a `tunnel` row declined when the flow cannot /// listen flow is a `tunnel` row declined when the flow cannot
/// serve — this wave: every build). /// serve; this wave: every build).
#[tokio::test] #[tokio::test]
async fn tcpip_forward_listen_requests_decline_in_every_build() { async fn tcpip_forward_listen_requests_decline_in_every_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -139,7 +139,7 @@ async fn tcpip_forward_listen_requests_decline_in_every_build() {
/// - *Contract stamp:* mapping.md §Global-request rows (the /// - *Contract stamp:* mapping.md §Global-request rows (the
/// `streamlocal-forward` unix listen form is the `tunnel` row's /// `streamlocal-forward` unix listen form is the `tunnel` row's
/// other half: unfeatured builds decline before any fabric call, /// other half: unfeatured builds decline before any fabric call,
/// a `tunnel` build declines on the unset listen seam — the same /// a `tunnel` build declines on the unset listen seam; the same
/// refused-global-request wire shape both ways). /// refused-global-request wire shape both ways).
#[tokio::test] #[tokio::test]
async fn streamlocal_forward_listen_requests_decline_in_every_build() { async fn streamlocal_forward_listen_requests_decline_in_every_build() {
@@ -155,10 +155,10 @@ async fn streamlocal_forward_listen_requests_decline_in_every_build() {
} }
/// - *Contract stamp:* mapping.md §Global-request rows (the cancel /// - *Contract stamp:* mapping.md §Global-request rows (the cancel
/// forms: `false` for an unknown pair — the sshd not-found parity; /// forms: `false` for an unknown pair (the sshd not-found parity);
/// unfeatured builds decline their gate before reaching the /// unfeatured builds decline their gate before reaching the
/// listener map, a `tunnel` build's empty map misses the pair — /// listener map, a `tunnel` build's empty map misses the pair; the
/// the same refused-cancel wire shape both ways). /// same refused-cancel wire shape both ways).
#[tokio::test] #[tokio::test]
async fn global_request_cancels_refuse_unknown_pairs_in_every_build() { async fn global_request_cancels_refuse_unknown_pairs_in_every_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -206,7 +206,7 @@ async fn x11_and_agent_requests_decline_wherever_sessions_open() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Default build (no features): everything declines (mapping.md // Default build (no features): everything declines (mapping.md
// §Decline matrix — "none (default): declines everything"). // §Decline matrix, "none (default): declines everything").
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[cfg(not(any(feature = "session", feature = "git", feature = "tunnel")))] #[cfg(not(any(feature = "session", feature = "git", feature = "tunnel")))]
@@ -215,8 +215,8 @@ mod default_build {
use common::harness::{open_direct_streamlocal, open_direct_tcpip}; use common::harness::{open_direct_streamlocal, open_direct_tcpip};
/// - *Contract stamp:* mapping.md §Decline matrix (none-build /// - *Contract stamp:* mapping.md §Decline matrix (none-build
/// row: every channel type declines at open — session channels /// row: every channel type declines at open (session channels
/// as unfeatured, tunnel rows as unfeatured — plus the /// as unfeatured, tunnel rows as unfeatured) plus the
/// never-feature x11). /// never-feature x11).
#[tokio::test] #[tokio::test]
async fn every_channel_open_declines_in_a_default_build() { async fn every_channel_open_declines_in_a_default_build() {
@@ -255,9 +255,9 @@ mod default_build {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// git-only build: routes git-parseable exec to the git row; declines // git-only build: routes git-parseable exec to the git row; declines
// pty/shell, plain exec, all other opens (mapping.md §Decline matrix — // pty/shell, plain exec, all other opens (mapping.md §Decline matrix,
// git row). The harness builds no seam, so the git row's decline is // git row). The harness builds no seam, so the git row's decline is
// the config-driven one (`w3-git-mapping`: seam-unconfigured — the // the config-driven one (`w3-git-mapping`: seam-unconfigured, the
// unset-declines contract, serving-path shape); the serving rows ride // unset-declines contract, serving-path shape); the serving rows ride
// `tests/git_mapping.rs`. // `tests/git_mapping.rs`.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -271,7 +271,7 @@ mod git_build {
}; };
/// - *Contract stamp:* the task's channel-open gate (session /// - *Contract stamp:* the task's channel-open gate (session
/// channels open where a session-serving feature exists — `git` /// channels open where a session-serving feature exists; `git`
/// qualifies, the parse path lives inside session channels). /// qualifies, the parse path lives inside session channels).
#[tokio::test] #[tokio::test]
async fn session_channels_open_in_a_git_build() { async fn session_channels_open_in_a_git_build() {
@@ -284,8 +284,8 @@ mod git_build {
/// - *Contract stamp:* ADR-006 point 3 (parse-only: a /// - *Contract stamp:* ADR-006 point 3 (parse-only: a
/// git-parseable exec routes to the git row; this build /// git-parseable exec routes to the git row; this build
/// configures no seam, so the row declines at its config gate — /// configures no seam, so the row declines at its config gate,
/// the serving-path shape, failure then close). The row's /// the serving-path shape: failure then close). The row's
/// serving surface (seam configured, real dispatch) is /// serving surface (seam configured, real dispatch) is
/// `tests/git_mapping.rs`'s. /// `tests/git_mapping.rs`'s.
#[tokio::test] #[tokio::test]
@@ -312,7 +312,7 @@ mod git_build {
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row: /// - *Contract stamp:* mapping.md §Decline matrix (git-build row:
/// plain exec declines; the parse-then-fail shape of POC-3 /// plain exec declines; the parse-then-fail shape of POC-3
/// stage A — the channel survives the decline). /// stage A; the channel survives the decline).
#[tokio::test] #[tokio::test]
async fn plain_exec_declines_and_keeps_the_channel_in_a_git_build() { async fn plain_exec_declines_and_keeps_the_channel_in_a_git_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -328,7 +328,7 @@ mod git_build {
} }
/// - *Contract stamp:* POC-3 §2 (unknown git services take the /// - *Contract stamp:* POC-3 §2 (unknown git services take the
/// agreed declined surface — `w3-git-mapping`'s decided class, /// agreed declined surface (`w3-git-mapping`'s decided class,
/// off the pipe row: `ExecRoute::Declined`, build-independent; /// off the pipe row: `ExecRoute::Declined`, build-independent;
/// the reply vocabulary is shared with parse-then-fail: failure, /// the reply vocabulary is shared with parse-then-fail: failure,
/// channel alive). /// channel alive).
@@ -353,7 +353,7 @@ mod git_build {
} }
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row: /// - *Contract stamp:* mapping.md §Decline matrix (git-build row:
/// pty/shell and all session requests decline — the pty row /// pty/shell and all session requests decline; the pty row
/// declines unfeatured (`channel_failure`, the channel alive) /// declines unfeatured (`channel_failure`, the channel alive)
/// exactly as the shell row does). /// exactly as the shell row does).
#[tokio::test] #[tokio::test]
@@ -383,7 +383,7 @@ mod git_build {
/// - *Contract stamp:* mapping.md §Request-level rows (subsystem: /// - *Contract stamp:* mapping.md §Request-level rows (subsystem:
/// `sftp` is the OQ-13 placeholder decline; git service names /// `sftp` is the OQ-13 placeholder decline; git service names
/// carry no repository id — ADR-006 decision 2). /// carry no repository id; ADR-006 decision 2).
#[tokio::test] #[tokio::test]
async fn subsystems_decline_with_their_named_reasons_in_a_git_build() { async fn subsystems_decline_with_their_named_reasons_in_a_git_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -405,7 +405,7 @@ mod git_build {
} }
/// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows: /// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows:
/// `env_request` declines unconditionally — no allowlist ships). /// `env_request` declines unconditionally; no allowlist ships).
#[tokio::test] #[tokio::test]
async fn env_requests_decline_in_a_git_build() { async fn env_requests_decline_in_a_git_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -421,7 +421,7 @@ mod git_build {
} }
/// - *Contract stamp:* mapping.md §Decline matrix (git-build /// - *Contract stamp:* mapping.md §Decline matrix (git-build
/// row: all other channel types decline — tunnel unfeatured). /// row: all other channel types decline, tunnel unfeatured).
#[tokio::test] #[tokio::test]
async fn tunnel_opens_decline_unfeatured_in_a_git_build() { async fn tunnel_opens_decline_unfeatured_in_a_git_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -453,7 +453,7 @@ mod git_build {
// its gate); with no seam/config this build's session rows decline at // its gate); with no seam/config this build's session rows decline at
// their config gates (the unset-declines contract, serving-path // their config gates (the unset-declines contract, serving-path
// shape: `w3-tty-session`); declines tunnel opens (mapping.md // shape: `w3-tty-session`); declines tunnel opens (mapping.md
// §Decline matrix — session row). // §Decline matrix, session row).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[cfg(all(feature = "session", not(feature = "git"), not(feature = "tunnel")))] #[cfg(all(feature = "session", not(feature = "git"), not(feature = "tunnel")))]
@@ -476,7 +476,7 @@ mod session_build {
} }
/// - *Contract stamp:* mapping.md §Decline matrix (session row: /// - *Contract stamp:* mapping.md §Decline matrix (session row:
/// git-parseable exec declines — the `git` parse is its gate; /// git-parseable exec declines; the `git` parse is its gate;
/// the channel survives the decline, POC-1 §5). /// the channel survives the decline, POC-1 §5).
#[tokio::test] #[tokio::test]
async fn git_parseable_exec_declines_in_a_session_build() { async fn git_parseable_exec_declines_in_a_session_build() {
@@ -498,7 +498,7 @@ mod session_build {
/// - *Contract stamp:* OQ-08 + the recorded wire shape /// - *Contract stamp:* OQ-08 + the recorded wire shape
/// (`w3-tty-session`: `exec_wrapper_argv` unset → the pipe row /// (`w3-tty-session`: `exec_wrapper_argv` unset → the pipe row
/// declines — the serving-path shape, failure then close, never /// declines (the serving-path shape, failure then close, never
/// a fallback). The row's serving surface (everything /// a fallback). The row's serving surface (everything
/// configured) is `tests/tty_mapping.rs`'s. /// configured) is `tests/tty_mapping.rs`'s.
#[tokio::test] #[tokio::test]
@@ -581,7 +581,7 @@ mod session_build {
} }
/// - *Contract stamp:* mapping.md §Decline matrix (session row: /// - *Contract stamp:* mapping.md §Decline matrix (session row:
/// other channel types decline — tunnel unfeatured). /// other channel types decline, tunnel unfeatured).
#[tokio::test] #[tokio::test]
async fn tunnel_opens_decline_unfeatured_in_a_session_build() { async fn tunnel_opens_decline_unfeatured_in_a_session_build() {
let session = authenticated().await; let session = authenticated().await;
@@ -612,8 +612,8 @@ mod session_build {
// tunnel-only build: session channels decline (auto-reject); the // tunnel-only build: session channels decline (auto-reject); the
// tunnel direct rows decline on the **absent handle** even with the // tunnel direct rows decline on the **absent handle** even with the
// feature on (the route-1 bridge is `w3-tunnel-direct`: the pre-admit // feature on (the route-1 bridge is `w3-tunnel-direct`: the pre-admit
// unset-handle class — the unset-declines posture; the fabric double's // unset-handle class, the unset-declines posture; the fabric double's
// serve/cap/fabric-failure rows ride tests/tunnel_mapping.rs) — // serve/cap/fabric-failure rows ride tests/tunnel_mapping.rs) per
// mapping.md §Decline matrix (tunnel row). // mapping.md §Decline matrix (tunnel row).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -624,7 +624,7 @@ mod tunnel_build {
/// - *Contract stamp:* ADR-006 point 3 as amended (parse-only /// - *Contract stamp:* ADR-006 point 3 as amended (parse-only
/// dispatch: a request-less/deserving-less session channel /// dispatch: a request-less/deserving-less session channel
/// declines as unfeatured — a tunnel-only build has no /// declines as unfeatured; a tunnel-only build has no
/// session-serving feature, so the open declines). /// session-serving feature, so the open declines).
#[tokio::test] #[tokio::test]
async fn session_channels_decline_in_a_tunnel_build() { async fn session_channels_decline_in_a_tunnel_build() {
@@ -641,9 +641,9 @@ mod tunnel_build {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows + the /// - *Contract stamp:* mapping.md §Channel-type rows + the
/// unset-declines posture (`w3-seam-config` — the feature alone /// unset-declines posture (`w3-seam-config`, the feature alone
/// configures nothing): the direct rows decline on the **absent /// configures nothing): the direct rows decline on the **absent
/// fabric handle** even with `tunnel` on — /// fabric handle** even with `tunnel` on:
/// [`connect failed`], the cannot-bridge class ADR-006 decision /// [`connect failed`], the cannot-bridge class ADR-006 decision
/// 1 pins (`w3-tunnel-direct`'s recorded wire classes: policy /// 1 pins (`w3-tunnel-direct`'s recorded wire classes: policy
/// refusals are administratively-prohibited, cannot-bridge is /// refusals are administratively-prohibited, cannot-bridge is
@@ -675,8 +675,8 @@ mod tunnel_build {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// All features: the routing decisions hold with the full graph on — // All features: the routing decisions hold with the full graph on; the
// the git row is still its config-driven decline in this binary (the // git row is still its config-driven decline in this binary (the
// serving rows ride tests/git_mapping.rs), and the session rows are // serving rows ride tests/git_mapping.rs), and the session rows are
// still their config-driven declines (the serving rows ride // still their config-driven declines (the serving rows ride
// tests/tty_mapping.rs). // tests/tty_mapping.rs).
@@ -706,7 +706,7 @@ mod all_features {
} }
/// - *Contract stamp:* OQ-08 + the recorded wire shape (the /// - *Contract stamp:* OQ-08 + the recorded wire shape (the
/// unset-wrapper pipe row declines — the serving-path shape; /// unset-wrapper pipe row declines, the serving-path shape;
/// `w3-tty-session`). The serving surface rides /// `w3-tty-session`). The serving surface rides
/// `tests/tty_mapping.rs`. /// `tests/tty_mapping.rs`.
#[tokio::test] #[tokio::test]
@@ -723,7 +723,7 @@ mod all_features {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows + OQ-08 (the /// - *Contract stamp:* mapping.md §Channel-type rows + OQ-08 (the
/// unset-seam pty/shell declines with the full graph on — the /// unset-seam pty/shell declines with the full graph on, since the
/// feature alone configures nothing; the serving surface rides /// feature alone configures nothing; the serving surface rides
/// `tests/tty_mapping.rs`). /// `tests/tty_mapping.rs`).
#[tokio::test] #[tokio::test]
@@ -751,7 +751,7 @@ mod all_features {
/// - *Contract stamp:* the unset-declines posture (`w3-seam-config`, /// - *Contract stamp:* the unset-declines posture (`w3-seam-config`,
/// carried through `w3-tunnel-direct`: the route-1 bridge's /// carried through `w3-tunnel-direct`: the route-1 bridge's
/// pre-admit unset-handle class) — the direct rows decline on /// pre-admit unset-handle class): the direct rows decline on
/// the **absent fabric handle** even with the full feature graph /// the **absent fabric handle** even with the full feature graph
/// on (`ConnectFailed`, the cannot-bridge class); the served /// on (`ConnectFailed`, the cannot-bridge class); the served
/// surface rides tests/tunnel_mapping.rs's fabric double. /// surface rides tests/tunnel_mapping.rs's fabric double.
@@ -781,8 +781,8 @@ mod all_features {
} }
/// - *Contract stamp:* mapping.md §Request-level rows (the /// - *Contract stamp:* mapping.md §Request-level rows (the
/// capability declines — sftp placeholder, git subsystem /// capability declines (sftp placeholder, git subsystem
/// names, env — hold with everything on). /// names, env) hold with everything on).
#[tokio::test] #[tokio::test]
async fn capability_declines_hold_with_all_features() { async fn capability_declines_hold_with_all_features() {
let session = authenticated().await; let session = authenticated().await;
+12 -12
View File
@@ -1,4 +1,4 @@
//! The wave-1 doubles' end-to-end rows — the seams exercised through //! The wave-1 doubles' end-to-end rows: the seams exercised through
//! the doubles exactly as the later waves' contract tests will run //! the doubles exactly as the later waves' contract tests will run
//! against them: fingerprint registration resolution, the vault //! against them: fingerprint registration resolution, the vault
//! custody round-trip and its failure postures, and the host-key //! custody round-trip and its failure postures, and the host-key
@@ -26,7 +26,7 @@ use common::{FIXTURE_KEYS, MemHostKeyPolicy, MemIdentityProvider, TestVault, fix
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint → /// - *Contract stamp:* ADR-004 decision 1 (fingerprint →
/// `resolve_from_fingerprint` → `Some` = accept; `None` = reject). /// `resolve_from_fingerprint` → `Some` = accept; `None` = reject).
/// - *Contract stamp:* ADR-005 context (client fingerprints never /// - *Contract stamp:* ADR-005 context (client fingerprints never
/// involve the vault — the `IdentityProvider` path only). /// involve the vault (the `IdentityProvider` path only).
#[test] #[test]
fn mem_identity_provider_registered_fingerprint_resolves() { fn mem_identity_provider_registered_fingerprint_resolves() {
let alpha = fixture("alpha"); let alpha = fixture("alpha");
@@ -51,7 +51,7 @@ fn mem_identity_provider_registered_fingerprint_resolves() {
); );
} }
/// A revocation makes the fingerprint resolve `None` again — the /// A revocation makes the fingerprint resolve `None` again; the
/// resolution set is inspectable and mutable. /// resolution set is inspectable and mutable.
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject is the /// - *Contract stamp:* ADR-004 decision 1 (`None` → reject is the
@@ -74,7 +74,7 @@ fn mem_identity_provider_revocation_resolves_none() {
assert!(provider.registered_fingerprints().is_empty()); assert!(provider.registered_fingerprints().is_empty());
} }
/// The token path resolves nothing on the SSH surface — the double /// The token path resolves nothing on the SSH surface; the double
/// never invents a token shape. /// never invents a token shape.
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint is the /// - *Contract stamp:* ADR-004 decision 1 (the fingerprint is the
@@ -89,8 +89,8 @@ fn mem_identity_provider_token_path_resolves_none() {
assert!(provider.resolve_from_token(&token).is_none()); assert!(provider.resolve_from_token(&token).is_none());
} }
/// Every fixture key's fingerprint — whether derived via the private /// Every fixture key's fingerprint, whether derived via the private
/// PEM or the committed public blob — equals its `ssh-keygen -lf` /// PEM or the committed public blob, equals its `ssh-keygen -lf`
/// recorded value. This row keeps the fixture set honest for every /// recorded value. This row keeps the fixture set honest for every
/// consumer; `tests/fingerprint_parity.rs` is the deep parity check. /// consumer; `tests/fingerprint_parity.rs` is the deep parity check.
/// ///
@@ -144,7 +144,7 @@ async fn test_vault_seal_then_key_source_decode_round_trips() {
/// The POC-2 §3 failure postures ride the real seams: locked vault → /// The POC-2 §3 failure postures ride the real seams: locked vault →
/// `Locked`, absent record → `Absent`, tampered ciphertext → /// `Locked`, absent record → `Absent`, tampered ciphertext →
/// `Decryption` — each a distinct variant, each a refusal, no /// `Decryption`; each a distinct variant, each a refusal, no
/// fallback key shape. /// fallback key shape.
/// ///
/// - *Contract stamp:* ADR-005 decision 4 (failure modes fail closed /// - *Contract stamp:* ADR-005 decision 4 (failure modes fail closed
@@ -181,7 +181,7 @@ async fn test_vault_postures_refuse_fail_closed_through_the_source() {
assert!(matches!(err, KeySourceError::Decryption(_)), "got {err:?}"); assert!(matches!(err, KeySourceError::Decryption(_)), "got {err:?}");
} }
/// The host-key set loads from the vault seam in registration order — /// The host-key set loads from the vault seam in registration order:
/// one custody model, both directions (the decrypt-at-construction /// one custody model, both directions (the decrypt-at-construction
/// load path). /// load path).
/// ///
@@ -215,10 +215,10 @@ async fn host_key_source_set_loads_from_the_vault_seam_in_order() {
/// The policy double's verdicts are honored and its decision log is /// The policy double's verdicts are honored and its decision log is
/// the inspectable record: pinned exact `(host, fingerprint)` pairs /// the inspectable record: pinned exact `(host, fingerprint)` pairs
/// accept, unmatched pairs fall to the default (reject — fail-closed), /// accept, unmatched pairs fall to the default (reject, fail-closed),
/// and every decision is logged with the full presented context. /// and every decision is logged with the full presented context.
/// ///
/// - *Contract stamp:* OQ-05 resolution (`host_key.rs` module docs — /// - *Contract stamp:* OQ-05 resolution (`host_key.rs` module docs:
/// the crate is policy-blind; verdicts are the policy's). /// the crate is policy-blind; verdicts are the policy's).
/// - *Contract stamp:* ADR-005 decision 6 (the host *pin* is public /// - *Contract stamp:* ADR-005 decision 6 (the host *pin* is public
/// config; exact-string comparison by contract). /// config; exact-string comparison by contract).
@@ -263,10 +263,10 @@ async fn mem_host_key_policy_verdicts_are_recorded_and_honored() {
} }
/// A permissive default is the test's explicit choice (never a /// A permissive default is the test's explicit choice (never a
/// default), and an explicit rule overrides it — the case-2 /// default), and an explicit rule overrides it; the case-2
/// ephemeral-fleet shape versus a pin-style correction. /// ephemeral-fleet shape versus a pin-style correction.
/// ///
/// - *Contract stamp:* OQ-05 resolution (`host_key.rs` — there is no /// - *Contract stamp:* OQ-05 resolution (`host_key.rs`; there is no
/// accept-all built-in crate option; case-2 policies are the /// accept-all built-in crate option; case-2 policies are the
/// assembly's to write). /// assembly's to write).
/// - *Contract stamp:* ADR-004 decision 6 posture analog /// - *Contract stamp:* ADR-004 decision 6 posture analog
+50 -50
View File
@@ -1,4 +1,4 @@
//! The client-direction real-tool e2e (`w5-e2e-client`) — the //! The client-direction real-tool e2e (`w5-e2e-client`): the
//! reverse-runner shape re-landed against a **real `sshd`** //! reverse-runner shape re-landed against a **real `sshd`**
//! (`docs/plans/implementation.md` §Wave 5, the real-tool e2e bullet's //! (`docs/plans/implementation.md` §Wave 5, the real-tool e2e bullet's
//! client half). The wave-4 client rows ran against the in-process //! client half). The wave-4 client rows ran against the in-process
@@ -8,24 +8,24 @@
//! (exit-status parity through a real server's reaping), the //! (exit-status parity through a real server's reaping), the
//! direct-tcpip dial (`-L`-shaped), the `-R` leg (the port-0 mint //! direct-tcpip dial (`-L`-shaped), the `-R` leg (the port-0 mint
//! against a real server's actually-bound reply, the server-initiated //! against a real server's actually-bound reply, the server-initiated
//! `forwarded-tcpip` open served through the acceptor registry — the //! `forwarded-tcpip` open served through the acceptor registry (the
//! reverse-runner consumer pattern end to end), the cancel form //! reverse-runner consumer pattern end to end), the cancel form the cancel form
//! tearing the real listen down, and the decline rows (the sftp //! tearing the real listen down, and the decline rows (the sftp
//! client-side placeholder decline, the server-refused direct-tcpip //! client-side placeholder decline, the server-refused direct-tcpip
//! open with the connection staying usable, the host-key policy //! open with the connection staying usable, the host-key policy
//! mismatch hard-decline — the case-1 pin posture against a real //! mismatch hard-decline, the case-1 pin posture against a real
//! presented key). //! presented key).
//! //!
//! **Placement decision (recorded, the task's acceptance criterion //! **Placement decision (recorded, the task's acceptance criterion
//! 1):** a dedicated `tests/e2e_client.rs` binary reusing //! 1):** a dedicated `tests/e2e_client.rs` binary reusing
//! `tests/common` — the wave-4 client binaries (`tests/client_dial.rs` //! `tests/common`, so the wave-4 client binaries (`tests/client_dial.rs`
//! / `client_ops.rs` / `client_reverse_listen.rs`) keep their //! / `client_ops.rs` / `client_reverse_listen.rs`) keep their
//! single-writer posture for `w5-decline-matrix`; this binary owns //! single-writer posture for `w5-decline-matrix`; this binary owns
//! only its own rows and its in-file `support` fixture module, //! only its own rows and its in-file `support` fixture module,
//! disjoint from `tests/e2e_server.rs` (the matrix task's binaries). //! disjoint from `tests/e2e_server.rs` (the matrix task's binaries).
//! //!
//! **The sshd fixture shape (decided + recorded):** a **per-row //! **The sshd fixture shape (decided + recorded):** a **per-row
//! spawned** real `sshd` — each row's config knobs differ //! spawned** real `sshd`; each row's config knobs differ
//! (`AllowTcpForwarding` yes/no) and each row dials its own server, //! (`AllowTcpForwarding` yes/no) and each row dials its own server,
//! so a shared per-binary instance buys nothing: per-row isolation //! so a shared per-binary instance buys nothing: per-row isolation
//! (own workdir, own minted ed25519 host key, own probed port) beats //! (own workdir, own minted ed25519 host key, own probed port) beats
@@ -39,7 +39,7 @@
//! dialed port; a server that never comes up (binary absent, //! dialed port; a server that never comes up (binary absent,
//! `ssh-keygen` absent, spawn refused, privilege-separation directory //! `ssh-keygen` absent, spawn refused, privilege-separation directory
//! missing, config refused) is the row's **reasoned skip** with the //! missing, config refused) is the row's **reasoned skip** with the
//! server's own `-e` log as the reason — never silently green. The //! server's own `-e` log as the reason, never silently green. The
//! dial rides the decrypt-at-dial path over the in-memory vault //! dial rides the decrypt-at-dial path over the in-memory vault
//! double (`VaultKeySource` over `TestVault`, fixture `alpha` sealed), //! double (`VaultKeySource` over `TestVault`, fixture `alpha` sealed),
//! and the case-1 pin posture (`MemHostKeyPolicy::pin`) accepts only //! and the case-1 pin posture (`MemHostKeyPolicy::pin`) accepts only
@@ -48,7 +48,7 @@
//! Rows are `#[cfg(unix)]` (the fixture spawns processes) and //! Rows are `#[cfg(unix)]` (the fixture spawns processes) and
//! feature-gated per row group under the established check-gated //! feature-gated per row group under the established check-gated
//! **skipped-with-reason** posture. Every real-tool interaction is //! **skipped-with-reason** posture. Every real-tool interaction is
//! deadline-bounded — a decline must surface a typed value to the //! deadline-bounded (a decline must surface a typed value to the
//! dialer, never a hang. Rows carry `Contract stamp:` doc markers //! dialer, never a hang. Rows carry `Contract stamp:` doc markers
//! against `client.md` / `reverse-runner.md` (the greppable //! against `client.md` / `reverse-runner.md` (the greppable
//! `tests/common` discipline). //! `tests/common` discipline).
@@ -69,12 +69,12 @@ mod support {
use super::common::{MemHostKeyPolicy, TestVault, fixture}; use super::common::{MemHostKeyPolicy, TestVault, fixture};
/// One shared row deadline — every real-process interaction is /// One shared row deadline; every real-process interaction is
/// bounded (the POC-1 §5 posture: a decline never hangs). /// bounded (the POC-1 §5 posture: a decline never hangs).
pub const ROW_DEADLINE: Duration = Duration::from_secs(30); pub const ROW_DEADLINE: Duration = Duration::from_secs(30);
/// The sshd candidates probed in order (`sbin` paths first — the /// The sshd candidates probed in order (`sbin` paths first, since
/// server binary rarely sits on the user's `PATH`). /// the server binary rarely sits on the user's `PATH`).
const SSHD_CANDIDATES: &[&str] = &[ const SSHD_CANDIDATES: &[&str] = &[
"/usr/sbin/sshd", "/usr/sbin/sshd",
"/usr/local/sbin/sshd", "/usr/local/sbin/sshd",
@@ -84,10 +84,10 @@ mod support {
/// Prints the row's reasoned skip (never silently green). /// Prints the row's reasoned skip (never silently green).
pub fn skip(row: &str, reason: &str) { pub fn skip(row: &str, reason: &str) {
eprintln!("SKIPPED (with reason): {row} — {reason}"); eprintln!("SKIPPED (with reason): {row}: {reason}");
} }
/// The `sshd` binary's location — probed once, `None` when the /// The `sshd` binary's location, probed once; `None` when the
/// environment carries no server (every row's reasoned skip). /// environment carries no server (every row's reasoned skip).
pub fn sshd_binary() -> Option<PathBuf> { pub fn sshd_binary() -> Option<PathBuf> {
SSHD_CANDIDATES SSHD_CANDIDATES
@@ -102,7 +102,7 @@ mod support {
Command::new("ssh-keygen").arg("-h").output().is_ok() Command::new("ssh-keygen").arg("-h").output().is_ok()
} }
/// The per-row fixture workdir — a throwaway directory under the /// The per-row fixture workdir, a throwaway directory under the
/// temp dir, one per row (the wave-5 e2e-server lesson: no shared /// temp dir, one per row (the wave-5 e2e-server lesson: no shared
/// workdirs, no cross-row races). /// workdirs, no cross-row races).
fn workdir(row: &str) -> PathBuf { fn workdir(row: &str) -> PathBuf {
@@ -113,7 +113,7 @@ mod support {
} }
/// Mints one ed25519 host key into the workdir (the independent /// Mints one ed25519 host key into the workdir (the independent
/// issuer) and returns its recorded fingerprint — byte-verbatim /// issuer) and returns its recorded fingerprint (byte-verbatim
/// from `ssh-keygen -lf`, the same `SHA256:` format the /// from `ssh-keygen -lf`, the same `SHA256:` format the
/// fingerprint parity contract pins (the policy's pin comparison /// fingerprint parity contract pins (the policy's pin comparison
/// is exact string equality by contract). /// is exact string equality by contract).
@@ -127,7 +127,7 @@ mod support {
.expect("ssh-keygen spawns"); .expect("ssh-keygen spawns");
assert!( assert!(
minted.status.success(), minted.status.success(),
"the host key mints — stderr: {}", "the host key mints; stderr: {}",
String::from_utf8_lossy(&minted.stderr) String::from_utf8_lossy(&minted.stderr)
); );
let listed = Command::new("ssh-keygen") let listed = Command::new("ssh-keygen")
@@ -145,7 +145,7 @@ mod support {
/// The in-process port allocator for the rows' named binds (the /// The in-process port allocator for the rows' named binds (the
/// e2e-server posture verbatim: a monotonic counter over a window /// e2e-server posture verbatim: a monotonic counter over a window
/// below the kernel's ephemeral range, probe-verified per /// below the kernel's ephemeral range, probe-verified per
/// allocation — parallel rows never collide). /// allocation; parallel rows never collide).
pub async fn pick_port() -> u16 { pub async fn pick_port() -> u16 {
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(2000); static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(2000);
loop { loop {
@@ -161,7 +161,7 @@ mod support {
} }
} }
/// The spawned real `sshd` — the per-row fixture (the shape /// The spawned real `sshd`, the per-row fixture (the shape
/// recorded in the module docs). The dial rides the /// recorded in the module docs). The dial rides the
/// decrypt-at-dial path over the in-memory vault double; the /// decrypt-at-dial path over the in-memory vault double; the
/// case-1 pinned policy accepts exactly the minted host key. /// case-1 pinned policy accepts exactly the minted host key.
@@ -175,7 +175,7 @@ mod support {
impl SshdFixture { impl SshdFixture {
/// A fixture dialing publickey-only with TCP forwarding /// A fixture dialing publickey-only with TCP forwarding
/// enabled (`AllowTcpForwarding yes`) — the op rows' server. /// enabled (`AllowTcpForwarding yes`), the op rows' server.
pub async fn spawn(row: &str) -> Result<Self, String> { pub async fn spawn(row: &str) -> Result<Self, String> {
Self::spawn_with(row, true).await Self::spawn_with(row, true).await
} }
@@ -301,7 +301,7 @@ mod support {
format!("127.0.0.1:{}", self.port) format!("127.0.0.1:{}", self.port)
} }
/// The case-1 pinned policy — accepts exactly this fixture's /// The case-1 pinned policy; it accepts exactly this fixture's
/// minted host fingerprint at `127.0.0.1` (the pin posture /// minted host fingerprint at `127.0.0.1` (the pin posture
/// consumed against a real presented key). A `Some` pin /// consumed against a real presented key). A `Some` pin
/// substitutes another fixture's fingerprint (the mismatch /// substitutes another fixture's fingerprint (the mismatch
@@ -316,7 +316,7 @@ mod support {
} }
/// The authenticated dial over the fixture under the given /// The authenticated dial over the fixture under the given
/// policy — the decrypt-at-dial path (fixture `alpha` sealed /// policy, the decrypt-at-dial path (fixture `alpha` sealed
/// into the in-memory vault, decrypted and loaded exactly at /// into the in-memory vault, decrypted and loaded exactly at
/// the dial), deadline-bounded. /// the dial), deadline-bounded.
pub async fn dial_from( pub async fn dial_from(
@@ -339,7 +339,7 @@ mod support {
.expect("the dial completes within the row deadline") .expect("the dial completes within the row deadline")
} }
/// The dial the op rows use — pinned to the fixture's own /// The dial the op rows use, pinned to the fixture's own
/// key, so the authenticated connection is the expected `Ok`. /// key, so the authenticated connection is the expected `Ok`.
pub async fn dial_default(&self) -> SshConnection { pub async fn dial_default(&self) -> SshConnection {
self.dial_from(self.policy(None)) self.dial_from(self.policy(None))
@@ -356,7 +356,7 @@ mod support {
} }
} }
/// The server's captured `-e` diagnostics — the skip reason's raw /// The server's captured `-e` diagnostics, the skip reason's raw
/// input, tail-truncated (best effort; a missing log is no text). /// input, tail-truncated (best effort; a missing log is no text).
fn log_tail(log: &Path) -> String { fn log_tail(log: &Path) -> String {
let Ok(mut file) = std::fs::File::open(log) else { let Ok(mut file) = std::fs::File::open(log) else {
@@ -375,7 +375,7 @@ mod support {
.join(" | ") .join(" | ")
} }
/// A tiny TCP echo server — the direct-tcpip row's real `-L` /// A tiny TCP echo server, the direct-tcpip row's real `-L`
/// target (the bytes round trip through the server-side bridge /// target (the bytes round trip through the server-side bridge
/// and this server, exactly the `-L` topology). /// and this server, exactly the `-L` topology).
pub async fn spawn_echo_server() -> u16 { pub async fn spawn_echo_server() -> u16 {
@@ -421,7 +421,7 @@ mod support {
} }
/// The exec row (the `session` build): the family-typed exec through /// The exec row (the `session` build): the family-typed exec through
/// a real server — stdout rides `data`, stderr rides /// a real server: stdout rides `data`, stderr rides
/// `extended_data(1)`, the exit status rides in-band last, and the /// `extended_data(1)`, the exit status rides in-band last, and the
/// server's reaping reports the program's real exit code (a nonzero /// server's reaping reports the program's real exit code (a nonzero
/// exit proves the parity is the server's, not a canned 0). /// exit proves the parity is the server's, not a canned 0).
@@ -434,7 +434,7 @@ mod exec_row {
use super::support; use super::support;
/// Drains the session's chunk plane until the exit chunk — /// Drains the session's chunk plane until the exit chunk;
/// returns (stdout, stderr, the exit code in-band, if any). The /// returns (stdout, stderr, the exit code in-band, if any). The
/// deadline bounds a wrong row instead of hanging it. /// deadline bounds a wrong row instead of hanging it.
async fn read_to_exit(mut session: alkssh::RemoteSession) -> (Vec<u8>, Vec<u8>, Option<i32>) { async fn read_to_exit(mut session: alkssh::RemoteSession) -> (Vec<u8>, Vec<u8>, Option<i32>) {
@@ -466,12 +466,12 @@ mod exec_row {
} }
/// - *Contract stamp:* `client.md` §op rows (row 1, the exec /// - *Contract stamp:* `client.md` §op rows (row 1, the exec
/// parity) + §The dial surface — the wave-4 double-driven exec /// parity) + §The dial surface; the wave-4 double-driven exec
/// row re-landed against a real `sshd`: the family-typed op /// row re-landed against a real `sshd`: the family-typed op
/// (`exec()`, the command verbatim) through the real server's /// (`exec()`, the command verbatim) through the real server's
/// own session reaping — stdout separated from stderr, the /// own session reaping: stdout separated from stderr, the
/// real exit status reported in-band last (`exit-status(3)`, /// real exit status reported in-band last (`exit-status(3)`,
/// parity — not a canned 0), and the case-1 pinned policy's /// parity, not a canned 0), and the case-1 pinned policy's
/// accept decision on record against the really-presented key. /// accept decision on record against the really-presented key.
#[tokio::test] #[tokio::test]
async fn real_sshd_exec_pumps_streams_and_reports_the_exit_status() { async fn real_sshd_exec_pumps_streams_and_reports_the_exit_status() {
@@ -503,7 +503,7 @@ mod exec_row {
assert_eq!( assert_eq!(
exit, exit,
Some(3), Some(3),
"the real server's reaping reports the program's exit status — parity, not a canned code" "the real server's reaping reports the program's exit status (parity, not a canned code)"
); );
assert!( assert!(
!connection.is_closed(), !connection.is_closed(),
@@ -514,7 +514,7 @@ mod exec_row {
assert_eq!( assert_eq!(
decisions.len(), decisions.len(),
1, 1,
"the dial called the policy exactly once — the real presented key" "the dial called the policy exactly once, with the real presented key"
); );
let decision = &decisions[0]; let decision = &decisions[0];
assert!(decision.accepted, "the pinned accept is the dial's verdict"); assert!(decision.accepted, "the pinned accept is the dial's verdict");
@@ -531,14 +531,14 @@ mod exec_row {
} }
/// The direct-tcpip row (the `tunnel` build): the `-L`-shaped dial /// The direct-tcpip row (the `tunnel` build): the `-L`-shaped dial
/// through a real server's forwarding — a real TCP connection to a /// through a real server's forwarding: a real TCP connection to a
/// live loopback echo server, bytes end to end. /// live loopback echo server, bytes end to end.
#[cfg(all(unix, feature = "tunnel"))] #[cfg(all(unix, feature = "tunnel"))]
mod direct_tcpip_row { mod direct_tcpip_row {
use super::support; use super::support;
/// - *Contract stamp:* `client.md` §op rows (row 2, the /// - *Contract stamp:* `client.md` §op rows (row 2, the
/// direct-tcpip dial) — the wave-4 double-driven `-L` row /// direct-tcpip dial); the wave-4 double-driven `-L` row
/// re-landed against a real server: `tunnel_direct` opens the /// re-landed against a real server: `tunnel_direct` opens the
/// channel on the real wire, the plain byte duplex pumps, and /// channel on the real wire, the plain byte duplex pumps, and
/// a **live** TCP listener on the far side answers /// a **live** TCP listener on the far side answers
@@ -592,7 +592,7 @@ mod reverse_rows {
use super::support; use super::support;
/// The acceptor double — records each open's payload fields and /// The acceptor double; it records each open's payload fields and
/// pumps the duplex as an echo (the assembly's pump shape). /// pumps the duplex as an echo (the assembly's pump shape).
#[derive(Clone)] #[derive(Clone)]
struct AcceptorDouble { struct AcceptorDouble {
@@ -627,7 +627,7 @@ mod reverse_rows {
/// - *Contract stamp:* `client.md` §op rows (row 4, the `-R` /// - *Contract stamp:* `client.md` §op rows (row 4, the `-R`
/// listen + ingest) + `reverse-runner.md` (the consumer /// listen + ingest) + `reverse-runner.md` (the consumer
/// pattern) — the wave-4 double-driven row re-landed against a /// pattern); the wave-4 double-driven row re-landed against a
/// real server: the port-0 mint returns the **actually-bound** /// real server: the port-0 mint returns the **actually-bound**
/// port (a real server's reply, verified by dialing it), the /// port (a real server's reply, verified by dialing it), the
/// server-initiated `forwarded-tcpip` open arrives into the /// server-initiated `forwarded-tcpip` open arrives into the
@@ -683,7 +683,7 @@ mod reverse_rows {
) )
.await .await
.expect("the dialer connects within the row deadline") .expect("the dialer connects within the row deadline")
.expect("the minted port is actually bound — a real server's reply, dialable locally"); .expect("the minted port is actually bound (a real server's reply, dialable locally)");
let (connected_address, connected_port, originator_address, originator_port) = let (connected_address, connected_port, originator_address, originator_port) =
tokio::time::timeout(Duration::from_secs(15), opens_rx.recv()) tokio::time::timeout(Duration::from_secs(15), opens_rx.recv())
@@ -714,7 +714,7 @@ mod reverse_rows {
} }
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// cancel form) — the wave-4 double-driven cancel row /// cancel form); the wave-4 double-driven cancel row
/// re-landed against a real server: an explicit-port listen is /// re-landed against a real server: an explicit-port listen is
/// actually bound (dialable), the listener handle's `cancel()` /// actually bound (dialable), the listener handle's `cancel()`
/// tears the **real** listen down (a later dial is refused), a /// tears the **real** listen down (a later dial is refused), a
@@ -776,7 +776,7 @@ mod reverse_rows {
} }
assert!( assert!(
torn_down, torn_down,
"the cancel frees the real bound port — connects are refused afterwards" "the cancel frees the real bound port; connects are refused afterwards"
); );
let again = connection let again = connection
@@ -796,7 +796,7 @@ mod reverse_rows {
} }
} }
/// The decline rows — the sftp placeholder (every build) and the /// The decline rows: the sftp placeholder (every build) and the
/// host-key policy mismatch (every build): both dial a real server /// host-key policy mismatch (every build): both dial a real server
/// and pin typed declines, feature-free. /// and pin typed declines, feature-free.
#[cfg(unix)] #[cfg(unix)]
@@ -808,7 +808,7 @@ mod declines {
use super::common::fixture as key_fixture; use super::common::fixture as key_fixture;
use super::support; use super::support;
/// - *Contract stamp:* OQ-13 + `client.md` §op rows (row 5) — /// - *Contract stamp:* OQ-13 + `client.md` §op rows (row 5):
/// the sftp placeholder declines in **every** build against a /// the sftp placeholder declines in **every** build against a
/// real authenticated connection: typed decline before any /// real authenticated connection: typed decline before any
/// wire op (the real server observes no subsystem request), /// wire op (the real server observes no subsystem request),
@@ -831,19 +831,19 @@ mod declines {
assert!(matches!(err, OpError::SftpNotServed), "got {err:?}"); assert!(matches!(err, OpError::SftpNotServed), "got {err:?}");
assert!( assert!(
!connection.is_closed(), !connection.is_closed(),
"the pre-wire decline touches no wire state — the connection stays up" "the pre-wire decline touches no wire state; the connection stays up"
); );
connection.close().await; connection.close().await;
fixture.stop().await; fixture.stop().await;
} }
/// - *Contract stamp:* `client.md` §The dial surface (the policy /// - *Contract stamp:* `client.md` §The dial surface (the policy
/// call at `check_server_key`, OQ-05) + Host-key §case 1 — the /// call at `check_server_key`, OQ-05) + Host-key §case 1: the
/// pin posture consumed against a real presented key: a /// pin posture consumed against a real presented key: a
/// mismatch hard-declines the dial (`DialError:: /// mismatch hard-declines the dial (`DialError::
/// HostKeyRejected`, no connection), the policy's decision log /// HostKeyRejected`, no connection), the policy's decision log
/// records the **real** fingerprint as rejected, and a /// records the **real** fingerprint as rejected, and a
/// correctly-pinned dial to the same server succeeds — the /// correctly-pinned dial to the same server succeeds; the
/// decline is the policy's decision, not the server's. /// decline is the policy's decision, not the server's.
#[tokio::test] #[tokio::test]
async fn real_presented_key_mismatch_hard_declines_and_the_right_pin_dials() { async fn real_presented_key_mismatch_hard_declines_and_the_right_pin_dials() {
@@ -869,7 +869,7 @@ mod declines {
assert_eq!( assert_eq!(
decisions.len(), decisions.len(),
1, 1,
"the dial called the policy exactly once — the real presented key" "the dial called the policy exactly once, with the real presented key"
); );
let decision = &decisions[0]; let decision = &decisions[0];
assert!( assert!(
@@ -894,7 +894,7 @@ mod declines {
} }
/// The server-refused direct-tcpip row (the `tunnel` **and** /// The server-refused direct-tcpip row (the `tunnel` **and**
/// `session` builds — the all-features posture): a real server with /// `session` builds, the all-features posture): a real server with
/// `AllowTcpForwarding no` refuses the typed open, and the /// `AllowTcpForwarding no` refuses the typed open, and the
/// connection stays usable (a following exec is served). The session /// connection stays usable (a following exec is served). The session
/// gate is the stays-usable proof's vehicle (recorded: under /// gate is the stays-usable proof's vehicle (recorded: under
@@ -910,11 +910,11 @@ mod unfeatured_direct_decline {
/// - *Contract stamp:* `client.md` §op rows (row 2's declined /// - *Contract stamp:* `client.md` §op rows (row 2's declined
/// mirror) + §Correctness facts (a decline is channel-scoped, /// mirror) + §Correctness facts (a decline is channel-scoped,
/// auth intact, connection usable) — the wave-4 typed-open /// auth intact, connection usable); the wave-4 typed-open
/// decline row re-landed against a real server's own refusal: /// decline row re-landed against a real server's own refusal:
/// `tunnel_direct` against `AllowTcpForwarding no` → /// `tunnel_direct` against `AllowTcpForwarding no` →
/// `OpenDeclined(Prohibited)` (the `administratively /// `OpenDeclined(Prohibited)` (the `administratively
/// prohibited` class), and a following exec is served — the /// prohibited` class), and a following exec is served; the
/// connection outlives the decline end to end. /// connection outlives the decline end to end.
#[tokio::test] #[tokio::test]
async fn real_server_refused_direct_tcpip_declines_and_the_connection_stays_usable() { async fn real_server_refused_direct_tcpip_declines_and_the_connection_stays_usable() {
@@ -944,14 +944,14 @@ mod unfeatured_direct_decline {
); );
assert!( assert!(
!connection.is_closed(), !connection.is_closed(),
"the decline is channel-scoped — the connection survives it" "the decline is channel-scoped; the connection survives it"
); );
let mut session = let mut session =
tokio::time::timeout(support::ROW_DEADLINE, connection.exec("echo still-usable")) tokio::time::timeout(support::ROW_DEADLINE, connection.exec("echo still-usable"))
.await .await
.expect("the exec opens within the row deadline") .expect("the exec opens within the row deadline")
.expect("the post-decline exec is served — the connection stays usable"); .expect("the post-decline exec is served; the connection stays usable");
let mut stdout = Vec::new(); let mut stdout = Vec::new();
let mut reader = alktty::wire::ChunkReader::new(&mut session.output); let mut reader = alktty::wire::ChunkReader::new(&mut session.output);
@@ -969,7 +969,7 @@ mod unfeatured_direct_decline {
} }
assert_eq!( assert_eq!(
stdout, b"still-usable\n", stdout, b"still-usable\n",
"the exec after the decline answers — auth intact, connection usable" "the exec after the decline answers; auth intact, connection usable"
); );
connection.close().await; connection.close().await;
+53 -53
View File
@@ -1,4 +1,4 @@
//! The server-direction real-tool e2e (`w5-e2e-server`) — the POC-3 //! The server-direction real-tool e2e (`w5-e2e-server`): the POC-3
//! gates re-landed against the shipped crate: real vanilla processes //! gates re-landed against the shipped crate: real vanilla processes
//! (`ssh` / `git` / `curl` / `sftp`) against the adapter mounted on a //! (`ssh` / `git` / `curl` / `sftp`) against the adapter mounted on a
//! real TCP listener over the established in-memory doubles (the //! real TCP listener over the established in-memory doubles (the
@@ -11,13 +11,13 @@
//! //!
//! **Placement decision (recorded, the task's acceptance criterion //! **Placement decision (recorded, the task's acceptance criterion
//! 1):** a dedicated `tests/e2e_server.rs` binary reusing //! 1):** a dedicated `tests/e2e_server.rs` binary reusing
//! `tests/common` — the wave-3/4 mapping binaries keep their //! `tests/common`, so the wave-3/4 mapping binaries keep their
//! single-writer posture for `w5-decline-matrix`; this binary owns //! single-writer posture for `w5-decline-matrix`; this binary owns
//! only its own rows and its in-file fixture helpers (the trimmed //! only its own rows and its in-file fixture helpers (the trimmed
//! leg-1/leg-2 fabric double for the tunnel rows, the support //! leg-1/leg-2 fabric double for the tunnel rows, the support
//! helpers). Rows arrive under the established check-gated //! helpers). Rows arrive under the established check-gated
//! **skipped-with-reason** posture (tool absent / version short — //! **skipped-with-reason** posture (tool absent / version short,
//! never silently green) and under a wall-clock deadline — a decline //! never silently green) and under a wall-clock deadline (a decline
//! must surface a clean diagnostic to the real client, never a hang //! must surface a clean diagnostic to the real client, never a hang
//! (POC-1 §5 posture). Rows carry `Contract stamp:` doc markers //! (POC-1 §5 posture). Rows carry `Contract stamp:` doc markers
//! against `mapping.md` / the POC-3 findings (the greppable //! against `mapping.md` / the POC-3 findings (the greppable
@@ -40,12 +40,12 @@ mod support {
use super::common::{MemIdentityProvider, fixture, identity}; use super::common::{MemIdentityProvider, fixture, identity};
/// One shared row deadline — every real-process interaction is /// One shared row deadline; every real-process interaction is
/// bounded (POC-1 §5: a decline never hangs). /// bounded (POC-1 §5: a decline never hangs).
pub const ROW_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30); pub const ROW_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
/// True when spawning the probe succeeds at all (exit codes are /// True when spawning the probe succeeds at all (exit codes are
/// the probe's own business — `ssh -V` writes to stderr, `sftp -h` /// the probe's own business (`ssh -V` writes to stderr, `sftp -h`
/// exits 1 with usage). /// exits 1 with usage).
pub fn tool_present(binary: &str, probe: &[&str]) -> bool { pub fn tool_present(binary: &str, probe: &[&str]) -> bool {
Command::new(binary).args(probe).output().is_ok() Command::new(binary).args(probe).output().is_ok()
@@ -53,11 +53,11 @@ mod support {
/// Prints the row's reasoned skip (never silently green). /// Prints the row's reasoned skip (never silently green).
pub fn skip(row: &str, reason: &str) { pub fn skip(row: &str, reason: &str) {
eprintln!("SKIPPED (with reason): {row} — {reason}"); eprintln!("SKIPPED (with reason): {row}: {reason}");
} }
/// The per-row throwaway workdir + the fixture key written into /// The per-row throwaway workdir + the fixture key written into
/// it (0600 — the real client's required file mode). /// it, 0600, the real client's required file mode).
pub fn write_key(row: &str) -> PathBuf { pub fn write_key(row: &str) -> PathBuf {
let work = let work =
std::env::temp_dir().join(format!("alkssh-e2e-server-{row}-{}", std::process::id())); std::env::temp_dir().join(format!("alkssh-e2e-server-{row}-{}", std::process::id()));
@@ -77,7 +77,7 @@ mod support {
} }
/// The decline-event observation for the e2e rows: poll the /// The decline-event observation for the e2e rows: poll the
/// capture until a matching event is on record — the row deadline /// capture until a matching event is on record; the row deadline
/// bounds the wait (not the capture's fixed window), so a slow CI /// bounds the wait (not the capture's fixed window), so a slow CI
/// host never turns an honest decline into a flake. /// host never turns an honest decline into a flake.
pub async fn decline_event(want: &[(&str, &str)]) -> super::common::event_capture::Captured { pub async fn decline_event(want: &[(&str, &str)]) -> super::common::event_capture::Captured {
@@ -160,7 +160,7 @@ mod support {
/// (`-L`/`-D` client-side listeners, the `-R` fabric's establisher /// (`-L`/`-D` client-side listeners, the `-R` fabric's establisher
/// bind): a monotonic counter over a window **below** the kernel's /// bind): a monotonic counter over a window **below** the kernel's
/// ephemeral port range, so parallel rows can never collect the /// ephemeral port range, so parallel rows can never collect the
/// same port and bind(0) allocations can never steal one — the /// same port and bind(0) allocations can never steal one; the
/// probe-bind re-verifies availability (a previous run's TIME_WAIT /// probe-bind re-verifies availability (a previous run's TIME_WAIT
/// tuples) and skips taken ports. /// tuples) and skips taken ports.
pub async fn pick_local_port() -> u16 { pub async fn pick_local_port() -> u16 {
@@ -208,7 +208,7 @@ mod support {
} }
} }
/// The git rows — `git` build: clone (the wave-3 smoke re-landed) and /// The git rows (`git` build): clone (the wave-3 smoke re-landed) and
/// push (the POC-3 stage-B receive-pack gate), through the git /// push (the POC-3 stage-B receive-pack gate), through the git
/// mapping over the hand-rolled recording quartet. /// mapping over the hand-rolled recording quartet.
#[cfg(unix)] #[cfg(unix)]
@@ -268,7 +268,7 @@ mod git_rows {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows (the git exec /// - *Contract stamp:* mapping.md §Channel-type rows (the git exec
/// row — `git clone ssh://…`; POC-3 §A.1 re-landed against the /// row: `git clone ssh://…`; POC-3 §A.1 re-landed against the
/// shipped crate). /// shipped crate).
#[tokio::test] #[tokio::test]
async fn real_git_clones_the_empty_repo_through_the_door() { async fn real_git_clones_the_empty_repo_through_the_door() {
@@ -309,7 +309,7 @@ mod git_rows {
let stderr = String::from_utf8_lossy(&outcome.stderr).to_string(); let stderr = String::from_utf8_lossy(&outcome.stderr).to_string();
assert!( assert!(
outcome.status.success(), outcome.status.success(),
"vanilla git clone (2.43+) through the door succeeds — stderr: {stderr}" "vanilla git clone (2.43+) through the door succeeds; stderr: {stderr}"
); );
assert!( assert!(
target.join(".git").is_dir(), target.join(".git").is_dir(),
@@ -317,7 +317,7 @@ mod git_rows {
); );
assert!( assert!(
stderr.contains("empty repository"), stderr.contains("empty repository"),
"the unborn-HEAD advertisement is what the clone consumed — stderr: {stderr}" "the unborn-HEAD advertisement is what the clone consumed; stderr: {stderr}"
); );
assert!( assert!(
doubles.resolved_ids().iter().any(|id| id == "owner/demo"), doubles.resolved_ids().iter().any(|id| id == "owner/demo"),
@@ -327,7 +327,7 @@ mod git_rows {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows (the git exec /// - *Contract stamp:* mapping.md §Channel-type rows (the git exec
/// row — `git push ssh://…` = receive-pack through the real /// row: `git push ssh://…` = receive-pack through the real
/// client; POC-3 §A stage-B gate re-landed, the wave-3 smoke's /// client; POC-3 §A stage-B gate re-landed, the wave-3 smoke's
/// clone-only coverage completed). /// clone-only coverage completed).
#[tokio::test] #[tokio::test]
@@ -362,7 +362,7 @@ mod git_rows {
.await; .await;
assert!( assert!(
init.status.success(), init.status.success(),
"the local repo initializes — stderr: {}", "the local repo initializes; stderr: {}",
String::from_utf8_lossy(&init.stderr) String::from_utf8_lossy(&init.stderr)
); );
std::fs::write(repo.join("hello.txt"), "alkssh e2e push\n") std::fs::write(repo.join("hello.txt"), "alkssh e2e push\n")
@@ -383,7 +383,7 @@ mod git_rows {
.await; .await;
assert!( assert!(
commit.status.success(), commit.status.success(),
"the commit lands — stderr: {}", "the commit lands; stderr: {}",
String::from_utf8_lossy(&commit.stderr) String::from_utf8_lossy(&commit.stderr)
); );
let head = run_git(&["-C", repo.to_str().expect("path"), "rev-parse", "HEAD"]).await; let head = run_git(&["-C", repo.to_str().expect("path"), "rev-parse", "HEAD"]).await;
@@ -415,7 +415,7 @@ mod git_rows {
let stderr = String::from_utf8_lossy(&outcome.stderr).to_string(); let stderr = String::from_utf8_lossy(&outcome.stderr).to_string();
assert!( assert!(
outcome.status.success(), outcome.status.success(),
"vanilla git push (receive-pack) through the door succeeds — stderr: {stderr}" "vanilla git push (receive-pack) through the door succeeds; stderr: {stderr}"
); );
let prepared = doubles.prepared(); let prepared = doubles.prepared();
@@ -440,7 +440,7 @@ mod git_rows {
} }
} }
/// The session rows — `session` build: the interactive pty/shell /// The session rows (`session` build): the interactive pty/shell
/// session (the wave-3 smoke re-landed, extended to the echo marker) /// session (the wave-3 smoke re-landed, extended to the echo marker)
/// and the plain pipe-exec row with its separate stderr. /// and the plain pipe-exec row with its separate stderr.
#[cfg(unix)] #[cfg(unix)]
@@ -457,7 +457,7 @@ mod session_rows {
use super::common::harness::serve_config as harness_serve_config; use super::common::harness::serve_config as harness_serve_config;
use super::support; use super::support;
/// The `local` composition for the session rows — alktty's own /// The `local` composition for the session rows: alktty's own
/// backend in the seam map (dev-dep unification: the `local` /// backend in the seam map (dev-dep unification: the `local`
/// feature rides the test graph only). /// feature rides the test graph only).
fn local_seam() -> SessionSeam { fn local_seam() -> SessionSeam {
@@ -536,17 +536,17 @@ mod session_rows {
assert!( assert!(
output.status.success(), output.status.success(),
"vanilla OpenSSH's pty+shell session through the door exits 0 \ "vanilla OpenSSH's pty+shell session through the door exits 0 \
(the exit chunk derived exit-status(0) before close) — stdout: {stdout:?}" (the exit chunk derived exit-status(0) before close); stdout: {stdout:?}"
); );
assert!( assert!(
stdout.contains("alkssh-e2e-pty"), stdout.contains("alkssh-e2e-pty"),
"the keystrokes and the shell's echo rode the real PTY round trip — stdout: {stdout:?}" "the keystrokes and the shell's echo rode the real PTY round trip; stdout: {stdout:?}"
); );
support::cleanup(&work); support::cleanup(&work);
} }
/// - *Contract stamp:* mapping.md §Request-level rows (the /// - *Contract stamp:* mapping.md §Request-level rows (the
/// `exec_request` pipe row — `wrapper ++ [cmd]`, stderr rides /// `exec_request` pipe row (`wrapper ++ [cmd]`, stderr rides
/// `extended_data(1)`; POC-3 §D.1's pipe split, end to end). /// `extended_data(1)`; POC-3 §D.1's pipe split, end to end).
#[tokio::test] #[tokio::test]
async fn real_openssh_pipe_exec_separates_stdout_and_stderr_through_the_door() { async fn real_openssh_pipe_exec_separates_stdout_and_stderr_through_the_door() {
@@ -600,23 +600,23 @@ mod session_rows {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
assert!( assert!(
output.status.success(), output.status.success(),
"vanilla `ssh host \"cmd\"` through the exec wrapper exits 0 — stdout: {stdout:?} stderr: {stderr:?}" "vanilla `ssh host \"cmd\"` through the exec wrapper exits 0; stdout: {stdout:?} stderr: {stderr:?}"
); );
assert!( assert!(
stdout.contains("alkssh-e2e-stdout"), stdout.contains("alkssh-e2e-stdout"),
"the pipe row's stdout rides SSH data — stdout: {stdout:?}" "the pipe row's stdout rides SSH data; stdout: {stdout:?}"
); );
assert!( assert!(
stderr.contains("alkssh-e2e-stderr"), stderr.contains("alkssh-e2e-stderr"),
"the pipe row's stderr rides extended_data(1) separately — stderr: {stderr:?}" "the pipe row's stderr rides extended_data(1) separately; stderr: {stderr:?}"
); );
support::cleanup(&work); support::cleanup(&work);
} }
} }
/// The tunnel rows — `tunnel` build: `-L` (the route-1 bridge), `-R` /// The tunnel rows (`tunnel` build): `-L` (the route-1 bridge), `-R`
/// (listen + arrival + data leg through the leg-2 fabric), and `-D` /// (listen + arrival + data leg through the leg-2 fabric), and `-D`
/// (curl's SOCKS traffic takes the `direct-tcpip` leg — POC-3 /// (curl's SOCKS traffic takes the `direct-tcpip` leg, POC-3
/// correction 2's posture). /// correction 2's posture).
#[cfg(unix)] #[cfg(unix)]
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
@@ -630,7 +630,7 @@ mod tunnel_rows {
use super::common::{fixture, identity}; use super::common::{fixture, identity};
use super::support; use super::support;
/// The in-file fabric fixtures — the trimmed leg-1/leg-2 fabric /// The in-file fabric fixtures (the trimmed leg-1/leg-2 fabric
/// double (the wave-3 smoke's recorded composition, POC-3 §C.4 /// double (the wave-3 smoke's recorded composition, POC-3 §C.4
/// recipe), TCP-only and happy-path only: the decline/collision /// recipe), TCP-only and happy-path only: the decline/collision
/// machinery stays the mapping binaries'. /// machinery stays the mapping binaries'.
@@ -775,7 +775,7 @@ mod tunnel_rows {
} }
/// The e2e fabric double for the `-R` row: the leg-1 shape /// The e2e fabric double for the `-R` row: the leg-1 shape
/// extended with the **bind-first listen half** — the /// extended with the **bind-first listen half**: the
/// establisher binds the wire-named endpoint for real (the /// establisher binds the wire-named endpoint for real (the
/// dynamic-address producer), mints the correlation token per /// dynamic-address producer), mints the correlation token per
/// the ADR-008 discipline, and the accept handler dials the /// the ADR-008 discipline, and the accept handler dials the
@@ -996,7 +996,7 @@ mod tunnel_rows {
} }
} }
/// A tiny HTTP/1.1 endpoint (the POC's shape) — the substrate the /// A tiny HTTP/1.1 endpoint (the POC's shape), the substrate the
/// forwarded traffic dials through the fabric. /// forwarded traffic dials through the fabric.
async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) { async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0") let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
@@ -1088,7 +1088,7 @@ mod tunnel_rows {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows (the /// - *Contract stamp:* mapping.md §Channel-type rows (the
/// `direct-tcpip` row — `ssh -L` through the route-1 bridge; /// `direct-tcpip` row: `ssh -L` through the route-1 bridge;
/// POC-3 §C.1 re-landed with the shipped crate). /// POC-3 §C.1 re-landed with the shipped crate).
#[tokio::test] #[tokio::test]
async fn real_ssh_local_forward_serves_the_direct_tcpip_leg() { async fn real_ssh_local_forward_serves_the_direct_tcpip_leg() {
@@ -1147,13 +1147,13 @@ mod tunnel_rows {
let stdout = String::from_utf8_lossy(&out.stdout).to_string(); let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!( assert!(
stdout.trim_end() == body.trim_end(), stdout.trim_end() == body.trim_end(),
"vanilla ssh -L + curl through the door → fabric → endpoint returns the body — got {stdout:?}" "vanilla ssh -L + curl through the door → fabric → endpoint returns the body; got {stdout:?}"
); );
support::cleanup(&work); support::cleanup(&work);
} }
/// - *Contract stamp:* mapping.md §Global-request rows (the `-R` /// - *Contract stamp:* mapping.md §Global-request rows (the `-R`
/// listen flow — `ssh -R`'s listen, the fabric-bound port, the /// listen flow: `ssh -R`'s listen, the fabric-bound port, the
/// server-initiated `forwarded-tcpip` arrival, the client-dialed /// server-initiated `forwarded-tcpip` arrival, the client-dialed
/// data leg; the `w3-tunnel-listen` flow re-landed against a /// data leg; the `w3-tunnel-listen` flow re-landed against a
/// real OpenSSH client). /// real OpenSSH client).
@@ -1224,13 +1224,13 @@ mod tunnel_rows {
assert!( assert!(
stdout.trim_end() == body.trim_end(), stdout.trim_end() == body.trim_end(),
"vanilla ssh -R + curl: fabric arrival → forwarded-tcpip → client-dialed endpoint returns \ "vanilla ssh -R + curl: fabric arrival → forwarded-tcpip → client-dialed endpoint returns \
the body — got {stdout:?}" the body; got {stdout:?}"
); );
support::cleanup(&work); support::cleanup(&work);
} }
/// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip` /// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip`
/// serves **all** `ssh -D` traffic per POC-3 correction 2 — the /// serves **all** `ssh -D` traffic per POC-3 correction 2; the
/// SOCKS handshake happens client-side in OpenSSH; POC-3 §B.2's /// SOCKS handshake happens client-side in OpenSSH; POC-3 §B.2's
/// corrected posture re-landed with real curl). /// corrected posture re-landed with real curl).
#[tokio::test] #[tokio::test]
@@ -1291,7 +1291,7 @@ mod tunnel_rows {
let stdout = String::from_utf8_lossy(&out.stdout).to_string(); let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!( assert!(
stdout.trim_end() == body.trim_end(), stdout.trim_end() == body.trim_end(),
"curl --socks5-hostname through vanilla ssh -D rides the door's direct-tcpip leg — \ "curl --socks5-hostname through vanilla ssh -D rides the door's direct-tcpip leg; \
got {stdout:?}" got {stdout:?}"
); );
} }
@@ -1299,7 +1299,7 @@ mod tunnel_rows {
} }
} }
/// The decline-observability row where the `session` feature is off — /// The decline-observability row where the `session` feature is off:
/// a real `ssh` pty attempt meets a clean, channel-scoped decline /// a real `ssh` pty attempt meets a clean, channel-scoped decline
/// (auth intact, no hang), with the door's decline event on record. /// (auth intact, no hang), with the door's decline event on record.
#[cfg(unix)] #[cfg(unix)]
@@ -1363,11 +1363,11 @@ mod unfeatured_session_decline {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
assert!( assert!(
!output.status.success(), !output.status.success(),
"the unfeatured pty attempt surfaces a failure to the real client — stderr: {stderr:?}" "the unfeatured pty attempt surfaces a failure to the real client; stderr: {stderr:?}"
); );
assert!( assert!(
!stderr.trim().is_empty(), !stderr.trim().is_empty(),
"the decline surfaces a clean diagnostic — stderr: {stderr:?}" "the decline surfaces a clean diagnostic; stderr: {stderr:?}"
); );
if cfg!(feature = "git") { if cfg!(feature = "git") {
let event = support::decline_event(&[ let event = support::decline_event(&[
@@ -1392,7 +1392,7 @@ mod unfeatured_session_decline {
} }
} }
/// The decline-observability rows where the `tunnel` feature is off — /// The decline-observability rows where the `tunnel` feature is off:
/// a real `ssh -R` dies at the listen gate with a clean diagnostic; /// a real `ssh -R` dies at the listen gate with a clean diagnostic;
/// a real `ssh -L`'s curls fail per connection without hanging the /// a real `ssh -L`'s curls fail per connection without hanging the
/// client; the door's decline events hold for both. /// client; the door's decline events hold for both.
@@ -1435,7 +1435,7 @@ mod unfeatured_tunnel_declines {
} }
/// - *Contract stamp:* mapping.md §Global-request rows (the gate: /// - *Contract stamp:* mapping.md §Global-request rows (the gate:
/// unfeatured `tcpip_forward` declines — POC-3 §C.2's refusal /// unfeatured `tcpip_forward` declines; POC-3 §C.2's refusal
/// shape re-landed with a real OpenSSH client and /// shape re-landed with a real OpenSSH client and
/// `ExitOnForwardFailure`). The -R row is gated here /// `ExitOnForwardFailure`). The -R row is gated here
/// (check-gated, skipped-with-reason, not silently green). /// (check-gated, skipped-with-reason, not silently green).
@@ -1480,11 +1480,11 @@ mod unfeatured_tunnel_declines {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
assert!( assert!(
!output.status.success(), !output.status.success(),
"the declined listen surfaces a failure to the real client — stderr: {stderr:?}" "the declined listen surfaces a failure to the real client; stderr: {stderr:?}"
); );
assert!( assert!(
stderr.contains("forwarding failed"), stderr.contains("forwarding failed"),
"OpenSSH's remote-forward-failed diagnostic is on record — stderr: {stderr:?}" "OpenSSH's remote-forward-failed diagnostic is on record; stderr: {stderr:?}"
); );
support::decline_event(&[ support::decline_event(&[
("row", "tcpip_forward"), ("row", "tcpip_forward"),
@@ -1496,7 +1496,7 @@ mod unfeatured_tunnel_declines {
} }
/// - *Contract stamp:* mapping.md §Channel-type rows (the /// - *Contract stamp:* mapping.md §Channel-type rows (the
/// `direct-tcpip` open gate — unfeatured declines; POC-1 §5: /// `direct-tcpip` open gate (unfeatured declines; POC-1 §5):
/// the real client observes a clean per-connection failure, /// the real client observes a clean per-connection failure,
/// no hang). /// no hang).
#[tokio::test] #[tokio::test]
@@ -1572,12 +1572,12 @@ mod unfeatured_tunnel_declines {
} }
/// The decline-observability row for the sftp subsystem decline /// The decline-observability row for the sftp subsystem decline
/// (OQ-13) — a real `sftp` client's subsystem request meets the /// (OQ-13): a real `sftp` client's subsystem request meets the
/// named, channel-scoped decline with a clean diagnostic and no hang. /// named, channel-scoped decline with a clean diagnostic and no hang.
/// ///
/// Row placement note: the sftp slot gates nothing, and the row can /// Row placement note: the sftp slot gates nothing, and the row can
/// only be exercised where session channels open (git / session /// only be exercised where session channels open (git / session
/// builds — in the none/tunnel builds the session-open decline is the /// builds; in the none/tunnel builds the session-open decline is the
/// observable, already pinned above and by the matrix suite). /// observable, already pinned above and by the matrix suite).
#[cfg(unix)] #[cfg(unix)]
#[cfg(any(feature = "session", feature = "git"))] #[cfg(any(feature = "session", feature = "git"))]
@@ -1647,11 +1647,11 @@ mod subsystem_decline {
let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string();
assert!( assert!(
!output.status.success(), !output.status.success(),
"the sftp decline surfaces a failure to the real client — stderr: {stderr:?}" "the sftp decline surfaces a failure to the real client; stderr: {stderr:?}"
); );
assert!( assert!(
!stderr.trim().is_empty(), !stderr.trim().is_empty(),
"the decline surfaces a clean diagnostic — stderr: {stderr:?}" "the decline surfaces a clean diagnostic; stderr: {stderr:?}"
); );
let event = support::decline_event(&[ let event = support::decline_event(&[
("row", "subsystem_request"), ("row", "subsystem_request"),
@@ -1665,7 +1665,7 @@ mod subsystem_decline {
} }
/// The decline-observability row for the unknown-repo collapsed /// The decline-observability row for the unknown-repo collapsed
/// generic denial — a real `git clone` of a nonexistent repo dies /// generic denial: a real `git clone` of a nonexistent repo dies
/// with the collapsed `access denied` band before any protocol byte /// with the collapsed `access denied` band before any protocol byte
/// (no existence oracle) and exit 128. /// (no existence oracle) and exit 128.
#[cfg(unix)] #[cfg(unix)]
@@ -1735,16 +1735,16 @@ mod unknown_repo_decline {
assert_eq!( assert_eq!(
outcome.status.code(), outcome.status.code(),
Some(128), Some(128),
"the collapsed denial is git's fatal-remote-error exit — stderr: {stderr:?}" "the collapsed denial is git's fatal-remote-error exit; stderr: {stderr:?}"
); );
assert!( assert!(
stderr.contains("access denied") && stderr.contains("owner/absent"), stderr.contains("access denied") && stderr.contains("owner/absent"),
"the denial names the collapsed `access denied` band with the normalized id, no existence \ "the denial names the collapsed `access denied` band with the normalized id, no existence \
oracle — stderr: {stderr:?}" oracle; stderr: {stderr:?}"
); );
assert!( assert!(
!stderr.contains("not found"), !stderr.contains("not found"),
"no repository-existence oracle leaks — stderr: {stderr:?}" "no repository-existence oracle leaks; stderr: {stderr:?}"
); );
support::cleanup(&work); support::cleanup(&work);
} }
+9 -9
View File
@@ -1,4 +1,4 @@
//! Fingerprint-format parity — ADR-004 decision 1's contract //! Fingerprint-format parity: ADR-004 decision 1's contract
//! invariant proven against independent-issuer vectors (POC-1 §1). //! invariant proven against independent-issuer vectors (POC-1 §1).
//! //!
//! The vectors are the wave-1 test-key fixture set //! The vectors are the wave-1 test-key fixture set
@@ -7,8 +7,8 @@
//! byte-verbatim from `ssh-keygen -lf` (OpenSSH 9.6p1, at //! byte-verbatim from `ssh-keygen -lf` (OpenSSH 9.6p1, at
//! vector-generation time; see `tests/vectors/fingerprints.txt`). The //! vector-generation time; see `tests/vectors/fingerprints.txt`). The
//! crate's single fingerprint path must reproduce those strings //! crate's single fingerprint path must reproduce those strings
//! exactly from every issuer shape — the committed public blob, the //! exactly from every issuer shape (the committed public blob, the
//! private key's derived public half, and the committed `.pub` file — //! private key's derived public half, and the committed `.pub` file)
//! so registration values from any standard issuer compare equal, //! so registration values from any standard issuer compare equal,
//! without cross-issuer drift. //! without cross-issuer drift.
@@ -21,7 +21,7 @@ use common::FIXTURE_KEYS;
const VECTOR_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors"); const VECTOR_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors");
/// The crate's single fingerprint path reproduces the recorded value /// The crate's single fingerprint path reproduces the recorded value
/// from the committed public blob alone — the public-key-only issuer /// from the committed public blob alone, the public-key-only issuer
/// shape (a registry that only ever sees public material). /// shape (a registry that only ever sees public material).
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint parity with /// - *Contract stamp:* ADR-004 decision 1 (fingerprint parity with
@@ -40,8 +40,8 @@ fn computed_fingerprints_match_recorded_values_for_public_blobs() {
} }
} }
/// The private-key path — the shape every auth/mapping test uses /// The private-key path, the shape every auth/mapping test uses
/// (`decode_secret_key` → sign/serve) — yields a public half whose /// (`decode_secret_key` → sign/serve), yields a public half whose
/// fingerprint is the same recorded value. /// fingerprint is the same recorded value.
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint string /// - *Contract stamp:* ADR-004 decision 1 (the fingerprint string
@@ -66,7 +66,7 @@ fn private_derived_fingerprints_match_recorded_values() {
/// value. /// value.
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (independent-issuer vectors /// - *Contract stamp:* ADR-004 decision 1 (independent-issuer vectors
/// are the parity proof — the commitment must be the recorded key). /// are the parity proof; the commitment must be the recorded key).
/// - *Contract stamp:* ADR-005 decision 1 (the fixtures' inner format /// - *Contract stamp:* ADR-005 decision 1 (the fixtures' inner format
/// is exactly the at-rest contract: unencrypted OpenSSH PEM). /// is exactly the at-rest contract: unencrypted OpenSSH PEM).
#[test] #[test]
@@ -106,7 +106,7 @@ fn committed_fixture_files_match_the_recorded_keys() {
} }
/// The recorded manifest (`tests/vectors/fingerprints.txt`) records /// The recorded manifest (`tests/vectors/fingerprints.txt`) records
/// every fixture — no fixture ships without its independent issuer's /// every fixture; no fixture ships without its independent issuer's
/// fingerprint. /// fingerprint.
/// ///
/// - *Contract stamp:* wave-1 test-double convention (recorded /// - *Contract stamp:* wave-1 test-double convention (recorded
@@ -130,7 +130,7 @@ fn recorded_manifest_covers_every_fixture() {
/// url-safe-alphabet drift (`+`/`/`, no `=`). /// url-safe-alphabet drift (`+`/`/`, no `=`).
/// ///
/// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded /// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded
/// base64, standard alphabet — byte-identical comparison by /// base64, standard alphabet; byte-identical comparison by
/// contract). /// contract).
#[test] #[test]
fn registered_string_compare_is_byte_identical() { fn registered_string_compare_is_byte_identical() {
+36 -35
View File
@@ -1,4 +1,4 @@
//! The `w3-git-mapping` contract rows — the git branch of the spine //! The `w3-git-mapping` contract rows: the git branch of the spine
//! served for real: the exec grammar routes to the published alkgit //! served for real: the exec grammar routes to the published alkgit
//! seam over the russh channel duplex (ADR-006 decision 2), the //! seam over the russh channel duplex (ADR-006 decision 2), the
//! process-mode dialect, the denial byte-order, the exit-status //! process-mode dialect, the denial byte-order, the exit-status
@@ -6,7 +6,7 @@
//! harness against the hand-rolled in-memory alkgit doubles //! harness against the hand-rolled in-memory alkgit doubles
//! (`tests/common/git_doubles.rs`). The decline-matrix rows *per //! (`tests/common/git_doubles.rs`). The decline-matrix rows *per
//! feature build* stay in `tests/dispatch_core.rs`; this binary adds //! feature build* stay in `tests/dispatch_core.rs`; this binary adds
//! the git row's serving and grammar-decline surface (feature-gated — //! the git row's serving and grammar-decline surface (feature-gated;
//! in a non-git build the binary compiles empty, per the crates' //! in a non-git build the binary compiles empty, per the crates'
//! compile-time feature isolation). //! compile-time feature isolation).
@@ -63,8 +63,8 @@ mod serving {
session session
} }
/// Opens a session channel and routes one git exec — the rows' /// Opens a session channel and routes one git exec (the rows'
/// serving driver. Asserts the routed exec's Success reply (the /// serving driver). Asserts the routed exec's Success reply (the
/// routing decision, on the wire). /// routing decision, on the wire).
pub(super) async fn route_git(session: &LoopbackSession, command: &str) -> Channel<Msg> { pub(super) async fn route_git(session: &LoopbackSession, command: &str) -> Channel<Msg> {
let mut channel = open_session_channel(&session.client) let mut channel = open_session_channel(&session.client)
@@ -83,7 +83,7 @@ mod serving {
channel channel
} }
/// Proves the channel survived a decline — a follow-up request /// Proves the channel survived a decline: a follow-up request
/// dispatches again (the POC-1 §5 session-survives-declines /// dispatches again (the POC-1 §5 session-survives-declines
/// posture; replicated from `dispatch_core.rs`). /// posture; replicated from `dispatch_core.rs`).
pub(super) async fn still_alive(channel: &mut Channel<Msg>, context: &str) { pub(super) async fn still_alive(channel: &mut Channel<Msg>, context: &str) {
@@ -247,7 +247,7 @@ mod serving {
std::str::from_utf8(&first_line).unwrap(), std::str::from_utf8(&first_line).unwrap(),
"version 2\n", "version 2\n",
"the advertisement speaks first (alkgit speaks first) and carries \ "the advertisement speaks first (alkgit speaks first) and carries \
no '# service=' smart prefix — process mode, byte-level" no '# service=' smart prefix; process mode, byte-level"
); );
let mut caps: Vec<String> = Vec::new(); let mut caps: Vec<String> = Vec::new();
@@ -269,7 +269,8 @@ mod serving {
/// `serve_duplex_resolved`) + the exit_status row (clean serve /// `serve_duplex_resolved`) + the exit_status row (clean serve
/// end → exit-status(0) **before** close, from the spawned task) /// end → exit-status(0) **before** close, from the spawned task)
/// + POC-3 §A.5 (vanilla git exits 0 on success). The registry /// + POC-3 §A.5 (vanilla git exits 0 on success). The registry
/// resolves the handed-through id (`owner/demo`) — dispatch /// resolves the handed-through id (`owner/demo`), which is the
/// dispatch
/// fidelity through the doubles. /// fidelity through the doubles.
#[tokio::test] #[tokio::test]
async fn ls_refs_round_trips_and_clean_serve_ends_exit_status_zero_before_close() { async fn ls_refs_round_trips_and_clean_serve_ends_exit_status_zero_before_close() {
@@ -351,9 +352,9 @@ mod serving {
} }
/// - *Contract stamp:* POC-3 §A.5 / alkgit ADR-008 N-2 (the /// - *Contract stamp:* POC-3 §A.5 / alkgit ADR-008 N-2 (the
/// collapsed generic denial before any protocol byte — unknown ≡ /// collapsed generic denial before any protocol byte (unknown ≡
/// unauthorized, no existence oracle): an unknown repo is *not* /// unauthorized, no existence oracle): an unknown repo is *not*
/// an alkssh decline — the seam's denial band is the first byte /// an alkssh decline): the seam's denial band is the first byte
/// on the channel, then exit-status(1) per the POC's wire /// on the channel, then exit-status(1) per the POC's wire
/// evidence, then close (status before close, spawned task). /// evidence, then close (status before close, spawned task).
#[tokio::test] #[tokio::test]
@@ -366,7 +367,7 @@ mod serving {
let band = match next_packet(&mut channel, &mut framer, "the denial band").await { let band = match next_packet(&mut channel, &mut framer, "the denial band").await {
Packet::Payload(payload) => payload, Packet::Payload(payload) => payload,
other => panic!( other => panic!(
"the collapsed denial is the first byte — no pack/ref byte precedes it, got {other:?}" "the collapsed denial is the first byte; no pack/ref byte precedes it, got {other:?}"
), ),
}; };
assert_eq!( assert_eq!(
@@ -388,10 +389,10 @@ mod serving {
/// convention 7) + the identity-ride decision (`w2-auth-bridge`'s /// convention 7) + the identity-ride decision (`w2-auth-bridge`'s
/// parked item, discharged `w3-git-mapping`): the resolved /// parked item, discharged `w3-git-mapping`): the resolved
/// identity is the *live* difference between two /// identity is the *live* difference between two
/// otherwise-identical rows — a private repo granted to the /// otherwise-identical rows: a private repo granted to the
/// riding identity serves, the same repo without the grant /// riding identity serves, the same repo without the grant
/// denies (alkgit's `authorize` evaluation witnesses the ride /// denies (alkgit's `authorize` evaluation witnesses the ride
/// through the seam — the authorize-path witness row). /// through the seam, the authorize-path witness row).
#[tokio::test] #[tokio::test]
async fn identity_ride_witnessed_by_the_authorize_gate() { async fn identity_ride_witnessed_by_the_authorize_gate() {
let granted = Arc::new(gd::MemGit::new()); let granted = Arc::new(gd::MemGit::new());
@@ -428,9 +429,9 @@ mod serving {
/// - *Contract stamp:* mapping.md §the seams (the git row: /// - *Contract stamp:* mapping.md §the seams (the git row:
/// `serve_duplex_resolved(repo_id, identity, service = push, …)` /// `serve_duplex_resolved(repo_id, identity, service = push, …)`
/// — the receive-pack direction) + alkgit ADR-020 §1 (the /// for the receive-pack direction) + alkgit ADR-020 §1 (the
/// packless CAS path: commands + flush, no pack) + ADR-013 §6–7 /// packless CAS path: commands + flush, no pack) + ADR-013 §6–7
/// (`GitRefs::apply_updates` is the single CAS home — the wire /// (`GitRefs::apply_updates` is the single CAS home; the wire
/// layer applies, the double records). /// layer applies, the double records).
#[tokio::test] #[tokio::test]
async fn packless_receive_pack_applies_the_cas_and_reports() { async fn packless_receive_pack_applies_the_cas_and_reports() {
@@ -508,7 +509,7 @@ mod serving {
} }
/// - *Contract stamp:* POC-3 §2 (unknown services declined /// - *Contract stamp:* POC-3 §2 (unknown services declined
/// honestly — the decided decline-with-reason surface; the /// honestly (the decided decline-with-reason surface; the
/// record): request-level failure, alive channel, nothing /// record): request-level failure, alive channel, nothing
/// reaches the seam. /// reaches the seam.
#[tokio::test] #[tokio::test]
@@ -546,7 +547,7 @@ mod serving {
session.unmount().await; session.unmount().await;
} }
/// - *Contract stamp:* POC-3 §A (SSH exec must carry the repo) — /// - *Contract stamp:* POC-3 §A (SSH exec must carry the repo):
/// service-only declines request-level, alive channel. /// service-only declines request-level, alive channel.
#[tokio::test] #[tokio::test]
async fn service_only_declines_request_level_with_alive_channel() { async fn service_only_declines_request_level_with_alive_channel() {
@@ -575,7 +576,7 @@ mod serving {
/// the shape gate (the parse hands the id through; invalid ids /// the shape gate (the parse hands the id through; invalid ids
/// decline at the git row, never resolve): charset, segments, /// decline at the git row, never resolve): charset, segments,
/// and the empty id all decline request-level with an alive /// and the empty id all decline request-level with an alive
/// channel — the `invalid-repo-id` class. /// channel, the `invalid-repo-id` class.
#[tokio::test] #[tokio::test]
async fn invalid_repo_ids_decline_before_the_seam() { async fn invalid_repo_ids_decline_before_the_seam() {
let doubles = Arc::new(gd::MemGit::new()); let doubles = Arc::new(gd::MemGit::new());
@@ -626,7 +627,7 @@ mod serving {
/// - *Contract stamp:* mapping.md §Data-plane/limits + /// - *Contract stamp:* mapping.md §Data-plane/limits +
/// `Limits::max_exec_command_len` (the per-length cap enforced /// `Limits::max_exec_command_len` (the per-length cap enforced
/// **before** any parse or allocation; decline-first, /// **before** any parse or allocation; decline-first,
/// alloc-never — the wave-2 carried-not-enforced reservation /// alloc-never; the wave-2 carried-not-enforced reservation
/// consumed `w3-git-mapping`): an over-length command answers /// consumed `w3-git-mapping`): an over-length command answers
/// failure, alive channel, nothing parsed. /// failure, alive channel, nothing parsed.
#[tokio::test] #[tokio::test]
@@ -688,18 +689,18 @@ mod serving {
} }
/// - *Contract stamp:* the status-before-close hardening /// - *Contract stamp:* the status-before-close hardening
/// (`w4-git-exit-close` — the wave-3 gate's booked finding 1, /// (`w4-git-exit-close`, the wave-3 gate's booked finding 1,
/// the saturated-`Sender<Msg>` tail made real on the wire): the /// the saturated-`Sender<Msg>` tail made real on the wire): the
/// serve's *answer volume* exceeds the whole outbound pipeline /// serve's *answer volume* exceeds the whole outbound pipeline
/// (the harness client's packet loop is frozen on the armed /// (the harness client's packet loop is frozen on the armed
/// data gate → the transport duplex `DUPLEX_CAPACITY` fills → /// data gate → the transport duplex `DUPLEX_CAPACITY` fills →
/// the server's flush pends → russh's bounded message queue — /// the server's flush pends → russh's bounded message queue
/// `event_buffer_size`, default 10, pinned below — fills → the /// (`event_buffer_size`, default 10, pinned below) fills → the
/// serve's remaining answer writes pend). The gate's release /// serve's remaining answer writes pend). The gate's release
/// then drives the serve tail through the just-unwinding /// then drives the serve tail through the just-unwinding
/// contention: the backlogged answer data drains **through the /// contention: the backlogged answer data drains **through the
/// same queue the report sends ride**, and the exit-status /// same queue the report sends ride**, and the exit-status
/// still lands **before** the close — the split-halves /// still lands **before** the close, the split-halves
/// construction's property (the report is the only /// construction's property (the report is the only
/// close-contracting sender; sequential awaited sends on the /// close-contracting sender; sequential awaited sends on the
/// one handle), pinning every wire row of this class against /// one handle), pinning every wire row of this class against
@@ -710,7 +711,7 @@ mod serving {
russh::server::Config::default().event_buffer_size, russh::server::Config::default().event_buffer_size,
10, 10,
"the row's construction pins russh's default message-queue capacity \ "the row's construction pins russh's default message-queue capacity \
(the saturated-tail scenario's depth) — a russh default change here \ (the saturated-tail scenario's depth); a russh default change here \
re-reads this row's freeze-settle yields, not its assertions" re-reads this row's freeze-settle yields, not its assertions"
); );
@@ -823,13 +824,13 @@ mod serving {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// The store-and-take handoff, observed: two sequential services on // The store-and-take handoff, observed: two sequential services on
// two channels of one session — the store is the session task's, // two channels of one session; the store is the session task's,
// never poisoned by the taken one. // never poisoned by the taken one.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/// - *Contract stamp:* POC-3 §A.4 (the accepted-but-held Channel → /// - *Contract stamp:* POC-3 §A.4 (the accepted-but-held Channel →
/// `channels.remove(&id)` → `into_stream()` in the spawned task; /// `channels.remove(&id)` → `into_stream()` in the spawned task;
/// the session task reuses the store) — the second serve on a /// the session task reuses the store); the second serve on a
/// fresh channel of the same session rides the same handoff. /// fresh channel of the same session rides the same handoff.
#[tokio::test] #[tokio::test]
async fn store_is_reused_across_sequential_serves() { async fn store_is_reused_across_sequential_serves() {
@@ -868,20 +869,20 @@ mod serving {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// The channel-not-held guard (`w4-channel-not-held-rows`): a second // The channel-not-held guard (`w4-channel-not-held-rows`): a second
// exec on an already-served channel declines request-level — the // exec on an already-served channel declines request-level; the
// store-and-take handoff holds a channel exactly once, the wave-3 // store-and-take handoff holds a channel exactly once, the wave-3
// gate's booked finding 2. // gate's booked finding 2.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/// - *Contract stamp:* mapping.md §Request-level rows (the git /// - *Contract stamp:* mapping.md §Request-level rows (the git
/// row's store-and-take handoff: the `channel-not-held` class — /// row's store-and-take handoff, the `channel-not-held` class:
/// a second exec on an already-served channel answers /// a second exec on an already-served channel answers
/// `channel_failure`, inline, the first serve rides on — no /// `channel_failure`, inline, the first serve rides on; no
/// second serving path ever spawns) + the event vocabulary /// second serving path ever spawns) + the event vocabulary
/// (`src/observe.rs`: class `request`, reason /// (`src/observe.rs`: class `request`, reason
/// `channel-not-held`, route `git`; the advertisement drains /// `channel-not-held`, route `git`; the advertisement drains
/// first and the serve parks at the gated `list_refs` behind it, /// first and the serve parks at the gated `list_refs` behind it,
/// so the wire is quiescent for the decline — race-free by /// so the wire is quiescent for the decline (race-free by
/// construction). /// construction).
#[tokio::test] #[tokio::test]
async fn second_exec_on_a_served_git_channel_declines_channel_not_held() { async fn second_exec_on_a_served_git_channel_declines_channel_not_held() {
@@ -913,7 +914,7 @@ mod serving {
doubles.resolved_ids(), doubles.resolved_ids(),
vec!["owner/demo".to_owned()], vec!["owner/demo".to_owned()],
"the first serve resolved and parks at the gated list_refs behind \ "the first serve resolved and parks at the gated list_refs behind \
its advertisement — the stored channel is taken, the wire quiescent" its advertisement; the stored channel is taken, the wire quiescent"
); );
request_exec(&channel, "git-upload-pack '/owner/demo'") request_exec(&channel, "git-upload-pack '/owner/demo'")
@@ -957,7 +958,7 @@ mod serving {
#[cfg(feature = "git")] #[cfg(feature = "git")]
mod real_tool { mod real_tool {
//! The real-tool smoke (`w3-git-mapping`): one `git clone` (2.43+) //! The real-tool smoke (`w3-git-mapping`): one `git clone` (2.43+)
//! through a TCP-mounted adapter against the hand-rolled doubles — //! through a TCP-mounted adapter against the hand-rolled doubles,
//! the POC-3 stage-A gate re-landed cheaply. Tooling posture //! the POC-3 stage-A gate re-landed cheaply. Tooling posture
//! (recorded): the smoke requires `git` ≥ 2.43 and `ssh` in the //! (recorded): the smoke requires `git` ≥ 2.43 and `ssh` in the
//! environment; when absent it is **skipped with a printed reason** //! environment; when absent it is **skipped with a printed reason**
@@ -977,11 +978,11 @@ mod real_tool {
async fn real_git_clone_smoke() { async fn real_git_clone_smoke() {
let (git_ok, git_reason) = git_available(); let (git_ok, git_reason) = git_available();
if !git_ok { if !git_ok {
eprintln!("SKIPPED (with reason): real-tool clone smoke — {git_reason}"); eprintln!("SKIPPED (with reason): real-tool clone smoke: {git_reason}");
return; return;
} }
if Command::new("ssh").arg("-V").output().is_err() { if Command::new("ssh").arg("-V").output().is_err() {
eprintln!("SKIPPED (with reason): real-tool clone smoke — ssh is unavailable"); eprintln!("SKIPPED (with reason): real-tool clone smoke, ssh is unavailable");
return; return;
} }
@@ -1053,7 +1054,7 @@ mod real_tool {
let stderr = String::from_utf8_lossy(&outcome.stderr).to_string(); let stderr = String::from_utf8_lossy(&outcome.stderr).to_string();
assert!( assert!(
outcome.status.success(), outcome.status.success(),
"vanilla git clone (2.43) through the door succeeds — stderr: {stderr}" "vanilla git clone (2.43) through the door succeeds; stderr: {stderr}"
); );
assert!( assert!(
target.join(".git").is_dir(), target.join(".git").is_dir(),
@@ -1061,7 +1062,7 @@ mod real_tool {
); );
assert!( assert!(
stderr.contains("empty repository"), stderr.contains("empty repository"),
"the unborn-HEAD advertisement is what the clone consumed — stderr: {stderr}" "the unborn-HEAD advertisement is what the clone consumed; stderr: {stderr}"
); );
assert!( assert!(
doubles.resolved_ids().iter().any(|id| id == "owner/demo"), doubles.resolved_ids().iter().any(|id| id == "owner/demo"),
+6 -6
View File
@@ -1,4 +1,4 @@
//! The `w2-loopback-harness` acceptance rows — the harness proved //! The `w2-loopback-harness` acceptance rows: the harness proved
//! live: a full in-process connect → authenticate (fixture key through //! live: a full in-process connect → authenticate (fixture key through
//! `MemIdentityProvider`) → authenticated-session observation through //! `MemIdentityProvider`) → authenticated-session observation through
//! the wave-2 stack, plus the observation shapes (auth failure, //! the wave-2 stack, plus the observation shapes (auth failure,
@@ -6,7 +6,7 @@
//! tests speak through the same helpers. //! tests speak through the same helpers.
//! //!
//! Every row drives the crate through the harness (`common::harness`) //! Every row drives the crate through the harness (`common::harness`)
//! — the recorded transport (tokio duplex pair, no TCP port) and the //! through it, the recorded transport (tokio duplex pair, no TCP port) and the
//! recorded client posture (fixture keys, `MemHostKeyPolicy`, the //! recorded client posture (fixture keys, `MemHostKeyPolicy`, the
//! `Limits` keepalive/wall-clock posture) are the harness's own //! `Limits` keepalive/wall-clock posture) are the harness's own
//! decisions, exercised here. //! decisions, exercised here.
@@ -46,7 +46,7 @@ fn permissive_policy() -> Arc<MemHostKeyPolicy> {
/// in-process transport) + ADR-004 decision 1 (the fixture key's /// in-process transport) + ADR-004 decision 1 (the fixture key's
/// fingerprint accepts through `MemIdentityProvider`) + ADR-005 /// fingerprint accepts through `MemIdentityProvider`) + ADR-005
/// decision 6 (the host key set loads decrypt-at-construction from /// decision 6 (the host key set loads decrypt-at-construction from
/// `MemHostKeySource` — the wave-1 doubles live through the wave-2 /// `MemHostKeySource`; the wave-1 doubles live through the wave-2
/// stack). /// stack).
#[tokio::test] #[tokio::test]
async fn in_process_pair_connects_and_authenticates_end_to_end() { async fn in_process_pair_connects_and_authenticates_end_to_end() {
@@ -91,7 +91,7 @@ async fn in_process_pair_connects_and_authenticates_end_to_end() {
/// - *Contract stamp:* ADR-001 (the authenticated session is live and /// - *Contract stamp:* ADR-001 (the authenticated session is live and
/// serving over the mount) + `mapping.md` §Decline matrix (the /// serving over the mount) + `mapping.md` §Decline matrix (the
/// fail-closed decline is observable through the harness as a /// fail-closed decline is observable through the harness as a
/// ChannelOpenFailure reason — the *specific* reason is /// ChannelOpenFailure reason; the *specific* reason is
/// `w2-dispatch-decline-core`'s row, which landed it; in builds with /// `w2-dispatch-decline-core`'s row, which landed it; in builds with
/// a session-serving feature this row never sees a decline, so it /// a session-serving feature this row never sees a decline, so it
/// is pinned to the featureless builds it speaks about). /// is pinned to the featureless builds it speaks about).
@@ -124,7 +124,7 @@ async fn authenticated_session_serves_and_declines_with_an_observed_reason() {
session.unmount().await; session.unmount().await;
} }
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject — the /// - *Contract stamp:* ADR-004 decision 1 (`None` → reject; the
/// failure shape rides the harness with the advertisement intact) /// failure shape rides the harness with the advertisement intact)
/// + ADR-004 decision 4 (the remaining-methods set is exactly /// + ADR-004 decision 4 (the remaining-methods set is exactly
/// `{publickey}`). /// `{publickey}`).
@@ -184,7 +184,7 @@ async fn fail_closed_host_key_policy_verdict_disconnects_the_dial() {
/// - *Contract stamp:* `auth.md` §Host-key policy case 1 (the /// - *Contract stamp:* `auth.md` §Host-key policy case 1 (the
/// trust-anchor shape: an exact `(host, fingerprint)` pin accepts /// trust-anchor shape: an exact `(host, fingerprint)` pin accepts
/// with a fail-closed default for everything else) — the policy /// with a fail-closed default for everything else); the policy
/// double's rule vocabulary driven over the wire. /// double's rule vocabulary driven over the wire.
#[tokio::test] #[tokio::test]
async fn pinned_policy_rule_serves_the_trust_anchor_case() { async fn pinned_policy_rule_serves_the_trust_anchor_case() {
+28 -27
View File
@@ -1,4 +1,4 @@
//! The `w3-decline-tracing` wire rows — the event presence pin: every //! The `w3-decline-tracing` wire rows, the event presence pin: every
//! decline class in `src/` (the open-level declines, the request-level //! decline class in `src/` (the open-level declines, the request-level
//! capability declines, the seam-stub serving declines with their //! capability declines, the seam-stub serving declines with their
//! spawned-path lifecycle, and the auth dispositions) emits its //! spawned-path lifecycle, and the auth dispositions) emits its
@@ -7,17 +7,17 @@
//! //!
//! **Test convention (recorded):** the family's only tracing-test //! **Test convention (recorded):** the family's only tracing-test
//! precedent is alktunnels' fmt-subscriber dev-dep, used for debug //! precedent is alktunnels' fmt-subscriber dev-dep, used for debug
//! *output* — there is no event-capture convention to adopt, so these //! *output*, and there is no event-capture convention to adopt, so these
//! rows pin the convention: a hand-rolled minimal //! rows pin the convention: a hand-rolled minimal
//! [`tracing::Subscriber`] capturing events into a shared buffer, //! [`tracing::Subscriber`] capturing events into a shared buffer,
//! installed once as the global default (the server side runs on //! installed once as the global default (the server side runs on
//! spawned tasks — a thread-local default cannot see them; a global //! spawned tasks, which a thread-local default cannot see; a global
//! capture needs no extra dependency). The install is up front — the //! capture needs no extra dependency). The install is up front; the
//! mount helpers call [`capture`] before any wire exchange, so no //! mount helpers call [`capture`] before any wire exchange, so no
//! event is emitted below the not-yet-installed global. Rows assert //! event is emitted below the not-yet-installed global. Rows assert
//! presence by field-matching (`row` + `decline_class` + `reason` + //! presence by field-matching (`row` + `decline_class` + `reason` +
//! build), and the vocabulary assertion checks each captured decline's exact //! build), and the vocabulary assertion checks each captured decline's exact
//! field set — the shape the three wave-3 mapping branches inherit. //! field set, the shape the three wave-3 mapping branches inherit.
//! The helper-level shapes (field order, the `warn` level of the //! The helper-level shapes (field order, the `warn` level of the
//! failure arm) are pinned in `src/observe.rs`'s own unit rows. //! failure arm) are pinned in `src/observe.rs`'s own unit rows.
//! //!
@@ -44,7 +44,7 @@ use russh::{Channel, ChannelMsg};
use alkssh::Limits; use alkssh::Limits;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The capture subscriber — the recorded test convention (see module docs). // The capture subscriber, the recorded test convention (see module docs).
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -122,7 +122,7 @@ fn capture() -> &'static Arc<GlobalCapture> {
}) })
} }
/// The shared capture snapshot — every test reads the whole buffer and /// The shared capture snapshot: every test reads the whole buffer and
/// matches fields, so parallel rows never order against each other. /// matches fields, so parallel rows never order against each other.
fn captured() -> Vec<Captured> { fn captured() -> Vec<Captured> {
capture().0.lock().unwrap().clone() capture().0.lock().unwrap().clone()
@@ -144,7 +144,7 @@ async fn await_event(want: &[(&str, &str)]) -> Captured {
} }
} }
/// The expected `feature_build` value, derived from `cfg!` — the /// The expected `feature_build` value, derived from `cfg!`; the
/// vocabulary assertion checks every captured decline against it (the /// vocabulary assertion checks every captured decline against it (the
/// same convention as `src/observe.rs`'s cfg-derived unit row). /// same convention as `src/observe.rs`'s cfg-derived unit row).
fn expected_build() -> &'static str { fn expected_build() -> &'static str {
@@ -173,7 +173,7 @@ fn expected_build() -> &'static str {
} }
} }
/// The global-request rows — the no-channel surfaces (the event /// The global-request rows, the no-channel surfaces (the event
/// vocabulary assertion needs to know the field set these carry). /// vocabulary assertion needs to know the field set these carry).
fn is_global_row(row: Option<&str>) -> bool { fn is_global_row(row: Option<&str>) -> bool {
row.is_some_and(|row| { row.is_some_and(|row| {
@@ -188,7 +188,7 @@ fn is_global_row(row: Option<&str>) -> bool {
} }
/// The vocabulary assertion: a captured decline carries the exact /// The vocabulary assertion: a captured decline carries the exact
/// field set (nothing more — the safety posture rides on this: no /// field set (nothing more; the safety posture rides on this: no
/// dynamic extra fields exist), with the build/class/reason/row the /// dynamic extra fields exist), with the build/class/reason/row the
/// caller matched on. /// caller matched on.
fn assert_decline(event: &Captured, want: &[(&str, &str)]) { fn assert_decline(event: &Captured, want: &[(&str, &str)]) {
@@ -239,10 +239,10 @@ fn assert_decline(event: &Captured, want: &[(&str, &str)]) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared rows — run in every feature build. // Shared rows; run in every feature build.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature — the open /// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature; the open
/// declines in every build; the event pins the deciding context). /// declines in every build; the event pins the deciding context).
#[tokio::test] #[tokio::test]
async fn x11_open_decline_emits_the_never_feature_event() { async fn x11_open_decline_emits_the_never_feature_event() {
@@ -261,12 +261,12 @@ async fn x11_open_decline_emits_the_never_feature_event() {
} }
/// - *Contract stamp:* mapping.md §Global-request rows (the `-R` /// - *Contract stamp:* mapping.md §Global-request rows (the `-R`
/// listen form declines this build's flow — the event carries no /// listen form declines this build's flow (the event carries no
/// channel id: global requests have none). The reason is the /// channel id: global requests have none). The reason is the
/// build's gate: `unfeatured` where the feature is off, /// build's gate: `unfeatured` where the feature is off,
/// `seam-unconfigured` where the tunnel feature is on but no /// `seam-unconfigured` where the tunnel feature is on but no
/// fabric leg/listen seam is configured (the unset-declines /// fabric leg/listen seam is configured (the unset-declines
/// posture — a bare `authenticated()` session configures neither). /// posture; a bare `authenticated()` session configures neither).
#[cfg(not(feature = "tunnel"))] #[cfg(not(feature = "tunnel"))]
#[tokio::test] #[tokio::test]
async fn tcpip_forward_decline_emits_the_global_request_event() { async fn tcpip_forward_decline_emits_the_global_request_event() {
@@ -289,7 +289,7 @@ async fn tcpip_forward_decline_emits_the_global_request_event() {
/// - *Contract stamp:* mapping.md §Global-request rows + the /// - *Contract stamp:* mapping.md §Global-request rows + the
/// decline-matrix's config-driven notes (the `w3-tunnel-listen` /// decline-matrix's config-driven notes (the `w3-tunnel-listen`
/// migrated stub row): a featured build whose fabric leg/listen /// migrated stub row): a featured build whose fabric leg/listen
/// seam is unconfigured declines `seam-unconfigured` — the feature /// seam is unconfigured declines `seam-unconfigured`; the feature
/// alone configures nothing. /// alone configures nothing.
#[cfg(feature = "tunnel")] #[cfg(feature = "tunnel")]
#[tokio::test] #[tokio::test]
@@ -310,7 +310,7 @@ async fn tcpip_forward_decline_emits_the_seam_unconfigured_event() {
session.unmount().await; session.unmount().await;
} }
/// - *Contract stamp:* ADR-004 (the bridge rejects fail-closed — /// - *Contract stamp:* ADR-004 (the bridge rejects fail-closed:
/// `None` resolution → reject disposition; the event pins the /// `None` resolution → reject disposition; the event pins the
/// decline without exposing who). /// decline without exposing who).
#[tokio::test] #[tokio::test]
@@ -363,7 +363,7 @@ async fn auth_none_decline_emits_the_auth_event() {
} }
/// - *Contract stamp:* auth.md §Dispositions (log exposure is /// - *Contract stamp:* auth.md §Dispositions (log exposure is
/// deployment policy — the crate's events are safe-by-default: no /// deployment policy; the crate's events are safe-by-default: no
/// username, secret, or fingerprint ever enters an event field or /// username, secret, or fingerprint ever enters an event field or
/// message; `w3-decline-tracing`'s safety posture, pinned by /// message; `w3-decline-tracing`'s safety posture, pinned by
/// scanning every captured event across the whole binary's rows). /// scanning every captured event across the whole binary's rows).
@@ -426,7 +426,7 @@ mod default_build {
use common::harness::open_session_channel; use common::harness::open_session_channel;
/// - *Contract stamp:* mapping.md §Decline matrix (the none-build /// - *Contract stamp:* mapping.md §Decline matrix (the none-build
/// row: everything declines — the event answers the booked /// row: everything declines; the event answers the booked
/// finding: the decline's deciding context is observable). /// finding: the decline's deciding context is observable).
#[tokio::test] #[tokio::test]
async fn unfeatured_session_open_emits_the_open_event() { async fn unfeatured_session_open_emits_the_open_event() {
@@ -453,11 +453,11 @@ mod git_build {
request_subsystem, request_x11, request_subsystem, request_x11,
}; };
/// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return — the /// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return; the
/// serving path is spawned; the lifecycle events started / /// serving path is spawned; the lifecycle events started /
/// decline / ended ride the spawned task, in wiring order). The /// decline / ended ride the spawned task, in wiring order). The
/// git row's decline in a seam-less build is config-driven /// git row's decline in a seam-less build is config-driven
/// (`w3-git-mapping`: the unset-declines contract — the reason /// (`w3-git-mapping`: the unset-declines contract, the reason
/// names the seam, not the stub). /// names the seam, not the stub).
#[tokio::test] #[tokio::test]
async fn git_seam_stub_declines_with_the_full_serving_lifecycle() { async fn git_seam_stub_declines_with_the_full_serving_lifecycle() {
@@ -623,7 +623,7 @@ mod session_build {
/// - *Contract stamp:* ADR-006 decision 4 (the pty serving decline /// - *Contract stamp:* ADR-006 decision 4 (the pty serving decline
/// is spawned; the lifecycle rides the task) + OQ-08 (the /// is spawned; the lifecycle rides the task) + OQ-08 (the
/// unset-seam decline: feature on, seam unset — the config- /// unset-seam decline (feature on, seam unset), the config-
/// driven class the real paths inherit, /// driven class the real paths inherit,
/// `w3-tty-session`). /// `w3-tty-session`).
#[tokio::test] #[tokio::test]
@@ -669,7 +669,7 @@ mod session_build {
} }
/// - *Contract stamp:* OQ-08 + the event vocabulary (`w3-tty- /// - *Contract stamp:* OQ-08 + the event vocabulary (`w3-tty-
/// session`: the unset-field pipe decline — /// session`: the unset-field pipe decline,
/// `wrapper-unconfigured`, the serving-path class from a /// `wrapper-unconfigured`, the serving-path class from a
/// spawned task). /// spawned task).
#[tokio::test] #[tokio::test]
@@ -703,7 +703,7 @@ mod tunnel_build {
/// - *Contract stamp:* the unset-declines posture (`w3-seam-config`, /// - *Contract stamp:* the unset-declines posture (`w3-seam-config`,
/// carried through `w3-tunnel-direct`: the route-1 bridge's /// carried through `w3-tunnel-direct`: the route-1 bridge's
/// pre-admit classes) — the direct row's absent-handle decline /// pre-admit classes): the direct row's absent-handle decline
/// emits the `seam-unconfigured` open-class event (the fabric /// emits the `seam-unconfigured` open-class event (the fabric
/// double's real decline events ride tests/tunnel_mapping.rs). /// double's real decline events ride tests/tunnel_mapping.rs).
#[tokio::test] #[tokio::test]
@@ -766,12 +766,13 @@ async fn authenticated(username: &str) -> common::harness::LoopbackSession {
session session
} }
/// A mounted (kex-complete, **unauthenticated**) loopback session — /// A mounted (kex-complete, **unauthenticated**) loopback session,
/// the auth-decline rows' driver: an auth disposition is observed /// the auth-decline rows' driver: an auth disposition is observed
/// before any acceptance exists, so these rows must not pre-derive /// before any acceptance exists, so these rows must not pre-derive
/// themselves through [`authenticated`] (a second auth exchange on an /// themselves through [`authenticated`] (a second auth exchange on an
/// authenticated session is not a supported russh surface — mounted /// authenticated session is not a supported russh surface, so it is
/// fresh is the honest shape, matching `auth_bridge.rs`'s rows). /// mounted fresh; that is the honest shape, matching
/// `auth_bridge.rs`'s rows).
async fn mounted() -> common::harness::LoopbackSession { async fn mounted() -> common::harness::LoopbackSession {
capture(); capture();
let provider = Arc::new(MemIdentityProvider::new()); let provider = Arc::new(MemIdentityProvider::new());
@@ -799,7 +800,7 @@ async fn expect_failure_then_close(channel: &mut Channel<Msg>) {
"the seam stub closes the channel" "the seam stub closes the channel"
); );
// Yield so the spawned task's post-close events (ended) are likely // Yield so the spawned task's post-close events (ended) are likely
// to have landed before the unmount — await_event's polling covers // to have landed before the unmount; await_event's polling covers
// the rest. // the rest.
tokio::time::sleep(Duration::from_millis(50)).await; tokio::time::sleep(Duration::from_millis(50)).await;
} }
+20 -20
View File
@@ -1,4 +1,4 @@
//! The spawn-and-return mechanics proof (`w2-spawn-return`) — the //! The spawn-and-return mechanics proof (`w2-spawn-return`), the
//! structural constraint (ADR-006 decision 4) pinned as tests, over //! structural constraint (ADR-006 decision 4) pinned as tests, over
//! the real wire. //! the real wire.
//! //!
@@ -7,7 +7,7 @@
//! peer's follow-up bytes (POC-3 proved by construction with //! peer's follow-up bytes (POC-3 proved by construction with
//! receive-pack). The crate's plumbing //! receive-pack). The crate's plumbing
//! ([`alkssh::spawn_serving_path`], //! ([`alkssh::spawn_serving_path`],
//! [`alkssh::report_exit_status_then_close`] — the `#[doc(hidden)]` //! [`alkssh::report_exit_status_then_close`], the `#[doc(hidden)]`
//! contract-test re-exports from `src/serve.rs`) makes the escape //! contract-test re-exports from `src/serve.rs`) makes the escape
//! mechanical: the handler hook clones `session.handle()`, spawns the //! mechanical: the handler hook clones `session.handle()`, spawns the
//! serving future, and returns. //! serving future, and returns.
@@ -16,10 +16,10 @@
//! proof handler stands in for the crate's own auth/dispatch hooks //! proof handler stands in for the crate's own auth/dispatch hooks
//! (their correctness is `w2-auth-bridge`/`w2-dispatch-decline-core` //! (their correctness is `w2-auth-bridge`/`w2-dispatch-decline-core`
//! contract territory; the `dispatch` seam stubs now spawn through the //! contract territory; the `dispatch` seam stubs now spawn through the
//! same plumbing, so the crate's own rows exercise the spawn too — //! same plumbing, so the crate's own rows exercise the spawn too;
//! `tests/dispatch_core.rs`). What is under test here is the //! `tests/dispatch_core.rs`). What is under test here is the
//! *mechanics*: the spawn-then-return, the cloned-handle reporting, //! *mechanics*: the spawn-then-return, the cloned-handle reporting,
//! and the wire ordering — `exit-status` before close, arriving after //! and the wire ordering (`exit-status` before close, arriving after
//! the handler already returned, while the session's packet loop keeps //! the handler already returned, while the session's packet loop keeps
//! routing the connection's follow-up traffic. //! routing the connection's follow-up traffic.
@@ -41,14 +41,14 @@ use russh::{Channel, ChannelId, ChannelMsg, Disconnect, Error, MethodKind, Metho
use alkssh::Limits; use alkssh::Limits;
/// The exit status the proof stub reports — an arbitrary distinct /// The exit status the proof stub reports, an arbitrary distinct
/// value the rows assert verbatim (`exit-status(7)`). /// value the rows assert verbatim (`exit-status(7)`).
const PROOF_EXIT: u32 = 7; const PROOF_EXIT: u32 = 7;
/// The command the rows exec — anything; the proof handler ignores it. /// The command the rows exec (anything; the proof handler ignores it).
const PROOF_COMMAND: &str = "mechanics-proof-serving-path"; const PROOF_COMMAND: &str = "mechanics-proof-serving-path";
/// The async completion gate the spawned serving paths hold on — /// The async completion gate the spawned serving paths hold on,
/// released only after the rows have proven the handler returned. /// released only after the rows have proven the handler returned.
/// `watch`-based (no lost-wakeup race: a late subscriber still sees /// `watch`-based (no lost-wakeup race: a late subscriber still sees
/// the release). /// the release).
@@ -56,7 +56,7 @@ fn serving_gate() -> tokio::sync::watch::Sender<bool> {
tokio::sync::watch::channel(false).0 tokio::sync::watch::channel(false).0
} }
/// The spawned task's wait: blocks until (or returns if) released — /// The spawned task's wait: blocks until (or returns if) released;
/// `borrow_and_update` marks the release seen so the wakeup is never /// `borrow_and_update` marks the release seen so the wakeup is never
/// lost or replayed. /// lost or replayed.
async fn gate_wait(rx: &mut tokio::sync::watch::Receiver<bool>) { async fn gate_wait(rx: &mut tokio::sync::watch::Receiver<bool>) {
@@ -65,7 +65,7 @@ async fn gate_wait(rx: &mut tokio::sync::watch::Receiver<bool>) {
} }
} }
/// What the proof handler records — the timestamps the rows compare. /// What the proof handler records: the timestamps the rows compare.
#[derive(Default)] #[derive(Default)]
struct ProofRecord { struct ProofRecord {
handler_returned_at: Option<Instant>, handler_returned_at: Option<Instant>,
@@ -74,7 +74,7 @@ struct ProofRecord {
type Shared = Arc<Mutex<ProofRecord>>; type Shared = Arc<Mutex<ProofRecord>>;
/// The proof handler (server side): mechanics-only — it authenticates /// The proof handler (server side): mechanics-only. It authenticates
/// the fixture alpha by the crate's one fingerprint path, accepts /// the fixture alpha by the crate's one fingerprint path, accepts
/// session channels, and serves every exec by *the crate's own /// session channels, and serves every exec by *the crate's own
/// spawn-and-return plumbing*: a serving path gated on the row's /// spawn-and-return plumbing*: a serving path gated on the row's
@@ -96,7 +96,7 @@ impl russh::server::Handler for ProofServer {
Ok(Auth::Accept) Ok(Auth::Accept)
} }
/// The mechanics rows' own fixture check — fingerprint equality /// The mechanics rows' own fixture check: fingerprint equality
/// against fixture alpha through the crate's fingerprint path /// against fixture alpha through the crate's fingerprint path
/// (not the bridge contract; that is `w2-auth-bridge`'s suite). /// (not the bridge contract; that is `w2-auth-bridge`'s suite).
async fn auth_publickey( async fn auth_publickey(
@@ -124,7 +124,7 @@ impl russh::server::Handler for ProofServer {
} }
/// The hook under proof: clone the handle, spawn the serving path /// The hook under proof: clone the handle, spawn the serving path
/// (gated — it stays pending past this method's return), record /// (gated, it stays pending past this method's return), record
/// the handler's return instant, and go. /// the handler's return instant, and go.
async fn exec_request( async fn exec_request(
&mut self, &mut self,
@@ -151,7 +151,7 @@ impl russh::server::Handler for ProofServer {
/// server task (russh's own stack over the proof handler), and the /// server task (russh's own stack over the proof handler), and the
/// connected client. Transport and posture mirror the loopback /// connected client. Transport and posture mirror the loopback
/// harness (its recorded decisions); the handler is intentionally not /// harness (its recorded decisions); the handler is intentionally not
/// the crate's `SessionHandler` — the crate stack under a custom /// the crate's `SessionHandler`; the crate stack under a custom
/// serving hook is the proof's subject. /// serving hook is the proof's subject.
struct ProofSession { struct ProofSession {
client: russh::client::Handle<HarnessClientHandler>, client: russh::client::Handle<HarnessClientHandler>,
@@ -210,7 +210,7 @@ async fn mount() -> ProofSession {
} }
impl ProofSession { impl ProofSession {
/// Releases the gate — the spawned serving paths complete /// Releases the gate; the spawned serving paths complete
/// asynchronously from here on. /// asynchronously from here on.
fn release_gate(&self) { fn release_gate(&self) {
let _ = self.gate.send(true); let _ = self.gate.send(true);
@@ -228,7 +228,7 @@ impl ProofSession {
} }
/// Waits (bounded) until the exec hook has provably run and /// Waits (bounded) until the exec hook has provably run and
/// returned — the client's `exec` returns when the request is on /// returned; the client's `exec` returns when the request is on
/// the wire; the loop's processing of it is observed here. /// the wire; the loop's processing of it is observed here.
async fn exec_hook_has_returned(&self) { async fn exec_hook_has_returned(&self) {
tokio::time::timeout(std::time::Duration::from_secs(5), async { tokio::time::timeout(std::time::Duration::from_secs(5), async {
@@ -241,7 +241,7 @@ impl ProofSession {
} }
/// The handler provably returned while the spawned serving path /// The handler provably returned while the spawned serving path
/// was still pending — the handler-returns-fast assertion. /// was still pending, the handler-returns-fast assertion.
fn assert_handler_returned_spawn_still_pending(&self, context: &str) { fn assert_handler_returned_spawn_still_pending(&self, context: &str) {
let record = self.record.lock(); let record = self.record.lock();
assert!( assert!(
@@ -250,7 +250,7 @@ impl ProofSession {
); );
assert!( assert!(
record.exit_reported_at.is_none(), record.exit_reported_at.is_none(),
"{context}: the spawned serving path is still gated — nothing reported yet" "{context}: the spawned serving path is still gated; nothing reported yet"
); );
} }
} }
@@ -260,7 +260,7 @@ impl ProofSession {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// - *Contract stamp:* ADR-006 decision 4 (the structural constraint: /// - *Contract stamp:* ADR-006 decision 4 (the structural constraint:
/// every serving path is spawned — the handler completes while the /// every serving path is spawned (the handler completes while the
/// spawned task is still running) + mapping.md §The structural /// spawned task is still running) + mapping.md §The structural
/// constraint: spawn-and-return (the follow-up-bytes deadlock /// constraint: spawn-and-return (the follow-up-bytes deadlock
/// inverted: the connection's loop keeps routing while a serving /// inverted: the connection's loop keeps routing while a serving
@@ -279,7 +279,7 @@ async fn handler_returns_fast_while_the_spawned_serving_path_is_pending() {
session.exec_hook_has_returned().await; session.exec_hook_has_returned().await;
session.assert_handler_returned_spawn_still_pending("after the exec"); session.assert_handler_returned_spawn_still_pending("after the exec");
// The follow-up-bytes inversion — POC-3's deadlock shape: the peer // The follow-up-bytes inversion, POC-3's deadlock shape: the peer
// sends immediate follow-up traffic and the loop routes it while // sends immediate follow-up traffic and the loop routes it while
// the first serving path is still pending. // the first serving path is still pending.
let follow_up = open_session_channel(&session.client) let follow_up = open_session_channel(&session.client)
@@ -335,7 +335,7 @@ async fn spawned_stub_reports_exit_status_before_close_after_the_handler_returne
}; };
// Strict wire order, message by message: exit-status first, close // Strict wire order, message by message: exit-status first, close
// after — the ordering contract verbatim. // after, the ordering contract verbatim.
let first = next_message(&mut channel) let first = next_message(&mut channel)
.await .await
.expect("the released serving path reports"); .expect("the released serving path reports");
+42 -42
View File
@@ -1,15 +1,15 @@
//! The `w3-tty-session` contract rows — the session branch of the //! The `w3-tty-session` contract rows: the session branch of the
//! spine served for real: pty/shell/pipe-exec through the published //! spine served for real: pty/shell/pipe-exec through the published
//! alktty seam (`drive_session_pre_negotiated`) over the translation //! alktty seam (`drive_session_pre_negotiated`) over the translation
//! bridge, the locally-built negotiation, the control rows, the exit //! bridge, the locally-built negotiation, the control rows, the exit
//! derivation contract (mapping.md rules 1–6), and the //! derivation contract (mapping.md rules 1–6), and the
//! negotiation-error decline class — all observed through the //! and the negotiation-error decline class; all observed through the
//! loopback harness against the hand-rolled TtyBackend shim //! loopback harness against the hand-rolled TtyBackend shim
//! (`tests/common/tty_doubles.rs`). The decline-matrix rows *per //! (`tests/common/tty_doubles.rs`). The decline-matrix rows *per
//! feature build* stay in `tests/dispatch_core.rs`; this binary adds //! feature build* stay in `tests/dispatch_core.rs`; this binary adds
//! the session rows' serving surface, the unset-field declines (which //! the session rows' serving surface, the unset-field declines (which
//! need a configured seam to isolate), and the real-tool pty smoke //! need a configured seam to isolate), and the real-tool pty smoke
//! (feature-gated — in a `session`-less build the binary compiles //! (feature-gated: in a `session`-less build the binary compiles
//! empty, per the crates' compile-time feature isolation). //! empty, per the crates' compile-time feature isolation).
#![allow(dead_code)] #![allow(dead_code)]
@@ -37,7 +37,7 @@ mod serving {
use super::common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity}; use super::common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity};
use alkssh::{Limits, ServeConfig, SshAdapter}; use alkssh::{Limits, ServeConfig, SshAdapter};
/// The observation surface on the wire — the terminal-observation /// The observation surface on the wire, the terminal-observation
/// enum these rows drive (russh-verbatim shapes, drained in order /// enum these rows drive (russh-verbatim shapes, drained in order
/// until the deadline). /// until the deadline).
#[derive(Debug)] #[derive(Debug)]
@@ -50,7 +50,7 @@ mod serving {
Failure, Failure,
} }
/// The next observation with a deadline — None past 10 s (nothing /// The next observation with a deadline; None past 10 s (nothing
/// else can arrive; a row hanging past the deadline is its bug). /// else can arrive; a row hanging past the deadline is its bug).
pub(super) async fn next_observation(channel: &mut Channel<Msg>) -> Option<Observed> { pub(super) async fn next_observation(channel: &mut Channel<Msg>) -> Option<Observed> {
tokio::time::timeout(std::time::Duration::from_secs(10), async { tokio::time::timeout(std::time::Duration::from_secs(10), async {
@@ -86,7 +86,7 @@ mod serving {
} }
/// Drains observations until the terminal outcome (status or /// Drains observations until the terminal outcome (status or
/// signal) — collecting the data rides for the ride rows, and /// signal), collecting the data rides for the ride rows, and
/// asserting the close follows after (the status-before-close /// asserting the close follows after (the status-before-close
/// order). /// order).
pub(super) async fn collect_terminal( pub(super) async fn collect_terminal(
@@ -127,7 +127,7 @@ mod serving {
} }
} }
/// Waits for one Success reply (want-reply requests) — with a /// Waits for one Success reply (want-reply requests), with a
/// deadline; a Failure or a silent close is the panic, the rows /// deadline; a Failure or a silent close is the panic, the rows
/// drive Success paths. /// drive Success paths.
pub(super) async fn expect_success(channel: &mut Channel<Msg>, context: &str) { pub(super) async fn expect_success(channel: &mut Channel<Msg>, context: &str) {
@@ -182,7 +182,7 @@ mod serving {
panic!("no signal reached the shim's control"); panic!("no signal reached the shim's control");
} }
/// Builds the session rows' serve-config — everything set (the /// Builds the session rows' serve-config, everything set (the
/// shim seam, the shell argv, the exec wrapper, session cwd/env), /// shim seam, the shell argv, the exec wrapper, session cwd/env),
/// overridable per row through the mutator. The identity resolves /// overridable per row through the mutator. The identity resolves
/// from the `alpha` fixture into `ssh-rider` (the witness id). /// from the `alpha` fixture into `ssh-rider` (the witness id).
@@ -203,12 +203,12 @@ mod serving {
} }
/// Mounts an authenticated loopback session over the given config /// Mounts an authenticated loopback session over the given config
/// — the rows' driver (fixture `alpha` → `ssh-rider`). /// (the rows' driver; fixture `alpha` → `ssh-rider`).
pub(super) async fn session_with(mutate: impl FnOnce(&mut ServeConfig)) -> LoopbackSession { pub(super) async fn session_with(mutate: impl FnOnce(&mut ServeConfig)) -> LoopbackSession {
session_with_limits(mutate, Limits::default()).await session_with_limits(mutate, Limits::default()).await
} }
/// [`session_with`] over non-default [`Limits`] — the rows that /// [`session_with`] over non-default [`Limits`], for the rows that
/// tune a budget (the channel-cap release row's ceiling of 1). /// tune a budget (the channel-cap release row's ceiling of 1).
pub(super) async fn session_with_limits( pub(super) async fn session_with_limits(
mutate: impl FnOnce(&mut ServeConfig), mutate: impl FnOnce(&mut ServeConfig),
@@ -235,13 +235,13 @@ mod serving {
session session
} }
/// A shim-backed seam — the rows' default (no ownership provider). /// A shim-backed seam, the rows' default (no ownership provider).
pub(super) fn shim_seam(shim: &Arc<td::TtyShim>) -> impl FnOnce(&mut ServeConfig) { pub(super) fn shim_seam(shim: &Arc<td::TtyShim>) -> impl FnOnce(&mut ServeConfig) {
let seam = td::tty_seam(shim); let seam = td::tty_seam(shim);
move |serve: &mut ServeConfig| serve.session_seam = Some(seam) move |serve: &mut ServeConfig| serve.session_seam = Some(seam)
} }
/// Opens a session channel and sends the pty+shell request pair — /// Opens a session channel and sends the pty+shell request pair;
/// replaced per-row below (the rows drive their own sequences). /// replaced per-row below (the rows drive their own sequences).
#[tokio::test] #[tokio::test]
@@ -338,7 +338,7 @@ mod serving {
assert_eq!( assert_eq!(
params.cmd, params.cmd,
vec!["/bin/wrap".to_owned(), "-x".to_owned(), "uptime".to_owned()], vec!["/bin/wrap".to_owned(), "-x".to_owned(), "uptime".to_owned()],
"the wire command appended to the wrapper argv — never naively split" "the wire command appended to the wrapper argv, never naively split"
); );
assert!( assert!(
params.terminal.is_none(), params.terminal.is_none(),
@@ -421,13 +421,13 @@ mod serving {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// The channel-not-held guard (`w4-channel-not-held-rows`): a second // The channel-not-held guard (`w4-channel-not-held-rows`): a second
// request on an already-served channel declines request-level — the // request on an already-served channel declines request-level; the
// store-and-take handoff holds a channel exactly once, the wave-3 // store-and-take handoff holds a channel exactly once, the wave-3
// gate's booked finding 2. // gate's booked finding 2.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/// - *Contract stamp:* mapping.md §Request-level rows (the exec /// - *Contract stamp:* mapping.md §Request-level rows (the exec
/// row's store-and-take handoff: the `channel-not-held` class — /// row's store-and-take handoff, the `channel-not-held` class:
/// a second exec on an already-served pipe channel answers /// a second exec on an already-served pipe channel answers
/// `channel_failure`, inline, the first serve rides on) + the /// `channel_failure`, inline, the first serve rides on) + the
/// event vocabulary (`src/observe.rs`: class `request`, reason /// event vocabulary (`src/observe.rs`: class `request`, reason
@@ -485,12 +485,12 @@ mod serving {
/// - *Contract stamp:* mapping.md §Request-level rows (the /// - *Contract stamp:* mapping.md §Request-level rows (the
/// `shell_request` row's store-and-take handoff: the /// `shell_request` row's store-and-take handoff: the
/// `channel-not-held` class — a second shell request on an /// `channel-not-held` class: a second shell request on an
/// already-served shell channel answers `channel_failure`, /// already-served shell channel answers `channel_failure`,
/// inline, the first serve rides on) + the event vocabulary /// inline, the first serve rides on) + the event vocabulary
/// (`src/observe.rs`: class `request`, reason /// (`src/observe.rs`: class `request`, reason
/// `channel-not-held`; the lifecycle row carries no /// `channel-not-held`; the lifecycle row carries no
/// `exec_route` — absent-field shape pinned on the wire). /// `exec_route`; absent-field shape pinned on the wire).
#[tokio::test] #[tokio::test]
async fn second_shell_request_on_a_served_shell_channel_declines_channel_not_held() { async fn second_shell_request_on_a_served_shell_channel_declines_channel_not_held() {
ec::install(); ec::install();
@@ -543,7 +543,7 @@ mod serving {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// The channel_close-fired per-channel cancel path on the wire // The channel_close-fired per-channel cancel path on the wire
// (`w4-tty-coverage`, wave-3 gate booked item 3): the cap release + // (`w4-tty-coverage`, wave-3 gate booked item 3): the cap release +
// the per-channel state teardown, wire-witnessed — the disjoint // the per-channel state teardown, wire-witnessed; the disjoint
// release sites the gate verified per row (the wire rows here // release sites the gate verified per row (the wire rows here
// witness the established-channel-close site, `channel_cap.rs` // witness the established-channel-close site, `channel_cap.rs`
// release site 1). // release site 1).
@@ -553,9 +553,9 @@ mod serving {
/// `channel_cap.rs` release site 1 + `handler.rs` /// `channel_cap.rs` release site 1 + `handler.rs`
/// §the control rows' per-channel surface (a client close on a /// §the control rows' per-channel surface (a client close on a
/// served row fires the `channel_close` hook: the bridge surface /// served row fires the `channel_close` hook: the bridge surface
/// fires the per-channel cancel — the serving task aborts the /// fires the per-channel cancel; the serving task aborts the
/// alktty session, alktty's kill-on-drop contract fires, nothing /// alktty session, alktty's kill-on-drop contract fires, nothing
/// fabricated on the wire — and the cap releases, disjoint from /// fabricated on the wire, and the cap releases, disjoint from
/// the bridge-failure and arrival sites). /// the bridge-failure and arrival sites).
#[tokio::test] #[tokio::test]
async fn a_client_close_on_a_served_row_fires_the_per_channel_cancel_and_releases_the_cap() { async fn a_client_close_on_a_served_row_fires_the_per_channel_cancel_and_releases_the_cap() {
@@ -605,7 +605,7 @@ mod serving {
} }
assert!( assert!(
!shim.observation().kills.lock().is_empty(), !shim.observation().kills.lock().is_empty(),
"the close-fired per-channel cancel aborted the alktty session — \ "the close-fired per-channel cancel aborted the alktty session; \
alktty's kill-on-drop contract fired (witnessed before unmount, \ alktty's kill-on-drop contract fired (witnessed before unmount, \
so this is the channel_close path, not the teardown)" so this is the channel_close path, not the teardown)"
); );
@@ -622,7 +622,7 @@ mod serving {
let mut recovered = open_session_channel(&session.client) let mut recovered = open_session_channel(&session.client)
.await .await
.expect("the cap recovered after the close — the channel_close hook released"); .expect("the cap recovered after the close, since the channel_close hook released");
request_exec(&recovered, "uptime") request_exec(&recovered, "uptime")
.await .await
.expect("the recovered channel routes"); .expect("the recovered channel routes");
@@ -634,19 +634,19 @@ mod serving {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// The Closed/Break no-exit-chunk close shape on the wire // The Closed/Break no-exit-chunk close shape on the wire
// (`w4-tty-coverage`): a bridge read that ends without an exit // (`w4-tty-coverage`): a bridge read that ends without an exit
// chunk reports nothing — the derivation contract's absent-report // chunk reports nothing; the derivation contract's absent-report
// posture pinned, not just the happy paths. (The frame-level // posture pinned, not just the happy paths. (The frame-level
// Closed/Break classification rows live in `src/tty_map.rs`'s test // Closed/Break classification rows live in `src/tty_map.rs`'s test
// mod; the Break classification is unreachable through alktty's // mod; the Break classification is unreachable through alktty's
// own validated `ChunkWriter` — a defensive arm.) // own validated `ChunkWriter`, a defensive arm.)
// ------------------------------------------------------------------ // ------------------------------------------------------------------
/// - *Contract stamp:* mapping.md §Exit chunk (the contract's /// - *Contract stamp:* mapping.md §Exit chunk (the contract's
/// close-shape class: the bridge read ends **without** an exit /// close-shape class: the bridge read ends **without** an exit
/// chunk → no report, plain close, the lifecycle's ended event — /// chunk → no report, plain close, the lifecycle's ended event:
/// its absence is neither 0 nor 255). The shape is constructed /// its absence is neither 0 nor 255). The shape is constructed
/// through the real path: the shim's stdout carries a payload /// through the real path: the shim's stdout carries a payload
/// beyond alktty's own `MAX_CHUNK_LEN` wire bound — alktty's /// beyond alktty's own `MAX_CHUNK_LEN` wire bound; alktty's
/// `ChunkWriter` rejects it locally, its drainer shuts the write /// `ChunkWriter` rejects it locally, its drainer shuts the write
/// half down, and the bridge's next read is EOF (`Closed`) with /// half down, and the bridge's next read is EOF (`Closed`) with
/// the scripted exit never forwarded. Nothing fabricated. /// the scripted exit never forwarded. Nothing fabricated.
@@ -754,13 +754,13 @@ mod serving {
} }
assert!( assert!(
!shim.observation().stdin_eof.lock().is_empty(), !shim.observation().stdin_eof.lock().is_empty(),
"the SSH EOF translated to the zero-length stdin-chunk sentinel — \ "the SSH EOF translated to the zero-length stdin-chunk sentinel: \
the backend's stdin shutdown was observed" the backend's stdin shutdown was observed"
); );
// The session never resolves its exit (script exit = None); // The session never resolves its exit (script exit = None);
// the disconnect is the connection-level cancel: the serving // the disconnect is the connection-level cancel: the serving
// task aborts the alktty session — the kill-on-drop witness // task aborts the alktty session (the kill-on-drop witness
// recorded, nothing fabricated on the wire. // recorded, nothing fabricated on the wire.
session.unmount().await; session.unmount().await;
for _ in 0..100 { for _ in 0..100 {
@@ -771,7 +771,7 @@ mod serving {
} }
assert!( assert!(
!shim.observation().kills.lock().is_empty(), !shim.observation().kills.lock().is_empty(),
"the cancellation aborted the never-resolving session — \ "the cancellation aborted the never-resolving session; \
alktty's kill-on-drop contract fired" alktty's kill-on-drop contract fired"
); );
} }
@@ -787,7 +787,7 @@ mod serving {
let session = session_with(shim_seam(&shim)).await; let session = session_with(shim_seam(&shim)).await;
// Pre-serve resize: after the pty is stored, before the shell // Pre-serve resize: after the pty is stored, before the shell
// request — the stored params resize (the final size wins). // request; the stored params resize (the final size wins).
let channel = open_session_channel(&session.client) let channel = open_session_channel(&session.client)
.await .await
.expect("the session channel opens"); .expect("the session channel opens");
@@ -840,7 +840,7 @@ mod serving {
.await .await
.expect("the signal rides (fire-and-forget)"); .expect("the signal rides (fire-and-forget)");
let name = await_signal(&shim).await; let name = await_signal(&shim).await;
assert_eq!(name, "TERM", "accepted verbatim — no alkssh-side filter"); assert_eq!(name, "TERM", "accepted verbatim, no alkssh-side filter");
session.unmount().await; session.unmount().await;
} }
@@ -853,7 +853,7 @@ mod serving {
/// ≥ 0 → `exit-status(N)`; negative `−N`, N ≥ 2 → /// ≥ 0 → `exit-status(N)`; negative `−N`, N ≥ 2 →
/// `exit-signal(name)` via the table; POC-3 §D.2's `−15` → /// `exit-signal(name)` via the table; POC-3 §D.2's `−15` →
/// `TERM` live evidence) + rules 3 (the `−1` sentinel → 255, /// `TERM` live evidence) + rules 3 (the `−1` sentinel → 255,
/// never a signal claim) — each rule's wire row. `core_dumped` /// never a signal claim); each rule's wire row follows. `core_dumped`
/// is never asserted (the bool stays false). /// is never asserted (the bool stays false).
#[tokio::test] #[tokio::test]
async fn exit_chunks_derive_by_the_contract_rules() { async fn exit_chunks_derive_by_the_contract_rules() {
@@ -883,7 +883,7 @@ mod serving {
assert!(matches!(signal_name, Sig::TERM), "rule 2: −15 → TERM"); assert!(matches!(signal_name, Sig::TERM), "rule 2: −15 → TERM");
assert!( assert!(
!core_dumped, !core_dumped,
"core_dumped is never asserted — the bool stays false" "core_dumped is never asserted; the bool stays false"
); );
} }
( (
@@ -913,7 +913,7 @@ mod serving {
/// - *Contract stamp:* mapping.md §the seams + the task's /// - *Contract stamp:* mapping.md §the seams + the task's
/// negotiate-then-serve decision (the alktty error frame → /// negotiate-then-serve decision (the alktty error frame →
/// `negotiation-failed`, `exit-status(255)`, close — the one /// `negotiation-failed`, `exit-status(255)`, close: the one
/// could-not-start claim this door makes: nothing ran). /// could-not-start claim this door makes: nothing ran).
#[tokio::test] #[tokio::test]
async fn unknown_backend_answers_255_then_close() { async fn unknown_backend_answers_255_then_close() {
@@ -958,7 +958,7 @@ mod serving {
assert_eq!( assert_eq!(
ids, ids,
vec!["ssh-rider".to_owned()], vec!["ssh-rider".to_owned()],
"the resolved identity rode to drive_session_pre_negotiated — \ "the resolved identity rode to drive_session_pre_negotiated; \
the parked wave-2 identity-landing item asserted live" the parked wave-2 identity-landing item asserted live"
); );
session.unmount().await; session.unmount().await;
@@ -1009,7 +1009,7 @@ mod unset_declines {
use russh::client::Msg; use russh::client::Msg;
use russh::{Channel, ChannelMsg}; use russh::{Channel, ChannelMsg};
/// An authenticated loopback over a mutated config — the /// An authenticated loopback over a mutated config, the
/// unset-field rows' driver. /// unset-field rows' driver.
async fn session_with(mutate: impl FnOnce(&mut ServeConfig)) -> LoopbackSession { async fn session_with(mutate: impl FnOnce(&mut ServeConfig)) -> LoopbackSession {
let provider = Arc::new(MemIdentityProvider::new()); let provider = Arc::new(MemIdentityProvider::new());
@@ -1049,7 +1049,7 @@ mod unset_declines {
} }
/// - *Contract stamp:* OQ-08 (`shell_argv` unset → `shell_request` /// - *Contract stamp:* OQ-08 (`shell_argv` unset → `shell_request`
/// declines — never a fallback) + the recorded wire shape (the /// declines, never a fallback) + the recorded wire shape (the
/// serving-path decline: failure then close, from a spawned /// serving-path decline: failure then close, from a spawned
/// task). /// task).
#[tokio::test] #[tokio::test]
@@ -1129,7 +1129,7 @@ mod unset_declines {
mod real_tool { mod real_tool {
//! The real-tool smoke (`w3-tty-session`): one OpenSSH pty //! The real-tool smoke (`w3-tty-session`): one OpenSSH pty
//! session (`ssh -tt`, a real PTY via a dev-dep alktty `local` //! session (`ssh -tt`, a real PTY via a dev-dep alktty `local`
//! composition — the posture decided: the session rows' smoke //! composition, the posture decided: the session rows' smoke
//! composes the REAL backend, not the shim, because a plain shim //! composes the REAL backend, not the shim, because a plain shim
//! row cannot observe what vanilla OpenSSH actually sends; POC-3 //! row cannot observe what vanilla OpenSSH actually sends; POC-3
//! stage D proved the same composition) through a TCP-mounted //! stage D proved the same composition) through a TCP-mounted
@@ -1152,7 +1152,7 @@ mod real_tool {
use super::common::harness::serve_config as harness_serve_config; use super::common::harness::serve_config as harness_serve_config;
use super::common::{MemIdentityProvider, fixture, identity}; use super::common::{MemIdentityProvider, fixture, identity};
/// The `local` composition for the smoke — alktty's own backend in /// The `local` composition for the smoke: alktty's own backend in
/// the seam map (dev-dep unification: the `local` feature rides the /// the seam map (dev-dep unification: the `local` feature rides the
/// test graph only). /// test graph only).
fn local_seam() -> SessionSeam { fn local_seam() -> SessionSeam {
@@ -1171,11 +1171,11 @@ mod real_tool {
#[tokio::test] #[tokio::test]
async fn real_openssh_pty_session_smoke() { async fn real_openssh_pty_session_smoke() {
if Command::new("ssh").arg("-V").output().is_err() { if Command::new("ssh").arg("-V").output().is_err() {
eprintln!("SKIPPED (with reason): real-tool pty smoke — ssh is unavailable"); eprintln!("SKIPPED (with reason): real-tool pty smoke, ssh is unavailable");
return; return;
} }
if Command::new("sh").arg("-c").arg("exit 0").output().is_err() { if Command::new("sh").arg("-c").arg("exit 0").output().is_err() {
eprintln!("SKIPPED (with reason): real-tool pty smoke — sh is unavailable"); eprintln!("SKIPPED (with reason): real-tool pty smoke, sh is unavailable");
return; return;
} }
@@ -1265,7 +1265,7 @@ mod real_tool {
assert!( assert!(
success, success,
"vanilla OpenSSH's pty+shell session through the door exits 0 \ "vanilla OpenSSH's pty+shell session through the door exits 0 \
(the exit chunk derived exit-status(0) before close) — stdout: {stdout:?}" (the exit chunk derived exit-status(0) before close); stdout: {stdout:?}"
); );
} }
} }
+88 -88
View File
@@ -1,13 +1,13 @@
//! The `w3-tunnel-direct` contract rows — the tunnel branch served for //! The `w3-tunnel-direct` contract rows: the tunnel branch served for
//! real: the route-1 bridge (ADR-006 decision 1) lands //! real: the route-1 bridge (ADR-006 decision 1) lands
//! `direct-tcpip`/`direct-streamlocal` through the published alktunnels //! `direct-tcpip`/`direct-streamlocal` through the published alktunnels
//! 0.5.0 seam (alkssh 0.2.0's W2 adoption: `TunnelSession::open_direct` //! 0.5.0 seam (alkssh 0.2.0's W2 adoption: `TunnelSession::open_direct`
//! → accept only after the fabric open succeeds → `take_halves()` //! → accept only after the fabric open succeeds → `take_halves()`
//! pumped against the taken channel; the forwarded serving surface on //! pumped against the taken channel; the forwarded serving surface on
//! the establisher shape — the `ForwardedGate` correlation gate), //! the establisher shape, the `ForwardedGate` correlation gate),
//! observed through the loopback harness against the **fabric //! observed through the loopback harness against the **fabric
//! double** (`w3-tunnel-direct`'s recorded test composition: the //! double** (`w3-tunnel-direct`'s recorded test composition: the
//! POC-3 §C.4 in-process recipe — `ChannelsAdapter` + //! POC-3 §C.4 in-process recipe (`ChannelsAdapter` +
//! `InstallChannelZero` + the alktunnels openables + a hand-rolled //! `InstallChannelZero` + the alktunnels openables + a hand-rolled
//! dial/dial-fail seam, hand-rolled here, never a sibling-checkout //! dial/dial-fail seam, hand-rolled here, never a sibling-checkout
//! path). //! path).
@@ -21,19 +21,19 @@
//! the release mechanism: an admitted-but-rejected bridge releases its //! the release mechanism: an admitted-but-rejected bridge releases its
//! count, a closed serving channel recovers), the composed identity //! count, a closed serving channel recovers), the composed identity
//! gate (the fabric's `AccessControl::check` refusing an unscoped //! gate (the fabric's `AccessControl::check` refusing an unscoped
//! channels identity — the visible-surface invariant's alkcall half, //! channels identity, the visible-surface invariant's alkcall half,
//! with the establisher never reached), the establisher's identity //! with the establisher never reached), the establisher's identity
//! witness (the POC-3 §C.3 shape), the OQ-11 off-Unix gate, and the //! witness (the POC-3 §C.3 shape), the OQ-11 off-Unix gate, and the
//! real-tool `ssh -L` smoke. //! real-tool `ssh -L` smoke.
//! //!
//! The fabric double's dial seam is in-process by design: the dial //! The fabric double's dial seam is in-process by design: the dial
//! target is matched against a map of pre-created duplex halves keyed //! target is matched against a map of pre-created duplex halves keyed
//! by the rendered target string (`host:port` / socket path — exactly //! by the rendered target string (`host:port` / socket path, exactly
//! the shape the establisher hands the `DialFn`), so the serve rows //! the shape the establisher hands the `DialFn`), so the serve rows
//! are fully deterministic (no sockets on the target substrate), while //! are fully deterministic (no sockets on the target substrate), while
//! the real-tool smoke hand-rolls a real-TCP dial closure against a //! the real-tool smoke hand-rolls a real-TCP dial closure against a
//! real loopback endpoint (the smoke exercises the substrate for real; //! real loopback endpoint (the smoke exercises the substrate for real;
//! tooling posture recorded like the git task — skipped-with-reason //! tooling posture recorded like the git task, skipped-with-reason
//! when `ssh`/`curl` is unavailable, never silently green). //! when `ssh`/`curl` is unavailable, never silently green).
#![allow(dead_code)] #![allow(dead_code)]
@@ -85,7 +85,7 @@ mod serving {
/// tunnel-featured serve-config holds. The accept side's channel-0 /// tunnel-featured serve-config holds. The accept side's channel-0
/// identity (`caller`) is what the fabric's /// identity (`caller`) is what the fabric's
/// `AccessControl::check(peer_identity)` gates the mapped opens /// `AccessControl::check(peer_identity)` gates the mapped opens
/// against — the assembly-composed identity of the real deployment; /// against, the assembly-composed identity of the real deployment;
/// `witness` records what the establisher's per-call auth (alkcall /// `witness` records what the establisher's per-call auth (alkcall
/// CF-006) saw. /// CF-006) saw.
pub(super) async fn wire_fabric( pub(super) async fn wire_fabric(
@@ -109,7 +109,7 @@ mod serving {
return; return;
}; };
let (writer, reader) = split_single_stream(channel0_bidi); let (writer, reader) = split_single_stream(channel0_bidi);
// The serving-side identity set once at install — the // The serving-side identity set once at install, the
// hub_leg.rs one-time-set precedent the door's // hub_leg.rs one-time-set precedent the door's
// `set_identity` mechanics decision reads (the identity // `set_identity` mechanics decision reads (the identity
// the serving dispatch resolves for the mapped opens' // the serving dispatch resolves for the mapped opens'
@@ -159,7 +159,7 @@ mod serving {
/// The leg-2 fabric double (`w3-tunnel-listen`'s recorded /// The leg-2 fabric double (`w3-tunnel-listen`'s recorded
/// composition): the leg-1 wire fabric extended with the /// composition): the leg-1 wire fabric extended with the
/// **bind-first listen half** — the assembly-composed shape the /// **bind-first listen half**, the assembly-composed shape the
/// vanilla `-R` listen flow rides, built exactly on the published /// vanilla `-R` listen flow rides, built exactly on the published
/// alktunnels pieces: a `bindfirst_establisher`-shaped /// alktunnels pieces: a `bindfirst_establisher`-shaped
/// establisher, `make_bindfirst_accept_handler`, and the /// establisher, `make_bindfirst_accept_handler`, and the
@@ -175,7 +175,7 @@ mod serving {
/// wire-mapped resource string as the bind endpoint directly /// wire-mapped resource string as the bind endpoint directly
/// (`resource = "127.0.0.1:9200"` binds `127.0.0.1:9200`) and mints /// (`resource = "127.0.0.1:9200"` binds `127.0.0.1:9200`) and mints
/// the correlation token per the ADR-008 discipline. Who may bind /// the correlation token per the ADR-008 discipline. Who may bind
/// what is the fabric's policy — the double binds what the room /// what is the fabric's policy: the double binds what the room
/// asks; a real fabric's ACL is the same seam. `localhost` /// asks; a real fabric's ACL is the same seam. `localhost`
/// renders to `127.0.0.1` so the real-tool smoke's curl is /// renders to `127.0.0.1` so the real-tool smoke's curl is
/// deterministic about the family. /// deterministic about the family.
@@ -184,7 +184,7 @@ mod serving {
pub(super) registry: Arc<OperationRegistry>, pub(super) registry: Arc<OperationRegistry>,
opener: Arc<tokio::sync::OnceCell<ForwardedOpener>>, opener: Arc<tokio::sync::OnceCell<ForwardedOpener>>,
/// The establisher's mint stream (the rows' real-token vehicle /// The establisher's mint stream (the rows' real-token vehicle
/// — the ACL row needs the genuinely minted token its listen's /// which the ACL row needs; the genuinely minted token its listen's
/// fabric established; the SSH layer never carries the token). /// fabric established; the SSH layer never carries the token).
minted: tokio::sync::Mutex<mpsc::UnboundedReceiver<u32>>, minted: tokio::sync::Mutex<mpsc::UnboundedReceiver<u32>>,
} }
@@ -192,7 +192,7 @@ mod serving {
impl FabricDouble { impl FabricDouble {
/// The fabric's `ForwardedOpener` (the call plane), exported /// The fabric's `ForwardedOpener` (the call plane), exported
/// for the rows' direct drives (an arrival the fabric would /// for the rows' direct drives (an arrival the fabric would
/// make, dialed by the row itself — the unknown-token and /// make, dialed by the row itself (the unknown-token and
/// composed-gate rows' vehicle). /// composed-gate rows' vehicle).
pub(super) async fn forwarded_opener(&self) -> ForwardedOpener { pub(super) async fn forwarded_opener(&self) -> ForwardedOpener {
for _ in 0..600 { for _ in 0..600 {
@@ -206,13 +206,13 @@ mod serving {
/// Drives the fabric's forwarded-open closure directly: /// Drives the fabric's forwarded-open closure directly:
/// `Ok` carries the adopted fabric-side halves (the door /// `Ok` carries the adopted fabric-side halves (the door
/// allocated the channel — the gate-pass/residual-race /// allocated the channel, so the gate-pass/residual-race
/// teardown-at-birth is then observable as an EOF on the /// teardown-at-birth is then observable as an EOF on the
/// `recv` half), `Err` is the door-side refusal (the /// `recv` half), `Err` is the door-side refusal (the
/// unknown-token gate miss / the composed-gate row's ACL /// unknown-token gate miss / the composed-gate row's ACL
/// answer — the arrival never became anything). /// answer; the arrival never became anything).
/// The next minted correlation token (one per accepted listen, /// The next minted correlation token (one per accepted listen,
/// in mint order — the rows' real-token vehicle). /// in mint order (the rows' real-token vehicle).
pub(super) async fn minted_token(&self) -> u32 { pub(super) async fn minted_token(&self) -> u32 {
self.minted self.minted
.lock() .lock()
@@ -240,7 +240,7 @@ mod serving {
/// The double's establisher mint discipline: `Counter` is the /// The double's establisher mint discipline: `Counter` is the
/// ADR-008 shape the real fabric carries (a non-zero-seeded /// ADR-008 shape the real fabric carries (a non-zero-seeded
/// counter, never 0, never re-minted); `WithCollisions` is the /// counter, never 0, never re-minted); `WithCollisions` is the
/// row-pinned collision construct — the first `collisions` mints /// row-pinned collision construct; the first `collisions` mints
/// return a fixed `token` (the test forces what the fabric's /// return a fixed `token` (the test forces what the fabric's
/// discipline prevents), then the counter continues from where it /// discipline prevents), then the counter continues from where it
/// left. /// left.
@@ -306,7 +306,7 @@ mod serving {
/// The double's real-socket bind seam: real TCP/unix binds of the /// The double's real-socket bind seam: real TCP/unix binds of the
/// wire-named endpoint (the platform boundary the /// wire-named endpoint (the platform boundary the
/// `tunnels/local` feature serves in a real deployment — /// `tunnels/local` feature serves in a real deployment,
/// hand-rolled here, never a sibling-checkout path). Unix binds /// hand-rolled here, never a sibling-checkout path). Unix binds
/// unlink a stale socket file first (`bind_unix`'s caller /// unlink a stale socket file first (`bind_unix`'s caller
/// posture). /// posture).
@@ -357,7 +357,7 @@ mod serving {
/// invocation (the dispatcher's per-accept contract), downcasting /// invocation (the dispatcher's per-accept contract), downcasting
/// the plan's opaque listener to the bind seam's own wrappers. The /// the plan's opaque listener to the bind seam's own wrappers. The
/// unix leg's `peer` is fabricated (an unnamed peer cannot ride /// unix leg's `peer` is fabricated (an unnamed peer cannot ride
/// the forwarded op's required `peer` param — the shipped /// the forwarded op's required `peer` param (the shipped
/// accept-pair posture). /// accept-pair posture).
fn real_accept_loop() -> BindFirstAcceptLoop { fn real_accept_loop() -> BindFirstAcceptLoop {
Arc::new(move |listener: Arc<dyn std::any::Any + Send + Sync>| { Arc::new(move |listener: Arc<dyn std::any::Any + Send + Sync>| {
@@ -412,7 +412,7 @@ mod serving {
.await .await
} }
/// Wires the leg-2 fabric double (leg 1 unchanged underneath — the /// Wires the leg-2 fabric double (leg 1 unchanged underneath, the
/// dial seam + direct openable stay exactly `wire_fabric`'s /// dial seam + direct openable stay exactly `wire_fabric`'s
/// composition, against a leg-1 fail dial the -R rows never /// composition, against a leg-1 fail dial the -R rows never
/// exercise). `door_scoped_caller` composes the fabric-side /// exercise). `door_scoped_caller` composes the fabric-side
@@ -420,7 +420,7 @@ mod serving {
/// `arrival_scoped` composes the door-side `ServingConfig.identity` /// `arrival_scoped` composes the door-side `ServingConfig.identity`
/// with or without the `tunnel:forwarded` scope (the composed-gate /// with or without the `tunnel:forwarded` scope (the composed-gate
/// row's negative leg). `mint` is the establisher's mint /// row's negative leg). `mint` is the establisher's mint
/// discipline ([`MintStrategy`]) — the collision rows' vehicle. /// discipline ([`MintStrategy`]), the collision rows' vehicle.
pub(super) async fn wire_fabric_with_listen_mint( pub(super) async fn wire_fabric_with_listen_mint(
door_scoped_caller: bool, door_scoped_caller: bool,
arrival_scoped: bool, arrival_scoped: bool,
@@ -428,9 +428,9 @@ mod serving {
) -> FabricDouble { ) -> FabricDouble {
// The two sides' registries are independent (the fabric's own // The two sides' registries are independent (the fabric's own
// dispatch registry; and the DOOR-side shared one the row // dispatch registry; and the DOOR-side shared one the row
// hands alkssh's seam — the registration into the fabric's // hands alkssh's seam; the registration into the fabric's
// registry would be the silent-fork shape). The earlier draft // registry would be the silent-fork shape). The earlier draft
// wired one Arc to both — pinned wrong here for exactly one // wired one Arc to both, pinned wrong here for exactly one
// reason: the door's `tunnel/forwarded` openable must live in // reason: the door's `tunnel/forwarded` openable must live in
// the DOOR's dispatch surface alone. // the DOOR's dispatch surface alone.
let door_registry = Arc::new(OperationRegistry::new()); let door_registry = Arc::new(OperationRegistry::new());
@@ -499,7 +499,7 @@ mod serving {
.await; .await;
// The real opener maps the wire refusal's // The real opener maps the wire refusal's
// reason through the `From<TunnelEstablish // reason through the `From<TunnelEstablish
// Error> for EstablishmentError` table — the // Error> for EstablishmentError` table; the
// typed `unknown_resource` (the gate's miss // typed `unknown_resource` (the gate's miss
// class) must survive the client side here, // class) must survive the client side here,
// so the double maps the details.reason back // so the double maps the details.reason back
@@ -622,7 +622,7 @@ mod serving {
} }
} }
/// A pre-created duplex target — the "in-process dial" half: the /// A pre-created duplex target, the "in-process dial" half: the
/// test holds the returned side, the map hands the dial seam the /// test holds the returned side, the map hands the dial seam the
/// other half when the establisher renders a matching target. /// other half when the establisher renders a matching target.
fn inproc_target(targets: &TargetMap, key: &str, capacity: usize) -> tokio::io::DuplexStream { fn inproc_target(targets: &TargetMap, key: &str, capacity: usize) -> tokio::io::DuplexStream {
@@ -637,7 +637,7 @@ mod serving {
/// The hand-rolled dial seam (the assembly's injected `DialFn`, /// The hand-rolled dial seam (the assembly's injected `DialFn`,
/// doubled): an exact `host:port` / socket-path key pops the /// doubled): an exact `host:port` / socket-path key pops the
/// pre-created half; an absent key is the dial-fail seam /// pre-created half; an absent key is the dial-fail seam
/// (`TunnelEstablishError::DialFailed`) — both the fabric-failure /// (`TunnelEstablishError::DialFailed`); both the fabric-failure
/// rows' shape and the release rows' failure leg. The call counter /// rows' shape and the release rows' failure leg. The call counter
/// witnesses the "no fabric call" assertions. /// witnesses the "no fabric call" assertions.
fn inproc_dial(targets: TargetMap, calls: Arc<AtomicUsize>) -> DialFn { fn inproc_dial(targets: TargetMap, calls: Arc<AtomicUsize>) -> DialFn {
@@ -665,7 +665,7 @@ mod serving {
}) })
} }
/// The fabric caller identity with the direct op's scope — the /// The fabric caller identity with the direct op's scope, the
/// composed-gate identity the serving side resolves, so the ACL /// composed-gate identity the serving side resolves, so the ACL
/// admits the mapped opens. /// admits the mapped opens.
fn scoped_caller() -> Identity { fn scoped_caller() -> Identity {
@@ -707,7 +707,7 @@ mod serving {
/// (kept near the top of the module for the rows' readability). /// (kept near the top of the module for the rows' readability).
use super::common::harness::authenticate_publickey; use super::common::harness::authenticate_publickey;
/// Matches an open failure's SSH reason — the wire decline reason /// Matches an open failure's SSH reason, the wire decline reason
/// the rows assert. /// the rows assert.
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) { fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
assert!( assert!(
@@ -717,7 +717,7 @@ mod serving {
} }
/// The serve row's data leg: the SSH channel against the in-process /// The serve row's data leg: the SSH channel against the in-process
/// target — one request down `direct-tcpip`, one reply back (the /// target: one request down `direct-tcpip`, one reply back (the
/// bidirectional pump of the route-1 bridge, the POC-3 §C.1 shape: /// bidirectional pump of the route-1 bridge, the POC-3 §C.1 shape:
/// zero framing, both directions live). /// zero framing, both directions live).
async fn round_trip( async fn round_trip(
@@ -773,7 +773,7 @@ mod serving {
/// - *Contract stamp:* ADR-006 decision 1 (the route-1 bridge: the /// - *Contract stamp:* ADR-006 decision 1 (the route-1 bridge: the
/// `direct-tcpip` open → `TunnelSession::open_direct` over a held /// `direct-tcpip` open → `TunnelSession::open_direct` over a held
/// `ChannelClient` → accept only after the fabric open succeeds → /// `ChannelClient` → accept only after the fabric open succeeds →
/// `take_halves()` pumped bidirectionally — the POC-3 §C.1 shape, /// `take_halves()` pumped bidirectionally (the POC-3 §C.1 shape,
/// zero framing both directions; the open hook returns fast). /// zero framing both directions; the open hook returns fast).
#[tokio::test] #[tokio::test]
async fn direct_tcpip_serves_through_the_route_1_bridge() { async fn direct_tcpip_serves_through_the_route_1_bridge() {
@@ -850,7 +850,7 @@ mod serving {
} }
/// - *Contract stamp:* ADR-006 decision 1 (fabric-open failure → /// - *Contract stamp:* ADR-006 decision 1 (fabric-open failure →
/// the explicit `ConnectFailed` reject — accept only after the /// the explicit `ConnectFailed` reject (accept only after the
/// fabric open succeeds; the wave-2 stub's reason surviving as /// fabric open succeeds; the wave-2 stub's reason surviving as
/// the real decline; POC-3 §C.2's failure shape). /// the real decline; POC-3 §C.2's failure shape).
#[tokio::test] #[tokio::test]
@@ -876,7 +876,7 @@ mod serving {
assert_eq!( assert_eq!(
calls.load(Ordering::SeqCst), calls.load(Ordering::SeqCst),
1, 1,
"the bridge dialed the fabric exactly once — the SSH open carried the fabric's answer" "the bridge dialed the fabric exactly once; the SSH open carried the fabric's answer"
); );
session.unmount().await; session.unmount().await;
@@ -885,10 +885,10 @@ mod serving {
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded cap release /// - *Contract stamp:* `w3-tunnel-direct`'s recorded cap release
/// mechanism (the admission is taken at the hook and held across /// mechanism (the admission is taken at the hook and held across
/// the fabric open; a fabric failure releases the count **inside /// the fabric open; a fabric failure releases the count **inside
/// the spawned task** — the `channel_close` release point only /// the spawned task**; the `channel_close` release point only
/// fires for established channels, so without this release a /// fires for established channels, so without this release a
/// failed bridge would leak budget): a failed bridge under a /// failed bridge would leak budget): a failed bridge under a
/// full cap does not strand the budget — the next open of the /// full cap does not strand the budget; the next open of the
/// same session is admitted (it would answer /// same session is admitted (it would answer
/// `AdministrativelyProhibited` if the count had leaked). /// `AdministrativelyProhibited` if the count had leaked).
#[tokio::test] #[tokio::test]
@@ -922,7 +922,7 @@ mod serving {
// of this same session observes the released count. // of this same session observes the released count.
let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36002) let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36002)
.await .await
.expect("the next open is admitted — the failed bridge released its count"); .expect("the next open is admitted; the failed bridge released its count");
round_trip( round_trip(
&mut channel, &mut channel,
target_side, target_side,
@@ -938,7 +938,7 @@ mod serving {
/// - *Contract stamp:* AGENTS.md convention 9 / `overview.md` /// - *Contract stamp:* AGENTS.md convention 9 / `overview.md`
/// §Bounded-resources policy + `ChannelCap`'s recorded /// §Bounded-resources policy + `ChannelCap`'s recorded
/// disjoint-release shape: a *closed serving* channel releases /// disjoint-release shape: a *closed serving* channel releases
/// through the `channel_close` hook — a full cap refuses with the /// through the `channel_close` hook; a full cap refuses with the
/// cap reason, and a recovered cap admits again. /// cap reason, and a recovered cap admits again.
#[tokio::test] #[tokio::test]
async fn the_cap_refuses_beyond_the_ceiling_and_recovers_on_close() { async fn the_cap_refuses_beyond_the_ceiling_and_recovers_on_close() {
@@ -984,7 +984,7 @@ mod serving {
// recovery is observable because the dial target is consumed // recovery is observable because the dial target is consumed
// once: the moment the cap admits again, the open proceeds // once: the moment the cap admits again, the open proceeds
// onto the bridge (and fails at the fabric dial with the // onto the bridge (and fails at the fabric dial with the
// connect-failed class) — an `AdministrativelyProhibited` // connect-failed class); an `AdministrativelyProhibited`
// answer here would mean a leaked count. // answer here would mean a leaked count.
tokio::time::timeout(std::time::Duration::from_secs(10), async { tokio::time::timeout(std::time::Duration::from_secs(10), async {
let mut attempts = 0u32; let mut attempts = 0u32;
@@ -1003,11 +1003,11 @@ mod serving {
Err(Error::ChannelOpenFailure( Err(Error::ChannelOpenFailure(
ChannelOpenFailure::AdministrativelyProhibited, ChannelOpenFailure::AdministrativelyProhibited,
)) => { )) => {
panic!("the cap never recovered after the close — the count leaked") panic!("the cap never recovered after the close; the count leaked")
} }
Err(Error::ChannelOpenFailure(_)) => { Err(Error::ChannelOpenFailure(_)) => {
// The open went past the cap answer and onto the // The open went past the cap answer and onto the
// bridge (the fabric dial's connect-failed) — the // bridge (the fabric dial's connect-failed); the
// released count is what this row observes. // released count is what this row observes.
return; return;
} }
@@ -1023,13 +1023,13 @@ mod serving {
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface /// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
/// invariant's alkcall half): the fabric's /// invariant's alkcall half): the fabric's
/// `AccessControl::check(peer_identity)` gates the mapped open — /// `AccessControl::check(peer_identity)` gates the mapped open:
/// a channels connection whose identity lacks the direct op's /// a channels connection whose identity lacks the direct op's
/// scope is refused by the fabric's dispatch (`FORBIDDEN`), the /// scope is refused by the fabric's dispatch (`FORBIDDEN`), the
/// establisher never runs, and the door answers the SSH open /// establisher never runs, and the door answers the SSH open
/// with the explicit `ConnectFailed`. The auth bridge's /// with the explicit `ConnectFailed`. The auth bridge's
/// SSH-resolved identity and the fabric leg's dial identity /// SSH-resolved identity and the fabric leg's dial identity
/// compose at the assembly (the deployment policy) — this row is /// compose at the assembly (the deployment policy); this row is
/// the composed-gate witness through the double. /// the composed-gate witness through the double.
#[tokio::test] #[tokio::test]
async fn the_fabric_acl_refuses_an_unscoped_channels_identity() { async fn the_fabric_acl_refuses_an_unscoped_channels_identity() {
@@ -1056,7 +1056,7 @@ mod serving {
assert_eq!( assert_eq!(
calls.load(Ordering::SeqCst), calls.load(Ordering::SeqCst),
0, 0,
"the ACL check precedes the establisher — a refused open never dialed" "the ACL check precedes the establisher; a refused open never dialed"
); );
session.unmount().await; session.unmount().await;
@@ -1064,7 +1064,7 @@ mod serving {
/// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip`'s /// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip`'s
/// target is the SSH open arguments verbatim; an impossible wire /// target is the SSH open arguments verbatim; an impossible wire
/// target — a port outside the u16 space — is a declined /// target, a port outside the u16 space, is a declined
/// pre-admission, `invalid-target`, connect-failed on the wire). /// pre-admission, `invalid-target`, connect-failed on the wire).
#[tokio::test] #[tokio::test]
async fn an_out_of_range_wire_port_declines_pre_admission() { async fn an_out_of_range_wire_port_declines_pre_admission() {
@@ -1097,7 +1097,7 @@ mod serving {
/// - *Contract stamp:* OQ-11 (the runtime-Unix gate: universal /// - *Contract stamp:* OQ-11 (the runtime-Unix gate: universal
/// compile, clean open-decline off-Unix **before any fabric /// compile, clean open-decline off-Unix **before any fabric
/// call** — never a partial bridge): this row compiles in this /// call**, never a partial bridge): this row compiles in this
/// binary only off-Unix and pins the gate with its dial-call /// binary only off-Unix and pins the gate with its dial-call
/// witness at zero. On-Unix builds ride the serve row above. /// witness at zero. On-Unix builds ride the serve row above.
#[cfg(not(unix))] #[cfg(not(unix))]
@@ -1125,7 +1125,7 @@ mod serving {
assert_eq!( assert_eq!(
calls.load(Ordering::SeqCst), calls.load(Ordering::SeqCst),
0, 0,
"the runtime gate precedes any fabric call — no partial bridge" "the runtime gate precedes any fabric call; no partial bridge"
); );
session.unmount().await; session.unmount().await;
@@ -1188,7 +1188,7 @@ mod serving {
/// The arrival row's pump legs: the arrival's request reaches the /// The arrival row's pump legs: the arrival's request reaches the
/// forwarded SSH channel, the channel's reply reaches the arrival /// forwarded SSH channel, the channel's reply reaches the arrival
/// socket — the fabric `BiStream` ↔ SSH channel duplex, zero /// socket: the fabric `BiStream` ↔ SSH channel duplex, zero
/// framing both directions (the route-1 bridge's primitive, /// framing both directions (the route-1 bridge's primitive,
/// reversed). /// reversed).
async fn arrival_pump( async fn arrival_pump(
@@ -1231,8 +1231,8 @@ mod serving {
/// (`tcpip_forward` → the family-ward listen dial) + §Channel-type /// (`tcpip_forward` → the family-ward listen dial) + §Channel-type
/// rows, the `forwarded-tcpip` row (the data leg: per fabric /// rows, the `forwarded-tcpip` row (the data leg: per fabric
/// arrival the door opens a `forwarded-tcpip` channel **toward /// arrival the door opens a `forwarded-tcpip` channel **toward
/// the vanilla client**, echoing the listen request's own args — /// the vanilla client**, echoing the listen request's own args
/// the OpenSSH matcher parity — and pumping the fabric /// (the OpenSSH matcher parity) and pumping the fabric
/// `BiStream` ↔ the SSH channel). Observed end to end through the /// `BiStream` ↔ the SSH channel). Observed end to end through the
/// harness's accept hook. /// harness's accept hook.
#[tokio::test] #[tokio::test]
@@ -1302,10 +1302,10 @@ mod serving {
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the /// - *Contract stamp:* `client.md` §Lifecycle primitives (the
/// bound-port spec: `port = 0` → the server picks and the /// bound-port spec: `port = 0` → the server picks and the
/// actually-bound number returns) — the out-param carries the /// actually-bound number returns); the out-param carries the
/// fabric's `bound` reply field's port, and the arrival's echo /// fabric's `bound` reply field's port, and the arrival's echo
/// port is that same allocated number (sshd's `get_local_port` /// port is that same allocated number (sshd's `get_local_port`
/// parity — a forwarded open for a `0`-minted listener never /// parity: a forwarded open for a `0`-minted listener never
/// claims port 0). /// claims port 0).
#[tokio::test] #[tokio::test]
async fn port_zero_mint_is_resolved_into_the_bound_port_out_param() { async fn port_zero_mint_is_resolved_into_the_bound_port_out_param() {
@@ -1356,11 +1356,11 @@ mod serving {
} }
/// - *Contract stamp:* the cancel forms (`cancel_tcpip_forward`): /// - *Contract stamp:* the cancel forms (`cancel_tcpip_forward`):
/// 1:1 into the family listener's teardown — the matching listen /// 1:1 into the family listener's teardown; the matching listen
/// cancels `Ok`, the fabric's bound listener goes away (a /// cancels `Ok`, the fabric's bound listener goes away (a
/// re-connect is refused), and a second cancel of the same /// re-connect is refused), and a second cancel of the same
/// (address, port) pair answers the not-found denial /// (address, port) pair answers the not-found denial
/// (`RequestDenied` — the sshd cancel-of-unknown parity). /// (`RequestDenied`, the sshd cancel-of-unknown parity).
#[tokio::test] #[tokio::test]
async fn cancel_tcpip_forward_tears_the_fabric_listener_down() { async fn cancel_tcpip_forward_tears_the_fabric_listener_down() {
let double = wire_fabric_with_listen(true, true).await; let double = wire_fabric_with_listen(true, true).await;
@@ -1385,7 +1385,7 @@ mod serving {
// The fabric listener is gone (the teardown cascaded: the // The fabric listener is gone (the teardown cascaded: the
// door's listener session dropped, the far accept loop's EOF // door's listener session dropped, the far accept loop's EOF
// sentinel fired, the plan's listener dropped — the bind // sentinel fired, the plan's listener dropped, the bind
// releases). Bounded retry: the connect is refused. // releases). Bounded retry: the connect is refused.
tokio::time::timeout(Duration::from_secs(10), async { tokio::time::timeout(Duration::from_secs(10), async {
loop { loop {
@@ -1415,10 +1415,10 @@ mod serving {
/// upstream guarantee the door defends in depth against: the /// upstream guarantee the door defends in depth against: the
/// test forces what the discipline prevents (the double /// test forces what the discipline prevents (the double
/// re-mints a live token), and the second listen **refuses /// re-mints a live token), and the second listen **refuses
/// fail-closed** — `RequestDenied`, its minted fabric listener /// fail-closed** (`RequestDenied`), its minted fabric listener
/// torn down immediately (the refused registration never left a /// torn down immediately (the refused registration never left a
/// half state), the listener cap released (the next mint is /// half state), the listener cap released (the next mint is
/// admitted against the same 1-count ceiling) — and the **first /// admitted against the same 1-count ceiling), and the **first
/// route still serves**: the collided mint can never steal a /// route still serves**: the collided mint can never steal a
/// live adapter's arrival routes. /// live adapter's arrival routes.
#[tokio::test] #[tokio::test]
@@ -1434,7 +1434,7 @@ mod serving {
.await; .await;
// The ceiling of 2 both admits the collided mint past the // The ceiling of 2 both admits the collided mint past the
// listener cap (so the refusal is the route register, not the // listener cap (so the refusal is the route register, not the
// cap — the cap row's shape) and makes the refusal's cap // cap, the cap row's shape) and makes the refusal's cap
// release observable: without the release, the third mint // release observable: without the release, the third mint
// would be cap-exceeded. // would be cap-exceeded.
let limits = Limits { let limits = Limits {
@@ -1475,16 +1475,16 @@ mod serving {
.expect("the refused listen's fabric listener was torn down"); .expect("the refused listen's fabric listener was torn down");
// The listener count was released at the refusal: against the // The listener count was released at the refusal: against the
// same 2-count ceiling, the next mint (a fresh token — the // same 2-count ceiling, the next mint (a fresh token, since
// collision was two mints on one listen, not a broken fabric) // collision was two mints on one listen, not a broken fabric)
// is admitted and registers normally (a `0` mint's success // is admitted and registers normally (a `0` mint's success
// reply carries the actually-bound number — the sshd parity). // reply carries the actually-bound number, the sshd parity).
let third = tcpip_forward(&session.client, "127.0.0.1", 0) let third = tcpip_forward(&session.client, "127.0.0.1", 0)
.await .await
.expect("the collision released the listener count"); .expect("the collision released the listener count");
assert_ne!(third, 0); assert_ne!(third, 0);
// The first route still serves — the steal is impossible: an // The first route still serves; the steal is impossible: an
// arrival onto the live route's endpoint opens the // arrival onto the live route's endpoint opens the
// forwarded-tcpip channel and the bytes ride the pump. // forwarded-tcpip channel and the bytes ride the pump.
let mut arrival = tokio::net::TcpStream::connect(("127.0.0.1", port_a)) let mut arrival = tokio::net::TcpStream::connect(("127.0.0.1", port_a))
@@ -1518,7 +1518,7 @@ mod serving {
// Nothing rode the acceptor beyond the live route's served // Nothing rode the acceptor beyond the live route's served
// arrival (the refused mint's listener was torn down at birth // arrival (the refused mint's listener was torn down at birth
// — its arrivals could not exist). // so its arrivals could not exist).
tokio::time::timeout(Duration::from_millis(500), opens.recv()) tokio::time::timeout(Duration::from_millis(500), opens.recv())
.await .await
.expect_err("no arrival may follow the refused mint"); .expect_err("no arrival may follow the refused mint");
@@ -1532,8 +1532,8 @@ mod serving {
/// whose `listen` token has no route (nothing was ever minted for /// whose `listen` token has no route (nothing was ever minted for
/// this door) fails the open at the call plane as a typed /// this door) fails the open at the call plane as a typed
/// `channel:open_failed` (`TunnelEstablishError::UnknownResource` /// `channel:open_failed` (`TunnelEstablishError::UnknownResource`
/// — the wire's `unknown_resource` reason) **before any channel /// the wire's `unknown_resource` reason) **before any channel
/// is born** — the phantom-channel shape (call-plane success, /// is born**; the phantom-channel shape (call-plane success,
/// then handler-side decline) is closed at the open: no channel /// then handler-side decline) is closed at the open: no channel
/// allocation, no adopted halves, no acceptor row. /// allocation, no adopted halves, no acceptor row.
#[tokio::test] #[tokio::test]
@@ -1552,7 +1552,7 @@ mod serving {
let outcome = double.open_forwarded_directly(params).await; let outcome = double.open_forwarded_directly(params).await;
let err = match outcome { let err = match outcome {
Ok(_) => { Ok(_) => {
panic!("the gate miss fails the open typed — no phantom channel exists to adopt") panic!("the gate miss fails the open typed; no phantom channel exists to adopt")
} }
Err(error) => error, Err(error) => error,
}; };
@@ -1561,7 +1561,7 @@ mod serving {
"expected UnknownResource at the call plane, got {err:?}" "expected UnknownResource at the call plane, got {err:?}"
); );
// No arrival rode the acceptor — the open died at the call // No arrival rode the acceptor (the open died at the call
// plane; the handler never ran. // plane; the handler never ran.
tokio::time::timeout(Duration::from_millis(500), opens.recv()) tokio::time::timeout(Duration::from_millis(500), opens.recv())
.await .await
@@ -1573,7 +1573,7 @@ mod serving {
/// - *Contract stamp:* the forwarded gate's pass posture (alktunnels /// - *Contract stamp:* the forwarded gate's pass posture (alktunnels
/// 0.5.0, review 003 W2 adoption): an arrival whose token is /// 0.5.0, review 003 W2 adoption): an arrival whose token is
/// genuinely routed behaves exactly as the base shape did — the /// genuinely routed behaves exactly as the base shape did; the
/// open succeeds at the call plane, the door allocates the SSH /// open succeeds at the call plane, the door allocates the SSH
/// channel at the acceptor, and the fabric-side halves are the /// channel at the acceptor, and the fabric-side halves are the
/// arrival's data plane (the gate adds an establishment phase; /// arrival's data plane (the gate adds an establishment phase;
@@ -1643,13 +1643,13 @@ mod serving {
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface /// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
/// invariant's alkcall half on the `-R` leg): the door-side /// invariant's alkcall half on the `-R` leg): the door-side
/// registry's `AccessControl::check` gates arrivals by the fabric /// registry's `AccessControl::check` gates arrivals by the fabric
/// caller's identity — an identity lacking the /// caller's identity (an identity lacking the
/// `tunnel:forwarded` scope is refused by alkcall's dispatch (the /// `tunnel:forwarded` scope is refused by alkcall's dispatch (the
/// openable's own spec) and no SSH channel opens, even for a /// openable's own spec) and no SSH channel opens, even for a
/// genuinely minted token. The genuinely-minted-token shape /// genuinely minted token. The genuinely-minted-token shape
/// double-pins the gate ordering now (the establisher variant's /// double-pins the gate ordering now (the establisher variant's
/// pinned order, alktunnels 0.5.0 / ADR-008 Amendment 2): the ACL /// pinned order, alktunnels 0.5.0 / ADR-008 Amendment 2): the ACL
/// refuses before the establisher's gate ever looks up the token — /// refuses before the establisher's gate ever looks up the token;
/// an unscoped arrival could not leak a gate pass. /// an unscoped arrival could not leak a gate pass.
#[tokio::test] #[tokio::test]
async fn the_door_side_acl_refuses_an_unscoped_arrival() { async fn the_door_side_acl_refuses_an_unscoped_arrival() {
@@ -1664,7 +1664,7 @@ mod serving {
); );
// A real token (minted for this listener), unscoped arrival // A real token (minted for this listener), unscoped arrival
// identity: the door-side ACL refuses the open op — the fabric // identity: the door-side ACL refuses the open op, and the fabric
// side sees the refusal, never a channel allocation. // side sees the refusal, never a channel allocation.
let token = double.minted_token().await; let token = double.minted_token().await;
let params = alktunnels::params::TunnelForwardedParams { let params = alktunnels::params::TunnelForwardedParams {
@@ -1693,7 +1693,7 @@ mod serving {
/// - *Contract stamp:* AGENTS.md convention 9 (the listener count /// - *Contract stamp:* AGENTS.md convention 9 (the listener count
/// is a bounded resource): the per-session listener map is /// is a bounded resource): the per-session listener map is
/// bounded through its dedicated counter against the same /// bounded through its dedicated counter against the same
/// `max_concurrent_channels` ceiling — the listen beyond the /// `max_concurrent_channels` ceiling; the listen beyond the
/// count answers the vanilla `RequestDenied` while channels stay /// count answers the vanilla `RequestDenied` while channels stay
/// free, and the cancel releases the count again. /// free, and the cancel releases the count again.
#[tokio::test] #[tokio::test]
@@ -1739,10 +1739,10 @@ mod serving {
/// - *Contract stamp:* AGENTS.md convention 9 / the acceptor /// - *Contract stamp:* AGENTS.md convention 9 / the acceptor
/// vocabulary's cap rows (the wave-2 gate's booked shape): the /// vocabulary's cap rows (the wave-2 gate's booked shape): the
/// per-arrival admission is the owning session's in-crate /// per-arrival admission is the owning session's in-crate
/// channel cap — an arrival onto an exhausted count is refused /// channel cap: an arrival onto an exhausted count is refused
/// at the accept surface (no SSH channel opens; the fabric half /// at the accept surface (no SSH channel opens; the fabric half
/// drops, teardown-at-birth), and an established channel's close /// drops, teardown-at-birth), and an established channel's close
/// recovers the count (the next arrival is admitted — the /// recovers the count (the next arrival is admitted, the
/// release proof). /// release proof).
#[tokio::test] #[tokio::test]
async fn the_arrival_admission_is_cap_gated_at_the_accept_surface() { async fn the_arrival_admission_is_cap_gated_at_the_accept_surface() {
@@ -1791,12 +1791,12 @@ mod serving {
.expect_err("the refused arrival's socket EOFs"); .expect_err("the refused arrival's socket EOFs");
let _ = eof; let _ = eof;
// The release proof: the first arrival's socket goes — the // The release proof: the first arrival's socket goes; the
// pump's fabric→SSH direction EOFs (the pump's // pump's fabric→SSH direction EOFs (the pump's
// shutdown-on-completion half-closes the SSH channel — the // shutdown-on-completion half-closes the SSH channel; the
// OpenSSH client, whose target socket died with the arrival, // OpenSSH client, whose target socket died with the arrival,
// closes its half; the harness's stand-in does exactly that). // closes its half; the harness's stand-in does exactly that).
// That close — not the pump — is what fires the packet loop's // That close (not the pump) is what fires the packet loop's
// established-channel release. // established-channel release.
drop(first_arrival); drop(first_arrival);
let _ = first_channel.close().await; let _ = first_channel.close().await;
@@ -1807,7 +1807,7 @@ mod serving {
third_arrival.write_all(b"again").await.expect("rides"); third_arrival.write_all(b"again").await.expect("rides");
let third_open = tokio::time::timeout(Duration::from_secs(15), opens.recv()) let third_open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
.await .await
.expect("the next arrival's open arrives — the released count recovered") .expect("the next arrival's open arrives; the released count recovered")
.expect("observed"); .expect("observed");
let ForwardedOpen::Tcpip { let ForwardedOpen::Tcpip {
channel: mut third_channel, channel: mut third_channel,
@@ -1836,7 +1836,7 @@ mod serving {
/// - *Contract stamp:* OQ-11 on the listen flow (the runtime-Unix /// - *Contract stamp:* OQ-11 on the listen flow (the runtime-Unix
/// gate riding the `streamlocal_forward` serving flow) + the unix /// gate riding the `streamlocal_forward` serving flow) + the unix
/// row of the `-R` leg: the unix listen serves through the fabric /// row of the `-R` leg: the unix listen serves through the fabric
/// (the path echoes verbatim — no port semantics, no port /// (the path echoes verbatim; no port semantics, no port
/// out-param), the arrival opens /// out-param), the arrival opens
/// `forwarded-streamlocal@openssh.com` toward the client, and the /// `forwarded-streamlocal@openssh.com` toward the client, and the
/// cancel form tears it down 1:1 (path-matched). /// cancel form tears it down 1:1 (path-matched).
@@ -1908,7 +1908,7 @@ mod serving {
} }
/// - *Contract stamp:* the unset-declines contract on the listen /// - *Contract stamp:* the unset-declines contract on the listen
/// seam (`w3-tunnel-listen`'s whole-`Option` shape — an /// seam (`w3-tunnel-listen`'s whole-`Option` shape; an
/// egress-only door is legitimate config): a `tunnel` build with /// egress-only door is legitimate config): a `tunnel` build with
/// a fabric leg but no `tunnel_listen_seam` declines every listen /// a fabric leg but no `tunnel_listen_seam` declines every listen
/// form with the vanilla `RequestDenied`, while the leg-1 rows /// form with the vanilla `RequestDenied`, while the leg-1 rows
@@ -1952,7 +1952,7 @@ mod serving {
assert!(matches!(err, Error::RequestDenied), "got {err:?}"); assert!(matches!(err, Error::RequestDenied), "got {err:?}");
// The egress door still routes: the direct row reaches the // The egress door still routes: the direct row reaches the
// fabric (the fail-dial class — `ConnectFailed`, not // fabric (the fail-dial class `ConnectFailed`, not
// `AdministrativelyProhibited`). // `AdministrativelyProhibited`).
let err = open_direct_tcpip(&session.client, "127.0.0.1", 36005) let err = open_direct_tcpip(&session.client, "127.0.0.1", 36005)
.await .await
@@ -1970,7 +1970,7 @@ mod serving {
/// one-per-op-per-registry (mapping.md §Global-request rows / the /// one-per-op-per-registry (mapping.md §Global-request rows / the
/// `install_forwarded_surface` claim): alkcall's `register` /// `install_forwarded_surface` claim): alkcall's `register`
/// silently replaces an existing op entry, so the door /// silently replaces an existing op entry, so the door
/// pre-checks — a second `SshAdapter` composed over the same /// pre-checks; a second `SshAdapter` composed over the same
/// shared seam registry is a **failed construction** /// shared seam registry is a **failed construction**
/// (`AdapterError::ListenRegistration`), never a silent steal of /// (`AdapterError::ListenRegistration`), never a silent steal of
/// the first adapter's arrival routes. /// the first adapter's arrival routes.
@@ -2011,7 +2011,7 @@ mod serving {
mod client_smoke { mod client_smoke {
//! The full-circle smoke (`w4-reverse-listen`'s recorded posture): //! The full-circle smoke (`w4-reverse-listen`'s recorded posture):
//! the **alkssh client's** `-R` rows against the **alkssh server //! the **alkssh client's** `-R` rows against the **alkssh server
//! direction's** `-R` rows — both directions of the crate in one //! direction's** `-R` rows, both directions of the crate in one
//! live wire, the route-1 bridge's two legs meeting through the //! live wire, the route-1 bridge's two legs meeting through the
//! alkcall fabric double (the composition the alkforge assembly //! alkcall fabric double (the composition the alkforge assembly
//! makes). A connection to the minted bound port round-trips //! makes). A connection to the minted bound port round-trips
@@ -2174,7 +2174,7 @@ mod client_smoke {
#[cfg(all(feature = "tunnel", unix))] #[cfg(all(feature = "tunnel", unix))]
mod real_tool { mod real_tool {
//! The real-tool smoke (`w3-tunnel-direct`): one `ssh -L` (+ curl) //! The real-tool smoke (`w3-tunnel-direct`): one `ssh -L` (+ curl)
//! through a TCP-mounted adapter against the fabric double — the //! through a TCP-mounted adapter against the fabric double, the
//! POC-3 §C.1 gate re-landed. Tooling posture (recorded, like the //! POC-3 §C.1 gate re-landed. Tooling posture (recorded, like the
//! git task's): the smoke requires `ssh`/`curl` in the environment; //! git task's): the smoke requires `ssh`/`curl` in the environment;
//! when absent it is **skipped with a printed reason** //! when absent it is **skipped with a printed reason**
@@ -2191,7 +2191,7 @@ mod real_tool {
use super::serving::{wire_fabric, wire_fabric_with_listen}; use super::serving::{wire_fabric, wire_fabric_with_listen};
use alkssh::TunnelListenSeam; use alkssh::TunnelListenSeam;
/// The smoke's dial seam: a real-TCP dial closure (hand-rolled — /// The smoke's dial seam: a real-TCP dial closure (hand-rolled;
/// the assembly's `DialFn` injected over the substrate for real; /// the assembly's `DialFn` injected over the substrate for real;
/// alktunnels' `local` feature stays off). /// alktunnels' `local` feature stays off).
fn tcp_dial() -> DialFn { fn tcp_dial() -> DialFn {
@@ -2210,7 +2210,7 @@ mod real_tool {
}) })
} }
/// A tiny HTTP/1.1 endpoint (the POC's shape) — the substrate the /// A tiny HTTP/1.1 endpoint (the POC's shape), the substrate the
/// forwarded traffic dials through the fabric. /// forwarded traffic dials through the fabric.
async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) { async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0") let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
@@ -2248,7 +2248,7 @@ mod real_tool {
.output() .output()
.is_err() .is_err()
{ {
eprintln!("SKIPPED (with reason): real-tool tunnel smoke — ssh is unavailable"); eprintln!("SKIPPED (with reason): real-tool tunnel smoke, ssh is unavailable");
return; return;
} }
if std::process::Command::new("curl") if std::process::Command::new("curl")
@@ -2256,7 +2256,7 @@ mod real_tool {
.output() .output()
.is_err() .is_err()
{ {
eprintln!("SKIPPED (with reason): real-tool tunnel smoke — curl is unavailable"); eprintln!("SKIPPED (with reason): real-tool tunnel smoke, curl is unavailable");
return; return;
} }
@@ -2371,7 +2371,7 @@ mod real_tool {
let stdout = String::from_utf8_lossy(&through.stdout).to_string(); let stdout = String::from_utf8_lossy(&through.stdout).to_string();
assert!( assert!(
through.status.success() && stdout.trim_end() == body.trim_end(), through.status.success() && stdout.trim_end() == body.trim_end(),
"vanilla ssh -L + curl through the door → fabric → endpoint returns the body — got {stdout:?}" "vanilla ssh -L + curl through the door → fabric → endpoint returns the body; got {stdout:?}"
); );
let _ = std::fs::remove_dir_all(&work); let _ = std::fs::remove_dir_all(&work);
@@ -2409,7 +2409,7 @@ mod real_tool {
.output() .output()
.is_err() .is_err()
{ {
eprintln!("SKIPPED (with reason): real-tool -R smoke — ssh is unavailable"); eprintln!("SKIPPED (with reason): real-tool -R smoke, ssh is unavailable");
return; return;
} }
if std::process::Command::new("curl") if std::process::Command::new("curl")
@@ -2417,7 +2417,7 @@ mod real_tool {
.output() .output()
.is_err() .is_err()
{ {
eprintln!("SKIPPED (with reason): real-tool -R smoke — curl is unavailable"); eprintln!("SKIPPED (with reason): real-tool -R smoke, curl is unavailable");
return; return;
} }
@@ -2524,7 +2524,7 @@ mod real_tool {
let stdout = String::from_utf8_lossy(&through.stdout).to_string(); let stdout = String::from_utf8_lossy(&through.stdout).to_string();
assert!( assert!(
through.status.success() && stdout.trim_end() == body.trim_end(), through.status.success() && stdout.trim_end() == body.trim_end(),
"vanilla ssh -R + curl through the fabric → arrival → forwarded-tcpip → client-dialed endpoint returns the body — got {stdout:?}" "vanilla ssh -R + curl through the fabric → arrival → forwarded-tcpip → client-dialed endpoint returns the body; got {stdout:?}"
); );
} }
+3 -3
View File
@@ -1,12 +1,12 @@
# Independent-issuer fingerprint vectors: the alkssh wave-1 test-key # Independent-issuer fingerprint vectors: the alkssh wave-1 test-key
# fixture set. Each entry pairs a committed OpenSSH ed25519 keypair (the # fixture set. Each entry pairs a committed OpenSSH ed25519 keypair (the
# .key/.pub files in this directory) with its fingerprint byte-verbatim # .key/.pub files in this directory) with its fingerprint byte-verbatim
# from `ssh-keygen -lf` — recorded from OpenSSH_9.6p1 # from `ssh-keygen -lf`, recorded from OpenSSH_9.6p1
# (Ubuntu-3ubuntu13.20) at vector-generation time. # (Ubuntu-3ubuntu13.20) at vector-generation time.
# #
# The keys are throwaway test fixtures minted for the test suite only; # The keys are throwaway test fixtures minted for the test suite only;
# their private halves are committed on purpose (committed keys are # their private halves are committed on purpose (committed keys are
# throwaway fixtures, not secrets — they are never registered anywhere # throwaway fixtures, not secrets; they are never registered anywhere
# real and never deployed). # real and never deployed).
# #
# <fixture> <expected fingerprint> # <fixture> <expected fingerprint>
@@ -16,7 +16,7 @@ charlie.pub SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U
delta.pub SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs delta.pub SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs
ca.pub SHA256:mPAq1aRKBuyH3gZs9IcLWtc1X3AhYi50P80mpo7XPSw ca.pub SHA256:mPAq1aRKBuyH3gZs9IcLWtc1X3AhYi50P80mpo7XPSw
# alpha-cert.pub: the alpha user certificate (serial 1, key id # alpha-cert.pub: the alpha user certificate (serial 1, key id
# `alkssh-fixture-alpha`, principal `git`, validity 2026–2036) — the # `alkssh-fixture-alpha`, principal `git`, validity 2026–2036): the
# ssh-keygen output above the fingerprint line is its OpenSSH text; the # ssh-keygen output above the fingerprint line is its OpenSSH text; the
# certificate's embedded public half is alpha.pub (the same fingerprint # certificate's embedded public half is alpha.pub (the same fingerprint
# as alpha.pub; `ssh-keygen -lf alpha-cert.pub` reports it with the # as alpha.pub; `ssh-keygen -lf alpha-cert.pub` reports it with the