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:
1 parent
f4b5f896b4
commit
1e583ab55b
55 files changed
+1756
-1751
No files matched your search
+21
-21
@@ -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
@@ -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/)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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>> {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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"),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user