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
+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.
|
||||
//!
|
||||
//! Two vehicles, each for a structural reason:
|
||||
//!
|
||||
//! - **Wire rows** (the adapter's tokio-duplex mount + russh's own
|
||||
//! 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
|
||||
//! `#[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
|
||||
//! precursor of the dedicated `w2-loopback-harness` vehicle (which
|
||||
//! records the transport choice and the helpers' vocabulary).
|
||||
//! - **Direct-hook rows** (the wave-1 doubles against
|
||||
//! [`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
|
||||
//! ([`alkssh::SessionHandler::resolve_bridge`], structured out of the
|
||||
//! `#[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::{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).
|
||||
struct AcceptAllHostKeys;
|
||||
|
||||
@@ -55,7 +55,7 @@ impl russh::client::Handler for AcceptAllHostKeys {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
provider: Arc<MemIdentityProvider>,
|
||||
limits: Limits,
|
||||
@@ -109,7 +109,7 @@ fn registered_provider(fingerprints: &[&str]) -> Arc<MemIdentityProvider> {
|
||||
}
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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");
|
||||
assert!(
|
||||
result.success(),
|
||||
"every username authenticates identically — {username}: {result:?}"
|
||||
"every username authenticates identically: {username}: {result:?}"
|
||||
);
|
||||
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
|
||||
/// the advertisement honesty (ADR-004 decision 4 — exactly
|
||||
/// the advertisement honesty (ADR-004 decision 4, exactly
|
||||
/// `{publickey}` remains advertised on the decline).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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
|
||||
/// hook, not the advertisement's).
|
||||
#[tokio::test]
|
||||
@@ -258,7 +258,7 @@ async fn auth_password_rejects_over_the_wire() {
|
||||
}
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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!(
|
||||
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
|
||||
/// weakly-authorized session, no cached resolution).
|
||||
#[tokio::test]
|
||||
@@ -375,18 +375,18 @@ async fn bridge_core_rejects_unknown_and_revoked_fingerprints() {
|
||||
provider.revoke(fixture("delta").fingerprint);
|
||||
assert!(
|
||||
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
|
||||
/// 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
|
||||
/// cover the scheme classes the in-tree build cannot even parse
|
||||
/// (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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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).
|
||||
#[test]
|
||||
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
|
||||
/// 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,
|
||||
/// and the absent shape still refuses fail-closed if it were).
|
||||
#[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
|
||||
//! in-crate concurrent-channel cap at every `channel_open_*` accept
|
||||
//! path, its recovery on close, its `Limits` tunability, and the
|
||||
@@ -8,7 +8,7 @@
|
||||
//! `channel_buffer_size`), the inactivity wall clock and the remaining
|
||||
//! auth-counting posture ride the config-mapping tests
|
||||
//! (`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.
|
||||
//!
|
||||
//! 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
|
||||
/// row's [`Limits`] — the bounded-resources rows' driver.
|
||||
/// row's [`Limits`], the bounded-resources rows' driver.
|
||||
async fn authenticated_with(limits: Limits) -> LoopbackSession {
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let adapter = SshAdapter::new(serve_config(
|
||||
@@ -67,7 +67,7 @@ async fn authenticated_with(limits: Limits) -> LoopbackSession {
|
||||
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.
|
||||
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
assert!(
|
||||
@@ -80,7 +80,7 @@ fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// client) + ADR-004 decision 6 (the mapping in `adapter.rs` makes
|
||||
/// 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
|
||||
/// failures with the advertisement intact; the third request past the
|
||||
/// 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
|
||||
/// **empty** remaining-methods set (no reply ever came; the
|
||||
/// advertisement is gone). The session ends cleanly server-side.
|
||||
@@ -165,7 +165,7 @@ async fn auth_attempt_ceiling_disconnects_an_exceeding_client() {
|
||||
);
|
||||
}
|
||||
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).
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ async fn auth_attempt_ceiling_disconnects_an_exceeding_client() {
|
||||
.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).
|
||||
fn remaining_ads(result: &russh::client::AuthResult) -> Option<russh::MethodSet> {
|
||||
match result {
|
||||
@@ -208,7 +208,7 @@ async fn concurrent_channel_cap_declines_and_recovers_over_the_wire() {
|
||||
|
||||
let err = open_session_channel(&session.client)
|
||||
.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(
|
||||
err,
|
||||
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
|
||||
// removes a locally-closed channel from its map before the
|
||||
// server's CHANNEL_CLOSE arrives back, so no message — and no
|
||||
// `None` — is ever delivered (the harness's `await_close` only
|
||||
// server's CHANNEL_CLOSE arrives back; no message, and no
|
||||
// `None`, is ever delivered (the harness's `await_close` only
|
||||
// observes *server-initiated* closes). The recovery observation is
|
||||
// the open itself, and it is deterministic: close and open ride
|
||||
// the client loop's single FIFO event queue, they are written in
|
||||
// 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
|
||||
// declines and this expect fails.
|
||||
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 {
|
||||
let err = open_session_channel(&session.client)
|
||||
.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(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
@@ -262,8 +262,8 @@ async fn zero_cap_refuses_every_open_and_poisons_nothing() {
|
||||
|
||||
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded admission
|
||||
/// mechanics (decline-first-alloc-never applies to the cap budget
|
||||
/// too): the tunnel row's pre-admit classes — here the **absent
|
||||
/// fabric handle** under the feature — answer before the admission
|
||||
/// too): the tunnel row's pre-admit classes (here the **absent
|
||||
/// fabric handle** under the feature) answer before the admission
|
||||
/// is taken, so under an exhausted cap the absent-handle class
|
||||
/// (`ConnectFailed`, the cannot-bridge wire class) wins over the cap
|
||||
/// 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)
|
||||
.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)
|
||||
.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
|
||||
//! client-direction vehicle) plus the crate-to-crate smoke against
|
||||
//! the alkssh server direction. The row/stamp discipline
|
||||
@@ -21,7 +21,7 @@ use russh::{ChannelOpenFailure, Error};
|
||||
|
||||
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
|
||||
/// the production source).
|
||||
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
|
||||
/// explicit-vs-drop boundary, the OQ-12 primitives-only posture) —
|
||||
/// **drop is never the clean path**: dropping the (feature-free,
|
||||
/// explicit-vs-drop boundary, the OQ-12 primitives-only posture):
|
||||
/// **drop is never the clean path**. Dropping the (feature-free,
|
||||
/// 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
|
||||
/// `dial_success_lands_an_authenticated_connection`'s `Ok`).
|
||||
#[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;
|
||||
assert!(
|
||||
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:?}"
|
||||
);
|
||||
double.shutdown();
|
||||
}
|
||||
|
||||
/// - *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,
|
||||
/// and the clean close the peer observes.
|
||||
#[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
|
||||
/// 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.
|
||||
///
|
||||
/// - *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
|
||||
/// 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
|
||||
/// wire crash class, never a fallback.
|
||||
#[tokio::test]
|
||||
@@ -224,7 +224,7 @@ async fn the_crate_default_policy_connects_to_nothing() {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// real fingerprint rode the policy; the crate never second-guesses).
|
||||
#[tokio::test]
|
||||
@@ -257,7 +257,7 @@ async fn pin_mismatch_hard_declines() {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// was this row's explicit choice, `MemHostKeyPolicy::
|
||||
/// with_default(true)`).
|
||||
@@ -285,7 +285,7 @@ async fn case2_policy_decides_and_the_dial_proceeds() {
|
||||
|
||||
/// - *Contract stamp:* the publickey-only family contract: the
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
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
|
||||
/// acceptance): the dial's russh `Config` carries the
|
||||
/// [`Limits`]-mapped knobs — the keepalive 15 s / 3 posture, the
|
||||
/// always-set wall clock, the window/packet/buffer knobs verbatim —
|
||||
/// [`Limits`]-mapped knobs (the keepalive 15 s / 3 posture, the
|
||||
/// always-set wall clock, the window/packet/buffer knobs verbatim)
|
||||
/// observed from outside the crate through the doc-hidden mapping.
|
||||
#[test]
|
||||
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
|
||||
/// 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
|
||||
/// accept-and-drop default; the never-feature rows (x11, agent)
|
||||
/// 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
|
||||
/// server surface exposes no initiator for an unknown type and the
|
||||
/// `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.)
|
||||
#[tokio::test]
|
||||
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!(
|
||||
policy.decisions().len(),
|
||||
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(&[
|
||||
@@ -440,7 +440,7 @@ async fn server_initiated_opens_decline_cleanly_on_the_wire() {
|
||||
|
||||
/// The smoke posture (`w4-dial`'s decided + recorded item): the
|
||||
/// 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
|
||||
/// 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
|
||||
//! ([`sshd_double`](common::sshd_double), the client-direction
|
||||
//! vehicle): featured + working rows (exec exit-status, pty
|
||||
//! window-change riding the live pump, the direct-tcpip pump, the git
|
||||
//! 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
|
||||
//! row/stamp discipline (`tests/common/mod.rs`'s doc) applies
|
||||
//! verbatim.
|
||||
@@ -13,8 +13,8 @@
|
||||
//! rows' cheap real-stack smoke is the alkssh-client-op-row ↔
|
||||
//! alkssh-server-direction loopback (both russh stacks; the client
|
||||
//! exec row driven against the server direction's *real* session rows
|
||||
//! — the chunk bridge's two arms meeting over one live wire), gated
|
||||
//! on tooling like the wave-3 smokes — skipped-with-reason when
|
||||
//! and the chunk bridge's two arms meeting over one live wire), gated
|
||||
//! on tooling like the wave-3 smokes (skipped-with-reason when
|
||||
//! absent, never silently green. A real-vanilla-`sshd` dial is wave-5
|
||||
//! 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::{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).
|
||||
fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
|
||||
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)))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// for the clean-close rows) and the double's observations.
|
||||
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
|
||||
/// session `Ok` — the decline rows' "the connection stays usable"
|
||||
/// session `Ok`, the decline rows' "the connection stays usable"
|
||||
/// final act.
|
||||
async fn expect_clean_close(connection: SshConnection, mut double: SshdDouble) {
|
||||
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**
|
||||
/// 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).
|
||||
///
|
||||
/// - *Contract stamp:* OQ-13 (the decline placeholder is the shipped
|
||||
/// 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,
|
||||
/// and the connection stays usable (the clean close is the double's
|
||||
/// clean session end).
|
||||
@@ -135,7 +135,7 @@ async fn sftp_feature_on_the_placeholder_still_declines() {
|
||||
let err = connection
|
||||
.sftp()
|
||||
.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!(observed.execs().is_empty());
|
||||
no_open_observed(&mut double, "the featured placeholder's decline").await;
|
||||
@@ -150,7 +150,7 @@ mod session_rows {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// 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`]).
|
||||
fn frame(stream_type: u8, payload: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![stream_type];
|
||||
@@ -159,14 +159,14 @@ mod session_rows {
|
||||
out
|
||||
}
|
||||
|
||||
/// Frames one control chunk — the family currency's own
|
||||
/// Frames one control chunk; the family currency's own
|
||||
/// serializer builds the payload.
|
||||
async fn control_frame(message: &ControlMessage) -> Vec<u8> {
|
||||
let payload = message.to_json().expect("the control serializes");
|
||||
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).
|
||||
async fn read_to_exit(mut session: alkssh::RemoteSession) -> (Vec<u8>, Vec<u8>, Option<i32>) {
|
||||
let mut stdout = Vec::new();
|
||||
@@ -206,7 +206,7 @@ mod session_rows {
|
||||
}
|
||||
|
||||
/// - *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`);
|
||||
/// stdout/stderr come back as distinct chunks and the exit
|
||||
/// status rides the family chunk in-band, last.
|
||||
@@ -241,7 +241,7 @@ mod session_rows {
|
||||
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,
|
||||
/// not as `Err` from the request call**) →
|
||||
/// [`OpError::RequestRefused`]; the refused row closes its
|
||||
@@ -271,7 +271,7 @@ mod session_rows {
|
||||
/// (the server direction's pre-parse cap, `Limits::
|
||||
/// max_exec_command_len`, mirrored at the command-construction
|
||||
/// 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]
|
||||
async fn oversized_exec_declines_before_any_wire_op() {
|
||||
let limits = Limits {
|
||||
@@ -295,7 +295,7 @@ mod session_rows {
|
||||
}
|
||||
|
||||
/// - *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:
|
||||
/// the double sees the params, and the wire request carries the
|
||||
/// 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
|
||||
/// 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
|
||||
/// `params: None` shell carries **no** pty request.
|
||||
#[tokio::test]
|
||||
@@ -348,7 +348,7 @@ mod session_rows {
|
||||
/// - *Contract stamp:* the pty control rows riding the **live
|
||||
/// pump** (the POC-3 §D.3 window-change/signal mechanics',
|
||||
/// 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`
|
||||
/// and `Signal` to the wire `signal` (the double's
|
||||
/// observations are the witnesses).
|
||||
@@ -418,7 +418,7 @@ mod session_rows {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// family code `−15` in-band (rule 2's value, the table read
|
||||
/// backwards; the derivation never invents).
|
||||
@@ -447,8 +447,8 @@ mod session_rows {
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* the never-half-capability posture (ADR-003
|
||||
/// decision 3, client mirror) — a **refused pty refuses the
|
||||
/// whole row**: the exec behind it never rides the wire (the
|
||||
/// decision 3, client mirror): a **refused pty refuses the
|
||||
/// whole row**; the exec behind it never rides the wire (the
|
||||
/// double's exec observations stay empty).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// refused (the scripted count) → `OpenDeclined(Prohibited)`,
|
||||
/// then a later launch on the *same connection* succeeds and
|
||||
/// pumps — the usable-after-decline posture — and everything
|
||||
/// closes clean.
|
||||
/// pumps; that is the usable-after-decline posture, and
|
||||
/// everything closes clean.
|
||||
#[tokio::test]
|
||||
async fn server_rejected_open_declines_and_the_connection_stays_usable() {
|
||||
let script = SshdScript {
|
||||
@@ -526,7 +526,7 @@ mod session_rows {
|
||||
/// - *Contract stamp:* the task's smoke item (the §module docs'
|
||||
/// decided posture; like the wave-3 smokes, skipped-with-reason
|
||||
/// 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
|
||||
/// arms meeting (the client's family plane consuming the
|
||||
/// server's family plane), data and the exit-status
|
||||
@@ -539,7 +539,7 @@ mod session_rows {
|
||||
.output()
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -622,8 +622,8 @@ mod tunnel_rows {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// - *Contract stamp:* `client.md` §op rows (row 2, the typed
|
||||
/// `channel_open_direct_tcpip` open — the alksocks `DialFn`
|
||||
/// composes over this surface) + POC-3 §C.1's recipe reversed —
|
||||
/// `channel_open_direct_tcpip` open; the alksocks `DialFn`
|
||||
/// composes over this surface) + POC-3 §C.1's recipe reversed:
|
||||
/// the pump is the raw byte duplex, round-tripping, no channel
|
||||
/// queue consumed anywhere between the open and the
|
||||
/// `into_stream()` conversion.
|
||||
@@ -654,7 +654,7 @@ mod tunnel_rows {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// decline class**: after the `ConnectFailed` class the
|
||||
/// 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
|
||||
/// 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;
|
||||
/// the on-Unix builds ride the usable-after-decline row's
|
||||
/// success half.
|
||||
@@ -729,7 +729,7 @@ mod git_rows {
|
||||
/// writes (a placeholder pack-side payload for the pump row).
|
||||
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
|
||||
/// carries `git-upload-pack '/owner/demo'` byte-verbatim) + the
|
||||
/// pack duplex pump + the exit-status consumption.
|
||||
@@ -793,7 +793,7 @@ mod git_rows {
|
||||
}
|
||||
|
||||
/// - *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.
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
async fn refused_git_exec_surfaces_the_request_refusal() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! ([`sshd_double`](common::sshd_double): the `tcpip-forward` /
|
||||
//! `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 cancels + the listener-cap ceiling), the acceptor-registry rows
|
||||
//! (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
|
||||
//! (OQ-11), and the connection-stays-usable row after every decline
|
||||
//! class. The lifecycle rows pin the handles' posture: the actually-
|
||||
@@ -20,12 +20,12 @@
|
||||
//!
|
||||
//! **Per-build matrix note (recorded):** this file is the
|
||||
//! `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
|
||||
//! every build through `tests/client_dial.rs`'s
|
||||
//! `server_initiated_opens_decline_cleanly_on_the_wire` (the
|
||||
//! 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")]
|
||||
|
||||
mod common;
|
||||
@@ -42,7 +42,7 @@ use common::sshd_double::{DblListen, SshdDouble, SshdObserved, SshdScript, spawn
|
||||
use common::{MemHostKeyPolicy, TestVault, fixture};
|
||||
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).
|
||||
fn alpha_key_source() -> Arc<dyn alkssh::KeySource> {
|
||||
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)))
|
||||
}
|
||||
|
||||
/// 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(
|
||||
script: SshdScript,
|
||||
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
|
||||
/// 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) {
|
||||
assert!(!connection.is_closed());
|
||||
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
|
||||
/// 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).
|
||||
async fn pick_port() -> u16 {
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// channel; the pump is the assembly's).
|
||||
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).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct OpenRecord {
|
||||
@@ -125,7 +125,7 @@ struct OpenRecord {
|
||||
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:
|
||||
/// the assembly owns the pump).
|
||||
#[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)]
|
||||
struct UnixAcceptorDouble {
|
||||
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).
|
||||
async fn next_open(
|
||||
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
|
||||
/// listen + ingest row) + the module docs' listen-gate record —
|
||||
/// featured + registered → served: the
|
||||
/// listen + ingest row) + the module docs' listen-gate record:
|
||||
/// featured + registered → served; the the
|
||||
/// `reverse_listen` request rides the wire (the double's
|
||||
/// `tcpip-forward` hook observes it verbatim), the actually-bound
|
||||
/// 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!(
|
||||
listener.endpoint().bound_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 =
|
||||
@@ -255,7 +255,7 @@ async fn reverse_listen_serves_and_the_data_leg_pumps() {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// it; the double's listener is the real socket, so the mint is the
|
||||
/// 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
|
||||
/// 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
|
||||
/// half-capability posture: a bound-but-unserveable listener is
|
||||
/// 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!(
|
||||
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;
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// live connection with an empty registry answers
|
||||
/// `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
|
||||
/// 2 — consumers turn capabilities on/off per connection) + the
|
||||
/// arrival-time gate — the same live connection: the pre-install
|
||||
/// 2, consumers turn capabilities on/off per connection) + the
|
||||
/// arrival-time gate, the same live connection: the pre-install
|
||||
/// arrival declines, the post-install arrival serves (never
|
||||
/// buffered, never half-served), and the acceptor double's late
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// post-uninstall arrival declines at arrival time (fail-closed;
|
||||
/// 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
|
||||
/// 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
|
||||
/// reconnects as refused), a cancel of an unknown listener is the
|
||||
/// peer's refusal (`ListenRefused`), and the connection stays
|
||||
@@ -557,7 +557,7 @@ async fn cancel_tears_the_forward_down_and_unknown_cancels_refuse() {
|
||||
}
|
||||
assert!(
|
||||
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;
|
||||
@@ -572,16 +572,16 @@ async fn cancel_tears_the_forward_down_and_unknown_cancels_refuse() {
|
||||
.await,
|
||||
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;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the
|
||||
/// explicit-vs-drop boundary, `w4-lifecycle`) — **no drop-timing
|
||||
/// wire state**: dropping the listener handle without a cancel
|
||||
/// frees nothing — the peer's listen keeps serving (the arrival
|
||||
/// explicit-vs-drop boundary, `w4-lifecycle`): **no drop-timing
|
||||
/// wire state**. Dropping the listener handle without a cancel
|
||||
/// frees nothing; the peer's listen keeps serving (the arrival
|
||||
/// still hands off through the registered acceptor); only the
|
||||
/// explicit cancel tears the forward down (the bound port then
|
||||
/// 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
|
||||
/// 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
|
||||
/// the ceiling the listens refuse pre-wire, the handle's cancel
|
||||
/// (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
|
||||
/// refuses (the unknown-listener wire answer, no release).
|
||||
#[tokio::test]
|
||||
@@ -733,7 +733,7 @@ async fn the_listener_handle_cancel_tears_the_forward_down_and_releases_the_budg
|
||||
.map(|(_, _, port)| *port)
|
||||
.collect::<Vec<_>>(),
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
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 \
|
||||
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
|
||||
/// 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
|
||||
/// tears the forward down (the bound port refuses a reconnect), the
|
||||
/// connection stays usable (the `cancel` row's channel-scoped
|
||||
/// 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]
|
||||
async fn cancel_mid_data_leg_tears_the_forward_down_mid_connection() {
|
||||
let scripted = SshdScript {
|
||||
@@ -814,14 +814,14 @@ async fn cancel_mid_data_leg_tears_the_forward_down_mid_connection() {
|
||||
}
|
||||
assert!(
|
||||
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;
|
||||
}
|
||||
|
||||
/// - *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:
|
||||
/// beyond it the listen declines **before any wire op**
|
||||
/// (`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
|
||||
/// 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
|
||||
/// uninstall/uninstall-again cycle reports honestly.
|
||||
#[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;
|
||||
assert!(
|
||||
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!(
|
||||
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
|
||||
/// 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**
|
||||
/// (`UnixUnavailable`; the platform-refused request consumes
|
||||
/// nothing).
|
||||
@@ -1044,7 +1044,7 @@ async fn streamlocal_declines_off_unix_before_any_wire_op() {
|
||||
);
|
||||
assert!(
|
||||
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;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
//! module) against. The recorded test convention (see
|
||||
//! `tests/observe.rs`'s module docs) applies verbatim: no
|
||||
//! `tracing-subscriber` dependency (the hand-rolled shape needs none),
|
||||
//! and the **global** default is the only capture shape that works —
|
||||
//! the server side runs on spawned tasks, so a thread-local default
|
||||
//! and the **global** default is the only capture shape that works
|
||||
//! (the server side runs on spawned tasks, so a thread-local default
|
||||
//! never sees their events.
|
||||
//!
|
||||
//! Rows install once per binary with [`install`], then match events by
|
||||
//! field pairs ([`await_event`]) — parallel rows never order against
|
||||
//! each other — and pin the decline event's exact vocabulary with
|
||||
//! field pairs ([`await_event`], so parallel rows never order against
|
||||
//! each other) and pin the decline event's exact vocabulary with
|
||||
//! [`assert_request_decline`] (the exact-field-set assertion is the
|
||||
//! safety posture: no dynamic extra fields exist).
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct 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> {
|
||||
self.fields
|
||||
.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).
|
||||
fn capture() -> &'static Arc<GlobalCapture> {
|
||||
static CAPTURE: OnceLock<Arc<GlobalCapture>> = OnceLock::new();
|
||||
@@ -108,7 +108,7 @@ pub fn install() {
|
||||
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
|
||||
/// bounded curls (the awaiting rows' `await_event` stays fixed-window;
|
||||
/// 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
|
||||
/// 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).
|
||||
pub async fn await_event(want: &[(&str, &str)]) -> Captured {
|
||||
let capture = capture();
|
||||
@@ -143,7 +143,7 @@ pub async fn await_event(want: &[(&str, &str)]) -> Captured {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
match (
|
||||
cfg!(feature = "session"),
|
||||
@@ -171,7 +171,7 @@ pub fn expected_build() -> &'static str {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// build self-describing, the message shape, and the `debug` level (a
|
||||
/// 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) {
|
||||
assert_eq!(
|
||||
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!(
|
||||
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
|
||||
//! `alkgit/src/backend.rs:164-254`'s noop impls (the shapes that file
|
||||
//! itself pins for dyn compatibility), with recording accessors the
|
||||
//! 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
|
||||
//! copy of alkgit's doubles' *code*.
|
||||
//!
|
||||
//! State/accessor split: the doubles are configurable per row (repo
|
||||
//! records with visibility + grants; advertised ref lines) and
|
||||
//! 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
|
||||
//! applied) is observable without alkgit's own test surface.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
@@ -33,13 +33,13 @@ use async_trait::async_trait;
|
||||
use gix_hash::ObjectId;
|
||||
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 {
|
||||
ObjectId::from([byte; 20])
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub struct RepoSetup {
|
||||
pub repo_id: String,
|
||||
@@ -116,7 +116,7 @@ pub struct GeneratedPack {
|
||||
|
||||
/// One per-repo serve gate (the `w4-git-exit-close` row's
|
||||
/// 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
|
||||
/// row schedules the serve's later stages (the answer flood, the
|
||||
/// 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
|
||||
/// trait and the rows share this state.
|
||||
#[derive(Default)]
|
||||
@@ -199,7 +199,7 @@ impl MemGit {
|
||||
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
|
||||
/// rows use the gate to prepare wire state (saturated queues,
|
||||
/// 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.
|
||||
fn record(id: &str, repos: &RepoRegistry) -> Result<RepoRecord, RegistryError> {
|
||||
match repos.get(id) {
|
||||
@@ -325,7 +325,7 @@ impl GitPackIngest for MemGit {
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn git_seam(mem: &Arc<MemGit>) -> 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
|
||||
//! crate: the wave-2 decline matrix, every wave-3 mapping's contract
|
||||
//! tests, and the client-direction smokes all drive the crate through
|
||||
@@ -8,7 +8,7 @@
|
||||
//! the server-side [`SshAdapter::handle`](alkssh::SshAdapter::handle)
|
||||
//! mount runs over a **tokio duplex pair** ([`tokio::io::duplex`],
|
||||
//! [`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
|
||||
//! 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
|
||||
@@ -16,13 +16,13 @@
|
||||
//! lifecycle, loopback timing, accept-loop flakiness under slow CI)
|
||||
//! buys only what the real-tool smokes of the mapping waves and the
|
||||
//! 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.
|
||||
//!
|
||||
//! **Placement decision (recorded, the `w2-loopback-harness`
|
||||
//! counterpart of the wave-1 doubles' location decision):** the
|
||||
//! 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
|
||||
//! (no re-export from `lib.rs`, no feature gate). Unlike the doubles
|
||||
//! it is a *vehicle* convention, not just a seam: later waves'
|
||||
@@ -37,7 +37,7 @@
|
||||
//! `check_server_key` hook ([`HarnessClientHandler`]), and the
|
||||
//! keepalive / wall-clock posture from [`alkssh::Limits`]
|
||||
//! ([`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,
|
||||
//! and the fail-closed rows construct `with_default(false)`.
|
||||
//!
|
||||
@@ -46,11 +46,11 @@
|
||||
//! [`LOOPBACK_HOST`] and [`LOOPBACK_PORT`]; host-key policy rows pin
|
||||
//! rules against those values verbatim. The presented key's
|
||||
//! 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
|
||||
//! 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
|
||||
//! plumbing, drain loops), never semantics: every observation is a
|
||||
//! russh-verbatim shape ([`AuthResult`], [`Error`],
|
||||
@@ -60,7 +60,7 @@
|
||||
//! landed the auth/session/exec/shell/subsystem vocabulary, and
|
||||
//! `w2-dispatch-decline-core` extended it with the decline-matrix
|
||||
//! 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
|
||||
//! construction this wave ... their live rows arrive with
|
||||
//! `w2-dispatch-decline-core`" is discharged by exactly that
|
||||
@@ -86,20 +86,20 @@ use alkssh::{
|
||||
|
||||
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
|
||||
/// this constant (see the module docs §Host identity).
|
||||
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.
|
||||
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.
|
||||
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`
|
||||
/// mapped verbatim, `inactivity_timeout` carried as always-set (the
|
||||
/// 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
|
||||
/// 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
|
||||
/// the row accepts (the harness hook answers the open itself).
|
||||
#[derive(Debug)]
|
||||
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.
|
||||
Tcpip {
|
||||
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
|
||||
/// forwarded opens are accepted and handed to the row through 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-
|
||||
/// in with no matching forward never accept-and-drops).
|
||||
pub struct ForwardedAcceptor {
|
||||
@@ -166,7 +166,7 @@ impl ForwardedAcceptor {
|
||||
/// The client-direction data gate (`w4-git-exit-close`'s row
|
||||
/// vehicle): armed on a channel id, the harness client's packet loop
|
||||
/// 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
|
||||
/// into the server's message queue until [`DataGate::release`]. The
|
||||
/// held counter (`DataGate::held_packets`) lets the row observe that
|
||||
@@ -179,7 +179,7 @@ pub struct 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.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -199,14 +199,14 @@ impl DataGate {
|
||||
*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
|
||||
/// loop is pended inside the hook).
|
||||
pub fn held_packets(&self) -> usize {
|
||||
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.
|
||||
pub fn release(&self) {
|
||||
*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
|
||||
/// the decision input exactly as the wave-4 dial wiring will shape it
|
||||
/// ([`PresentedHostKey`] over the crate's fingerprint path), plus the
|
||||
@@ -241,9 +241,9 @@ pub struct 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
|
||||
/// 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
|
||||
/// [`HarnessClientHandler::with_forwarded_acceptor`]).
|
||||
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
|
||||
/// 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
|
||||
/// row releases. Inert otherwise; also the block counter the row
|
||||
/// 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
|
||||
/// keys, with the session limits applied.
|
||||
pub fn serve_config(
|
||||
@@ -389,7 +389,7 @@ pub fn serve_config(
|
||||
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
|
||||
/// the construction tests' business (`w2-server-mount`), so a harness
|
||||
/// 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
|
||||
/// connected client handle and its policy double.
|
||||
pub struct LoopbackSession {
|
||||
/// The connected russh client — the helpers' driver.
|
||||
/// The connected russh client, the helpers' driver.
|
||||
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.
|
||||
pub host_key_policy: Arc<MemHostKeyPolicy>,
|
||||
server_task: tokio::task::JoinHandle<Result<(), AdapterError>>,
|
||||
@@ -416,7 +416,7 @@ pub struct LoopbackSession {
|
||||
impl LoopbackSession {
|
||||
/// Ends one loopback row cleanly: disconnects the client, waits
|
||||
/// 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) {
|
||||
self.client
|
||||
.disconnect(Disconnect::ByApplication, "row done", "en")
|
||||
@@ -432,7 +432,7 @@ impl LoopbackSession {
|
||||
/// Drives one loopback session: mounts the adapter over a duplex pair
|
||||
/// (spawned to completion, the assembly's accept-loop shape), connects
|
||||
/// 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.
|
||||
///
|
||||
/// 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 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
|
||||
/// hook consumes the installed acceptor; `None` = the clean-decline
|
||||
/// posture [`drive`] carries).
|
||||
@@ -459,7 +459,7 @@ pub async fn drive_with_acceptor(
|
||||
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.
|
||||
pub async fn drive_with_data_gate(
|
||||
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
|
||||
/// [`AuthResult`] comes back verbatim for the success / failure-shape
|
||||
/// rows to match on.
|
||||
@@ -515,7 +515,7 @@ pub async fn authenticate_publickey(
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn remaining_methods(result: &AuthResult) -> Option<MethodSet> {
|
||||
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
|
||||
/// ([`Error::ChannelOpenFailure`] carries the SSH decline reason the
|
||||
/// decline-matrix rows match on).
|
||||
@@ -536,7 +536,7 @@ pub async fn open_session_channel(
|
||||
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,
|
||||
/// the decline-matrix rows' reason surface.
|
||||
pub async fn open_direct_tcpip(
|
||||
@@ -549,7 +549,7 @@ pub async fn open_direct_tcpip(
|
||||
.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.
|
||||
pub async fn open_direct_streamlocal(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
@@ -558,7 +558,7 @@ pub async fn open_direct_streamlocal(
|
||||
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.
|
||||
pub async fn open_x11_channel(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
@@ -570,7 +570,7 @@ pub async fn open_x11_channel(
|
||||
.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`]
|
||||
/// through [`next_message`]).
|
||||
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
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub async fn request_pty(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
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).
|
||||
pub async fn request_env(channel: &Channel<Msg>, name: &str, value: &str) -> Result<(), Error> {
|
||||
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).
|
||||
pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
channel
|
||||
@@ -607,16 +607,16 @@ pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends an agent-forwarding request over a channel (`want_reply` set
|
||||
/// — the never-feature request rows; ADR-003 §5).
|
||||
/// Sends an agent-forwarding request over a channel (`want_reply`
|
||||
/// set, the never-feature request rows; ADR-003 §5).
|
||||
pub async fn request_agent_forward(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
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
|
||||
/// ([`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).
|
||||
pub async fn tcpip_forward(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
@@ -651,7 +651,7 @@ pub async fn cancel_streamlocal_forward(
|
||||
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
|
||||
/// end).
|
||||
pub async fn next_forwarded_open(
|
||||
@@ -660,13 +660,13 @@ pub async fn next_forwarded_open(
|
||||
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).
|
||||
pub async fn next_message(channel: &mut Channel<Msg>) -> Option<ChannelMsg> {
|
||||
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
|
||||
/// observation shape) or the channel's end, which is `None`.
|
||||
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
|
||||
/// (`false`).
|
||||
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/`
|
||||
//! convention, adapted): an in-memory `IdentityProvider`
|
||||
//! ([`MemIdentityProvider`]), the in-memory vault seam ([`TestVault`]),
|
||||
@@ -6,8 +6,8 @@
|
||||
//! ed25519 test-key fixtures ([`FIXTURE_KEYS`]).
|
||||
//!
|
||||
//! **Location decision (recorded, the OQ-03 freeze-inventory
|
||||
//! discipline):** the doubles live *only* here — a shared module under
|
||||
//! `tests/` reached with `mod common;` — and never in the public API:
|
||||
//! discipline):** the doubles live *only* here (a shared module under
|
||||
//! `tests/` reached with `mod common;`) and never in the public API:
|
||||
//! no re-export from `lib.rs`, no feature gate, nothing doubles-related
|
||||
//! reachable from the crate root. The doubles' shapes are test
|
||||
//! surface, test-versioned freely; the only `src/`-side consumers are
|
||||
@@ -15,7 +15,7 @@
|
||||
//! `include_str!`, never a public export.
|
||||
//!
|
||||
//! **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
|
||||
//! `unlock_new` arms in-memory. The double is therefore a *wrapper*
|
||||
//! ([`TestVault`]) around that construction, not a local re-impl
|
||||
@@ -26,7 +26,7 @@
|
||||
//!
|
||||
//! **Fixture decision (recorded):** committed static OpenSSH-PEM test
|
||||
//! 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
|
||||
//! fixtures, not secrets: minted for this suite only, never registered
|
||||
//! anywhere real, never deployed. Each key's fingerprint is recorded
|
||||
@@ -36,43 +36,43 @@
|
||||
//! asserts parity against them.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! The row/stamp discipline (adapted from the sibling crates): test
|
||||
//! functions are *rows* — the name states the property, not the
|
||||
//! caller — and each carries a doc-comment list item beginning
|
||||
//! functions are *rows* (the name states the property, not the
|
||||
//! caller), and each carries a doc-comment list item beginning
|
||||
//! **`Contract stamp:`** citing the pinning spec §/ADR § (never a bare
|
||||
//! wave or date). The marker is greppable:
|
||||
//! `grep -rn "Contract stamp:" tests/` is the inventory.
|
||||
|
||||
#![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:
|
||||
/// `tests/common/harness.rs`).
|
||||
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
|
||||
/// own placement/shape decisions live in the module docs:
|
||||
/// `tests/common/sshd_double.rs`).
|
||||
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
|
||||
/// noop shapes; the module's own doc records the decisions:
|
||||
/// `tests/common/git_doubles.rs`).
|
||||
#[cfg(feature = "git")]
|
||||
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
|
||||
/// doc records the decisions: `tests/common/tty_doubles.rs`).
|
||||
#[cfg(feature = "session")]
|
||||
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
|
||||
/// through it (the module's own doc records the convention:
|
||||
/// `tests/common/event_capture.rs`).
|
||||
@@ -92,7 +92,8 @@ use alkssh::{HostKeyPolicy, HostKeySource, KeySourceError, PresentedHostKey};
|
||||
/// (`tests/vectors/<name>.pub`), and the fingerprint recorded
|
||||
/// 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.
|
||||
#[derive(Debug)]
|
||||
pub struct FixtureKey {
|
||||
@@ -101,10 +102,10 @@ pub struct FixtureKey {
|
||||
/// The OpenSSH private-key PEM, included verbatim from the
|
||||
/// committed fixture file.
|
||||
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.
|
||||
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
|
||||
/// parity against (ADR-004 decision 1's contract invariant).
|
||||
pub fingerprint: &'static str,
|
||||
@@ -116,7 +117,7 @@ impl FixtureKey {
|
||||
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
|
||||
/// exactly the at-rest contract: unencrypted OpenSSH PEM).
|
||||
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.
|
||||
pub fn fixture(name: &str) -> &'static FixtureKey {
|
||||
FIXTURE_KEYS
|
||||
@@ -165,7 +166,7 @@ pub fn fixture(name: &str) -> &'static FixtureKey {
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn identity(id: &str, scopes: &[&str]) -> 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.
|
||||
///
|
||||
/// The double the wave-2 auth bridge and dispatch tests resolve
|
||||
@@ -187,7 +188,7 @@ pub struct MemIdentityProvider {
|
||||
}
|
||||
|
||||
impl MemIdentityProvider {
|
||||
/// An empty registry — nothing resolves until registered
|
||||
/// An empty registry; nothing resolves until registered
|
||||
/// (fail-closed: unregistered fingerprints decline).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -202,12 +203,12 @@ impl MemIdentityProvider {
|
||||
.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) {
|
||||
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> {
|
||||
let mut keys = self
|
||||
.identities
|
||||
@@ -227,37 +228,37 @@ impl Default 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.
|
||||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
|
||||
self.identities.read().get(fingerprint).cloned()
|
||||
}
|
||||
|
||||
/// The token path resolves nothing in the SSH direction —
|
||||
/// ADR-004's dispositions are fingerprint-only on this surface
|
||||
/// The token path resolves nothing in the SSH direction,
|
||||
/// since ADR-004's dispositions are fingerprint-only on this surface
|
||||
/// (`auth-bridge.md`); the double never invents a token shape.
|
||||
fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
|
||||
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
|
||||
/// construction via `unlock_new`).
|
||||
///
|
||||
/// The locked/absent/tampered/mismatch failure postures (POC-2 §3) are
|
||||
/// 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
|
||||
/// 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)]
|
||||
pub struct TestVault {
|
||||
handle: VaultServiceHandle,
|
||||
}
|
||||
|
||||
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 {
|
||||
let handle = VaultServiceHandle::new();
|
||||
handle
|
||||
@@ -266,13 +267,13 @@ impl TestVault {
|
||||
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).
|
||||
pub fn handle(&self) -> VaultServiceHandle {
|
||||
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
|
||||
/// `EncryptedData` record).
|
||||
pub fn seal(&self, key: &FixtureKey) -> EncryptedData {
|
||||
@@ -287,7 +288,7 @@ impl TestVault {
|
||||
.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).
|
||||
pub fn lock(&self) {
|
||||
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
|
||||
/// `SshAdapter`, whose construction wants a non-empty host-key set, and
|
||||
/// 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`]).
|
||||
///
|
||||
/// `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
|
||||
/// that accidentally loads an absent double fails loudly.
|
||||
pub struct MemHostKeySource {
|
||||
@@ -319,7 +320,7 @@ impl MemHostKeySource {
|
||||
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).
|
||||
pub fn from_fixtures(keys: &[&'static FixtureKey]) -> 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
|
||||
/// case-1/case-2 rows of every host-key contract test).
|
||||
///
|
||||
/// Verdicts: a rules list of exact `(host, fingerprint)` matches —
|
||||
/// last matching rule wins — falling back to a configurable
|
||||
/// Verdicts: a rules list of exact `(host, fingerprint)` matches;
|
||||
/// the last matching rule wins, falling back to a configurable
|
||||
/// default verdict for unlisted pairs. The default of the
|
||||
/// **default-constructed double is reject** (the crate's fail-closed
|
||||
/// posture, `OQ-05`); permissive verdicts are the test's explicit
|
||||
@@ -363,7 +364,7 @@ struct Rule {
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Decision {
|
||||
@@ -380,7 +381,7 @@ impl MemHostKeyPolicy {
|
||||
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
|
||||
/// explicit choice, never a default.
|
||||
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).
|
||||
pub fn pin(&self, host: &str, fingerprint: &str) {
|
||||
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> {
|
||||
self.log.lock().clone()
|
||||
}
|
||||
@@ -440,7 +441,7 @@ impl HostKeyPolicy for MemHostKeyPolicy {
|
||||
|
||||
/// The leg-2 rows' tracing visibility (`w3-tunnel-listen`): a minimal
|
||||
/// 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`
|
||||
/// dependency; events fire from spawned tasks on both sides of the
|
||||
/// 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
|
||||
//! in for the vanilla `sshd` every wave-4 dial row is pointed at (the
|
||||
//! reverse-runner shape). The alkssh loopback harness
|
||||
//! ([`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
|
||||
//! loopback TCP listener: a dial target is an address the dial
|
||||
//! surface resolves and connects to, which a duplex pair has no
|
||||
@@ -11,7 +11,7 @@
|
||||
//!
|
||||
//! **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
|
||||
//! freely, no public API export, no feature gate; the client
|
||||
//! 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
|
||||
//! same double).
|
||||
//! - **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
|
||||
//! vanilla-parity stand-in for a real `sshd`'s
|
||||
//! `authorized_keys`). Every other auth hook is an explicit reject,
|
||||
//! never an inherited default (the ADR-004 decision 3 hygiene).
|
||||
//! - **Host keys**: the fixture host-key pair rides 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.
|
||||
//! - **The session handle**: the double's `auth_succeeded` hook hands
|
||||
//! 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 →
|
||||
//! client `channel_open_*`; the dialed client's skeleton declines;
|
||||
//! 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 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 →
|
||||
//! client `channel_open_*`; the dialed client's skeleton declines;
|
||||
//! 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 stays-usable-after-declines row here; the op-row vehicles
|
||||
//! later) arm the double to accept client session opens and hand
|
||||
//! the channel out ([`SshdDouble::next_opened_session`]).
|
||||
//! - **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
|
||||
//! 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
|
||||
//! changes/signals the client rows assert the wire verbatim
|
||||
//! against). The double stays vanilla-parity: a hand-rolled
|
||||
//! responder standing in for `sshd`, nothing alkssh-typed inside.
|
||||
//!
|
||||
//! - **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
|
||||
//! (scripted accept/deny; an accept **binds a real socket** in
|
||||
//! 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
|
||||
//! connection's peer as the originator, then the bytes pumped
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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::{Channel, ChannelOpenFailure, MethodKind, MethodSet};
|
||||
|
||||
/// The double's scripted exit report — what a launched program's
|
||||
/// death would produce (`exit-status` / `exit-signal`).
|
||||
/// The double's scripted exit report (what a launched program's
|
||||
/// death would produce: `exit-status` / `exit-signal`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DblExit {
|
||||
/// `exit-status(N)`.
|
||||
Status(u32),
|
||||
/// `exit-signal(name)` — the wire's signal-name class.
|
||||
/// `exit-signal(name)`, the wire's signal-name class.
|
||||
Signal(&'static str),
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ pub enum DblExit {
|
||||
/// (`direct-tcpip` / `direct-streamlocal`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
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).
|
||||
Reject,
|
||||
/// 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
|
||||
/// listen requests (`tcpip-forward` / `streamlocal-forward@…) — the
|
||||
/// listen requests (`tcpip-forward` / `streamlocal-forward@…`), the
|
||||
/// `-R` rows' scripted class (`w4-reverse-listen`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
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
|
||||
/// op row mirrors). The script's default (fail-closed double).
|
||||
#[default]
|
||||
Deny,
|
||||
/// 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,
|
||||
/// and spawns the accept loop (per accepted connection: a
|
||||
/// server-initiated `forwarded-*` open toward the client echoing
|
||||
/// the listen's own args + the connection's peer, then the bytes
|
||||
/// pumped bidirectionally; a client-declined open closes that
|
||||
/// accepted connection — vanilla parity).
|
||||
/// accepted connection, vanilla parity).
|
||||
Accept,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SshdScript {
|
||||
@@ -142,7 +142,7 @@ pub struct SshdScript {
|
||||
pub stdout: &'static [u8],
|
||||
/// The channel data written as `extended_data(1)` stderr.
|
||||
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).
|
||||
pub exit: Option<DblExit>,
|
||||
/// The `direct-tcpip` opens' behavior.
|
||||
@@ -154,7 +154,7 @@ pub struct SshdScript {
|
||||
/// The `streamlocal-forward` listen form's behavior (the unix
|
||||
/// `-R` rows).
|
||||
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
|
||||
/// `accept_session_channels`): the stays-usable-after-a-decline
|
||||
/// 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
|
||||
/// 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.
|
||||
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
|
||||
/// rows put on the wire).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -201,11 +201,11 @@ pub struct SshdObserved {
|
||||
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).
|
||||
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).
|
||||
pub type WindowRecord = (u32, u32, u32, u32);
|
||||
|
||||
@@ -234,13 +234,13 @@ impl SshdObserved {
|
||||
self.windows.lock().clone()
|
||||
}
|
||||
|
||||
/// The signals seen (russh's `Sig` debug names — "TERM",
|
||||
/// The signals seen (russh's `Sig` debug names, "TERM",
|
||||
/// `Custom("..")`, …).
|
||||
pub fn signals(&self) -> Vec<String> {
|
||||
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).
|
||||
pub fn listens(&self) -> Vec<ListenRecord> {
|
||||
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
|
||||
/// report, then the close (the vanilla sshd's exit-then-close
|
||||
/// discipline).
|
||||
@@ -292,7 +292,7 @@ async fn scripted_program(handle: Handle, channel_id: russh::ChannelId, script:
|
||||
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.
|
||||
async fn direct_echo(mut stream: russh::ChannelStream<Msg>) {
|
||||
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):
|
||||
/// 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,
|
||||
/// 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
|
||||
/// socket. A client-declined open closes that accepted connection
|
||||
/// (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
|
||||
/// 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) {
|
||||
let mut stream = channel.into_stream();
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// handle carrier and the session-open acceptor stub.
|
||||
struct SshdDoubleHandler {
|
||||
@@ -397,7 +397,7 @@ struct SshdDoubleHandler {
|
||||
remaining_declines: std::sync::Arc<std::sync::atomic::AtomicUsize>,
|
||||
/// The `-R` bookkeeping (per-connection, sshd's listener table):
|
||||
/// 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
|
||||
/// real listener with it).
|
||||
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
|
||||
/// attempt — POC-1 §4); the real disposition is
|
||||
/// attempt, POC-1 §4); the real disposition is
|
||||
/// [`Self::auth_publickey`]'s.
|
||||
async fn auth_publickey_offered(
|
||||
&mut self,
|
||||
@@ -513,7 +513,7 @@ impl russh::server::Handler for SshdDoubleHandler {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
async fn channel_open_direct_tcpip(
|
||||
&mut self,
|
||||
@@ -633,7 +633,7 @@ impl russh::server::Handler for SshdDoubleHandler {
|
||||
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`
|
||||
/// request's mint is the socket's real bound port, the out-param
|
||||
/// 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
|
||||
/// 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).
|
||||
async fn cancel_tcpip_forward(
|
||||
&mut self,
|
||||
@@ -700,7 +700,7 @@ impl russh::server::Handler for SshdDoubleHandler {
|
||||
/// scripts, binds the real unix socket (stale file unlinked
|
||||
/// first), and spawns the unix accept loop. Off-Unix the hook is
|
||||
/// 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.
|
||||
#[cfg(unix)]
|
||||
async fn streamlocal_forward(
|
||||
@@ -729,7 +729,7 @@ impl russh::server::Handler for SshdDoubleHandler {
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
async fn cancel_streamlocal_forward(
|
||||
&mut self,
|
||||
@@ -753,8 +753,8 @@ impl russh::server::Handler for SshdDoubleHandler {
|
||||
|
||||
impl SshdDoubleHandler {
|
||||
/// The scripted request reply: success launches the scripted
|
||||
/// program through the session's handle (spawned — the packet
|
||||
/// loop returns fast) *when a program is scripted at all* — an
|
||||
/// program through the session's handle (spawned, so the packet
|
||||
/// loop returns fast) *when a program is scripted at all*; an
|
||||
/// output-less/no-exit script launches nothing (the interactive
|
||||
/// sessions stay open; close is the client's or the connection's);
|
||||
/// 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).
|
||||
pub struct SshdAccepts {
|
||||
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 {
|
||||
/// The dial target, `<ip>:<port>` — the row's
|
||||
/// The dial target, `<ip>:<port>`, the row's
|
||||
/// `DialConfig.address` value.
|
||||
pub address: String,
|
||||
session_carrier: Option<tokio::sync::oneshot::Receiver<Handle>>,
|
||||
@@ -806,7 +806,7 @@ pub struct SshdDouble {
|
||||
/// `host_keys` rides the `russh::server::Config`;
|
||||
/// `accept_session_channels` arms the session-open acceptor stub.
|
||||
/// 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`]).
|
||||
pub async fn spawn_scripted(
|
||||
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'
|
||||
/// 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.
|
||||
pub async fn spawn(
|
||||
host_keys: Vec<PrivateKey>,
|
||||
@@ -896,7 +896,7 @@ pub async fn spawn(
|
||||
}
|
||||
|
||||
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`
|
||||
/// carrier; the dial has authenticated by the time a row calls
|
||||
/// 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
|
||||
/// accepted — `None` after the session's end (the observation
|
||||
/// accepted; `None` after the session's end (the observation
|
||||
/// shape's honest empty).
|
||||
pub async fn next_opened_session(&mut self) -> Option<Channel<Msg>> {
|
||||
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
|
||||
/// sessions' `Ok`; an abrupt peer close is an `Err` wire class).
|
||||
/// `None` after the double's loop ends.
|
||||
|
||||
+22
-22
@@ -1,17 +1,17 @@
|
||||
//! 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
|
||||
//! hand-rolls the shim per the `TtyHandle` field shapes: a backend
|
||||
//! that records the `TtyParams` it was allocated, consumes stdin
|
||||
//! (recording bytes, the EOF shutdown and the abort kill), emits
|
||||
//! scripted stdout/stderr chunks and a scripted exit code, and
|
||||
//! 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
|
||||
//! identity ride's live witness (the task's "identity-landing item").
|
||||
//!
|
||||
//! 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).
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -28,7 +28,7 @@ use bytes::Bytes;
|
||||
use tokio::io::AsyncWrite;
|
||||
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/
|
||||
/// signals, the abort kill witness).
|
||||
#[derive(Debug, Default)]
|
||||
@@ -38,20 +38,20 @@ pub struct TtyShimObservation {
|
||||
/// Bytes written to the shim's stdin, concatenated per allocation.
|
||||
pub stdin: parking_lot::Mutex<Vec<u8>>,
|
||||
/// Whether a stdin shutdown (the EOF translation's destination)
|
||||
/// was observed — one flag per shutdown, but one allocation per
|
||||
/// row in practice.
|
||||
/// was observed (one flag per shutdown, but one allocation per
|
||||
/// row in practice).
|
||||
pub stdin_eof: parking_lot::Mutex<Vec<bool>>,
|
||||
/// Resizes from the control handle (`cols`, `rows`).
|
||||
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>>,
|
||||
/// 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.
|
||||
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.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ShimScript {
|
||||
@@ -66,7 +66,7 @@ pub struct ShimScript {
|
||||
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
|
||||
/// observation + script are `Arc`s).
|
||||
#[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)
|
||||
/// always evaluates.
|
||||
fn resource_id(&self, _params: &TtyParams) -> Option<(&'static str, String)> {
|
||||
@@ -143,7 +143,7 @@ impl TtyBackend for TtyShim {
|
||||
|
||||
impl ShimScript {
|
||||
/// 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;
|
||||
/// the shim sees the converted `TtyParams`).
|
||||
fn terminal_is_pty(&self, params: &TtyParams) -> bool {
|
||||
@@ -151,9 +151,9 @@ impl ShimScript {
|
||||
}
|
||||
}
|
||||
|
||||
/// A scripted stream — the chunks in order, then end. Instantly
|
||||
/// drained (the alktty pumps consume; the exit chunk arrives after —
|
||||
/// the adapter's exit-chunk-is-last invariant orders everything).
|
||||
/// A scripted stream: the chunks in order, then end. Instantly
|
||||
/// drained (the alktty pumps consume; the exit chunk arrives after,
|
||||
/// 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>> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(chunks.len().max(1) + 1);
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// the backend).
|
||||
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 {
|
||||
observation: Arc<TtyShimObservation>,
|
||||
}
|
||||
@@ -255,7 +255,7 @@ impl TtyControl for ShimControl {
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn tty_seam(shim: &Arc<TtyShim>) -> alkssh::SessionSeam {
|
||||
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::
|
||||
/// OwnershipProvider`]): records every identity evaluation and rules
|
||||
/// 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).
|
||||
#[derive(Debug)]
|
||||
pub struct MemOwnership {
|
||||
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.
|
||||
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.
|
||||
pub fn tty_seam_with_ownership(
|
||||
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.
|
||||
pub fn misconfigured_seam() -> alkssh::SessionSeam {
|
||||
alkssh::SessionSeam {
|
||||
|
||||
+43
-43
@@ -1,5 +1,5 @@
|
||||
//! 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
|
||||
//! through the loopback harness. This task tests the rows it lands;
|
||||
//! the full per-build matrix suite is wave-5's.
|
||||
@@ -32,7 +32,7 @@ use russh::{Channel, ChannelOpenFailure, Error};
|
||||
|
||||
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
|
||||
/// `MemIdentityProvider`, host keys `bravo`, the permissive host-key
|
||||
/// posture (the explicit test choice).
|
||||
@@ -58,7 +58,7 @@ async fn authenticated() -> LoopbackSession {
|
||||
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.
|
||||
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
assert!(
|
||||
@@ -71,7 +71,7 @@ fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
async fn expect_failure(channel: &mut Channel<Msg>, context: &str) {
|
||||
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
|
||||
/// 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.
|
||||
async fn expect_failure_then_close(channel: &mut Channel<Msg>, context: &str) {
|
||||
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.
|
||||
async fn expect_channel_alive(channel: &mut Channel<Msg>) {
|
||||
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).
|
||||
#[tokio::test]
|
||||
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`
|
||||
/// listen flow is a `tunnel` row declined when the flow cannot
|
||||
/// serve — this wave: every build).
|
||||
/// serve; this wave: every build).
|
||||
#[tokio::test]
|
||||
async fn tcpip_forward_listen_requests_decline_in_every_build() {
|
||||
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
|
||||
/// `streamlocal-forward` unix listen form is the `tunnel` row's
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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
|
||||
/// listener map, a `tunnel` build's empty map misses the pair —
|
||||
/// the same refused-cancel wire shape both ways).
|
||||
/// listener map, a `tunnel` build's empty map misses the pair; the
|
||||
/// same refused-cancel wire shape both ways).
|
||||
#[tokio::test]
|
||||
async fn global_request_cancels_refuse_unknown_pairs_in_every_build() {
|
||||
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
|
||||
// §Decline matrix — "none (default): declines everything").
|
||||
// §Decline matrix, "none (default): declines everything").
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[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};
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (none-build
|
||||
/// row: every channel type declines at open — session channels
|
||||
/// as unfeatured, tunnel rows as unfeatured — plus the
|
||||
/// row: every channel type declines at open (session channels
|
||||
/// as unfeatured, tunnel rows as unfeatured) plus the
|
||||
/// never-feature x11).
|
||||
#[tokio::test]
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// `tests/git_mapping.rs`.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -271,7 +271,7 @@ mod git_build {
|
||||
};
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// git-parseable exec routes to the git row; this build
|
||||
/// configures no seam, so the row declines at its config gate —
|
||||
/// the serving-path shape, failure then close). The row's
|
||||
/// configures no seam, so the row declines at its config gate,
|
||||
/// the serving-path shape: failure then close). The row's
|
||||
/// serving surface (seam configured, real dispatch) is
|
||||
/// `tests/git_mapping.rs`'s.
|
||||
#[tokio::test]
|
||||
@@ -312,7 +312,7 @@ mod git_build {
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row:
|
||||
/// 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]
|
||||
async fn plain_exec_declines_and_keeps_the_channel_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
@@ -328,7 +328,7 @@ mod git_build {
|
||||
}
|
||||
|
||||
/// - *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;
|
||||
/// the reply vocabulary is shared with parse-then-fail: failure,
|
||||
/// channel alive).
|
||||
@@ -353,7 +353,7 @@ mod git_build {
|
||||
}
|
||||
|
||||
/// - *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)
|
||||
/// exactly as the shell row does).
|
||||
#[tokio::test]
|
||||
@@ -383,7 +383,7 @@ mod git_build {
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (subsystem:
|
||||
/// `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]
|
||||
async fn subsystems_decline_with_their_named_reasons_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
@@ -405,7 +405,7 @@ mod git_build {
|
||||
}
|
||||
|
||||
/// - *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]
|
||||
async fn env_requests_decline_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
@@ -421,7 +421,7 @@ mod 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]
|
||||
async fn tunnel_opens_decline_unfeatured_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
@@ -453,7 +453,7 @@ mod git_build {
|
||||
// its gate); with no seam/config this build's session rows decline at
|
||||
// their config gates (the unset-declines contract, serving-path
|
||||
// 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")))]
|
||||
@@ -476,7 +476,7 @@ mod session_build {
|
||||
}
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// (`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
|
||||
/// configured) is `tests/tty_mapping.rs`'s.
|
||||
#[tokio::test]
|
||||
@@ -581,7 +581,7 @@ mod session_build {
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (session row:
|
||||
/// other channel types decline — tunnel unfeatured).
|
||||
/// other channel types decline, tunnel unfeatured).
|
||||
#[tokio::test]
|
||||
async fn tunnel_opens_decline_unfeatured_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
@@ -612,8 +612,8 @@ mod session_build {
|
||||
// tunnel-only build: session channels decline (auto-reject); 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
|
||||
// unset-handle class — the unset-declines posture; the fabric double's
|
||||
// serve/cap/fabric-failure rows ride tests/tunnel_mapping.rs) —
|
||||
// unset-handle class, the unset-declines posture; the fabric double's
|
||||
// serve/cap/fabric-failure rows ride tests/tunnel_mapping.rs) per
|
||||
// mapping.md §Decline matrix (tunnel row).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -624,7 +624,7 @@ mod tunnel_build {
|
||||
|
||||
/// - *Contract stamp:* ADR-006 point 3 as amended (parse-only
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
async fn session_channels_decline_in_a_tunnel_build() {
|
||||
@@ -641,9 +641,9 @@ mod tunnel_build {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// fabric handle** even with `tunnel` on —
|
||||
/// fabric handle** even with `tunnel` on:
|
||||
/// [`connect failed`], the cannot-bridge class ADR-006 decision
|
||||
/// 1 pins (`w3-tunnel-direct`'s recorded wire classes: policy
|
||||
/// 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 —
|
||||
// the git row is still its config-driven decline in this binary (the
|
||||
// All features: the routing decisions hold with the full graph on; 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
|
||||
// still their config-driven declines (the serving rows ride
|
||||
// tests/tty_mapping.rs).
|
||||
@@ -706,7 +706,7 @@ mod all_features {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// `tests/tty_mapping.rs`.
|
||||
#[tokio::test]
|
||||
@@ -723,7 +723,7 @@ mod all_features {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// `tests/tty_mapping.rs`).
|
||||
#[tokio::test]
|
||||
@@ -751,7 +751,7 @@ mod all_features {
|
||||
|
||||
/// - *Contract stamp:* the unset-declines posture (`w3-seam-config`,
|
||||
/// 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
|
||||
/// on (`ConnectFailed`, the cannot-bridge class); the served
|
||||
/// surface rides tests/tunnel_mapping.rs's fabric double.
|
||||
@@ -781,8 +781,8 @@ mod all_features {
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (the
|
||||
/// capability declines — sftp placeholder, git subsystem
|
||||
/// names, env — hold with everything on).
|
||||
/// capability declines (sftp placeholder, git subsystem
|
||||
/// names, env) hold with everything on).
|
||||
#[tokio::test]
|
||||
async fn capability_declines_hold_with_all_features() {
|
||||
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
|
||||
//! against them: fingerprint registration resolution, the vault
|
||||
//! 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 →
|
||||
/// `resolve_from_fingerprint` → `Some` = accept; `None` = reject).
|
||||
/// - *Contract stamp:* ADR-005 context (client fingerprints never
|
||||
/// involve the vault — the `IdentityProvider` path only).
|
||||
/// involve the vault (the `IdentityProvider` path only).
|
||||
#[test]
|
||||
fn mem_identity_provider_registered_fingerprint_resolves() {
|
||||
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.
|
||||
///
|
||||
/// - *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());
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// - *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());
|
||||
}
|
||||
|
||||
/// Every fixture key's fingerprint — whether derived via the private
|
||||
/// PEM or the committed public blob — equals its `ssh-keygen -lf`
|
||||
/// Every fixture key's fingerprint, whether derived via the private
|
||||
/// PEM or the committed public blob, equals its `ssh-keygen -lf`
|
||||
/// recorded value. This row keeps the fixture set honest for every
|
||||
/// 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 →
|
||||
/// `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.
|
||||
///
|
||||
/// - *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:?}");
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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 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.
|
||||
///
|
||||
/// - *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).
|
||||
/// - *Contract stamp:* ADR-005 decision 6 (the host *pin* is public
|
||||
/// 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
|
||||
/// 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.
|
||||
///
|
||||
/// - *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
|
||||
/// assembly's to write).
|
||||
/// - *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`**
|
||||
//! (`docs/plans/implementation.md` §Wave 5, the real-tool e2e bullet's
|
||||
//! 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
|
||||
//! direct-tcpip dial (`-L`-shaped), the `-R` leg (the port-0 mint
|
||||
//! against a real server's actually-bound reply, the server-initiated
|
||||
//! `forwarded-tcpip` open served through the acceptor registry — the
|
||||
//! reverse-runner consumer pattern end to end), the cancel form
|
||||
//! `forwarded-tcpip` open served through the acceptor registry (the
|
||||
//! reverse-runner consumer pattern end to end), the cancel form the cancel form
|
||||
//! tearing the real listen down, and the decline rows (the sftp
|
||||
//! client-side placeholder decline, the server-refused direct-tcpip
|
||||
//! 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).
|
||||
//!
|
||||
//! **Placement decision (recorded, the task's acceptance criterion
|
||||
//! 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
|
||||
//! single-writer posture for `w5-decline-matrix`; this binary owns
|
||||
//! only its own rows and its in-file `support` fixture module,
|
||||
//! disjoint from `tests/e2e_server.rs` (the matrix task's binaries).
|
||||
//!
|
||||
//! **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,
|
||||
//! so a shared per-binary instance buys nothing: per-row isolation
|
||||
//! (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,
|
||||
//! `ssh-keygen` absent, spawn refused, privilege-separation directory
|
||||
//! 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
|
||||
//! double (`VaultKeySource` over `TestVault`, fixture `alpha` sealed),
|
||||
//! and the case-1 pin posture (`MemHostKeyPolicy::pin`) accepts only
|
||||
@@ -48,7 +48,7 @@
|
||||
//! Rows are `#[cfg(unix)]` (the fixture spawns processes) and
|
||||
//! feature-gated per row group under the established check-gated
|
||||
//! **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
|
||||
//! against `client.md` / `reverse-runner.md` (the greppable
|
||||
//! `tests/common` discipline).
|
||||
@@ -69,12 +69,12 @@ mod support {
|
||||
|
||||
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).
|
||||
pub const ROW_DEADLINE: Duration = Duration::from_secs(30);
|
||||
|
||||
/// The sshd candidates probed in order (`sbin` paths first — the
|
||||
/// server binary rarely sits on the user's `PATH`).
|
||||
/// The sshd candidates probed in order (`sbin` paths first, since
|
||||
/// the server binary rarely sits on the user's `PATH`).
|
||||
const SSHD_CANDIDATES: &[&str] = &[
|
||||
"/usr/sbin/sshd",
|
||||
"/usr/local/sbin/sshd",
|
||||
@@ -84,10 +84,10 @@ mod support {
|
||||
|
||||
/// Prints the row's reasoned skip (never silently green).
|
||||
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).
|
||||
pub fn sshd_binary() -> Option<PathBuf> {
|
||||
SSHD_CANDIDATES
|
||||
@@ -102,7 +102,7 @@ mod support {
|
||||
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
|
||||
/// workdirs, no cross-row races).
|
||||
fn workdir(row: &str) -> PathBuf {
|
||||
@@ -113,7 +113,7 @@ mod support {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// fingerprint parity contract pins (the policy's pin comparison
|
||||
/// is exact string equality by contract).
|
||||
@@ -127,7 +127,7 @@ mod support {
|
||||
.expect("ssh-keygen spawns");
|
||||
assert!(
|
||||
minted.status.success(),
|
||||
"the host key mints — stderr: {}",
|
||||
"the host key mints; stderr: {}",
|
||||
String::from_utf8_lossy(&minted.stderr)
|
||||
);
|
||||
let listed = Command::new("ssh-keygen")
|
||||
@@ -145,7 +145,7 @@ mod support {
|
||||
/// The in-process port allocator for the rows' named binds (the
|
||||
/// e2e-server posture verbatim: a monotonic counter over a window
|
||||
/// 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 {
|
||||
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(2000);
|
||||
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
|
||||
/// decrypt-at-dial path over the in-memory vault double; the
|
||||
/// case-1 pinned policy accepts exactly the minted host key.
|
||||
@@ -175,7 +175,7 @@ mod support {
|
||||
|
||||
impl SshdFixture {
|
||||
/// 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> {
|
||||
Self::spawn_with(row, true).await
|
||||
}
|
||||
@@ -301,7 +301,7 @@ mod support {
|
||||
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
|
||||
/// consumed against a real presented key). A `Some` pin
|
||||
/// substitutes another fixture's fingerprint (the mismatch
|
||||
@@ -316,7 +316,7 @@ mod support {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// the dial), deadline-bounded.
|
||||
pub async fn dial_from(
|
||||
@@ -339,7 +339,7 @@ mod support {
|
||||
.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`.
|
||||
pub async fn dial_default(&self) -> SshConnection {
|
||||
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).
|
||||
fn log_tail(log: &Path) -> String {
|
||||
let Ok(mut file) = std::fs::File::open(log) else {
|
||||
@@ -375,7 +375,7 @@ mod support {
|
||||
.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
|
||||
/// and this server, exactly the `-L` topology).
|
||||
pub async fn spawn_echo_server() -> u16 {
|
||||
@@ -421,7 +421,7 @@ mod support {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// server's reaping reports the program's real exit code (a nonzero
|
||||
/// exit proves the parity is the server's, not a canned 0).
|
||||
@@ -434,7 +434,7 @@ mod exec_row {
|
||||
|
||||
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
|
||||
/// deadline bounds a wrong row instead of hanging it.
|
||||
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
|
||||
/// 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
|
||||
/// (`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)`,
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
async fn real_sshd_exec_pumps_streams_and_reports_the_exit_status() {
|
||||
@@ -503,7 +503,7 @@ mod exec_row {
|
||||
assert_eq!(
|
||||
exit,
|
||||
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!(
|
||||
!connection.is_closed(),
|
||||
@@ -514,7 +514,7 @@ mod exec_row {
|
||||
assert_eq!(
|
||||
decisions.len(),
|
||||
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];
|
||||
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
|
||||
/// 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.
|
||||
#[cfg(all(unix, feature = "tunnel"))]
|
||||
mod direct_tcpip_row {
|
||||
use super::support;
|
||||
|
||||
/// - *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
|
||||
/// channel on the real wire, the plain byte duplex pumps, and
|
||||
/// a **live** TCP listener on the far side answers
|
||||
@@ -592,7 +592,7 @@ mod reverse_rows {
|
||||
|
||||
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).
|
||||
#[derive(Clone)]
|
||||
struct AcceptorDouble {
|
||||
@@ -627,7 +627,7 @@ mod reverse_rows {
|
||||
|
||||
/// - *Contract stamp:* `client.md` §op rows (row 4, the `-R`
|
||||
/// 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**
|
||||
/// port (a real server's reply, verified by dialing it), the
|
||||
/// server-initiated `forwarded-tcpip` open arrives into the
|
||||
@@ -683,7 +683,7 @@ mod reverse_rows {
|
||||
)
|
||||
.await
|
||||
.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) =
|
||||
tokio::time::timeout(Duration::from_secs(15), opens_rx.recv())
|
||||
@@ -714,7 +714,7 @@ mod reverse_rows {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// actually bound (dialable), the listener handle's `cancel()`
|
||||
/// tears the **real** listen down (a later dial is refused), a
|
||||
@@ -776,7 +776,7 @@ mod reverse_rows {
|
||||
}
|
||||
assert!(
|
||||
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
|
||||
@@ -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
|
||||
/// and pin typed declines, feature-free.
|
||||
#[cfg(unix)]
|
||||
@@ -808,7 +808,7 @@ mod declines {
|
||||
use super::common::fixture as key_fixture;
|
||||
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
|
||||
/// real authenticated connection: typed decline before any
|
||||
/// wire op (the real server observes no subsystem request),
|
||||
@@ -831,19 +831,19 @@ mod declines {
|
||||
assert!(matches!(err, OpError::SftpNotServed), "got {err:?}");
|
||||
assert!(
|
||||
!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;
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// mismatch hard-declines the dial (`DialError::
|
||||
/// HostKeyRejected`, no connection), the policy's decision log
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
async fn real_presented_key_mismatch_hard_declines_and_the_right_pin_dials() {
|
||||
@@ -869,7 +869,7 @@ mod declines {
|
||||
assert_eq!(
|
||||
decisions.len(),
|
||||
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];
|
||||
assert!(
|
||||
@@ -894,7 +894,7 @@ mod declines {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// connection stays usable (a following exec is served). The session
|
||||
/// 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
|
||||
/// 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:
|
||||
/// `tunnel_direct` against `AllowTcpForwarding no` →
|
||||
/// `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.
|
||||
#[tokio::test]
|
||||
async fn real_server_refused_direct_tcpip_declines_and_the_connection_stays_usable() {
|
||||
@@ -944,14 +944,14 @@ mod unfeatured_direct_decline {
|
||||
);
|
||||
assert!(
|
||||
!connection.is_closed(),
|
||||
"the decline is channel-scoped — the connection survives it"
|
||||
"the decline is channel-scoped; the connection survives it"
|
||||
);
|
||||
|
||||
let mut session =
|
||||
tokio::time::timeout(support::ROW_DEADLINE, connection.exec("echo still-usable"))
|
||||
.await
|
||||
.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 reader = alktty::wire::ChunkReader::new(&mut session.output);
|
||||
@@ -969,7 +969,7 @@ mod unfeatured_direct_decline {
|
||||
}
|
||||
assert_eq!(
|
||||
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;
|
||||
|
||||
+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
|
||||
//! (`ssh` / `git` / `curl` / `sftp`) against the adapter mounted on a
|
||||
//! real TCP listener over the established in-memory doubles (the
|
||||
@@ -11,13 +11,13 @@
|
||||
//!
|
||||
//! **Placement decision (recorded, the task's acceptance criterion
|
||||
//! 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
|
||||
//! only its own rows and its in-file fixture helpers (the trimmed
|
||||
//! leg-1/leg-2 fabric double for the tunnel rows, the support
|
||||
//! helpers). Rows arrive under the established check-gated
|
||||
//! **skipped-with-reason** posture (tool absent / version short —
|
||||
//! never silently green) and under a wall-clock deadline — a decline
|
||||
//! **skipped-with-reason** posture (tool absent / version short,
|
||||
//! never silently green) and under a wall-clock deadline (a decline
|
||||
//! must surface a clean diagnostic to the real client, never a hang
|
||||
//! (POC-1 §5 posture). Rows carry `Contract stamp:` doc markers
|
||||
//! against `mapping.md` / the POC-3 findings (the greppable
|
||||
@@ -40,12 +40,12 @@ mod support {
|
||||
|
||||
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).
|
||||
pub const ROW_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// 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).
|
||||
pub fn tool_present(binary: &str, probe: &[&str]) -> bool {
|
||||
Command::new(binary).args(probe).output().is_ok()
|
||||
@@ -53,11 +53,11 @@ mod support {
|
||||
|
||||
/// Prints the row's reasoned skip (never silently green).
|
||||
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
|
||||
/// 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 {
|
||||
let work =
|
||||
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
|
||||
/// 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
|
||||
/// host never turns an honest decline into a flake.
|
||||
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
|
||||
/// bind): a monotonic counter over a window **below** the kernel's
|
||||
/// 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
|
||||
/// tuples) and skips taken ports.
|
||||
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
|
||||
/// mapping over the hand-rolled recording quartet.
|
||||
#[cfg(unix)]
|
||||
@@ -268,7 +268,7 @@ mod git_rows {
|
||||
}
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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();
|
||||
assert!(
|
||||
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!(
|
||||
target.join(".git").is_dir(),
|
||||
@@ -317,7 +317,7 @@ mod git_rows {
|
||||
);
|
||||
assert!(
|
||||
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!(
|
||||
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
|
||||
/// 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
|
||||
/// clone-only coverage completed).
|
||||
#[tokio::test]
|
||||
@@ -362,7 +362,7 @@ mod git_rows {
|
||||
.await;
|
||||
assert!(
|
||||
init.status.success(),
|
||||
"the local repo initializes — stderr: {}",
|
||||
"the local repo initializes; stderr: {}",
|
||||
String::from_utf8_lossy(&init.stderr)
|
||||
);
|
||||
std::fs::write(repo.join("hello.txt"), "alkssh e2e push\n")
|
||||
@@ -383,7 +383,7 @@ mod git_rows {
|
||||
.await;
|
||||
assert!(
|
||||
commit.status.success(),
|
||||
"the commit lands — stderr: {}",
|
||||
"the commit lands; stderr: {}",
|
||||
String::from_utf8_lossy(&commit.stderr)
|
||||
);
|
||||
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();
|
||||
assert!(
|
||||
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();
|
||||
@@ -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)
|
||||
/// and the plain pipe-exec row with its separate stderr.
|
||||
#[cfg(unix)]
|
||||
@@ -457,7 +457,7 @@ mod session_rows {
|
||||
use super::common::harness::serve_config as harness_serve_config;
|
||||
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`
|
||||
/// feature rides the test graph only).
|
||||
fn local_seam() -> SessionSeam {
|
||||
@@ -536,17 +536,17 @@ mod session_rows {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"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!(
|
||||
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);
|
||||
}
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
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();
|
||||
assert!(
|
||||
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!(
|
||||
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!(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`
|
||||
/// (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).
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "tunnel")]
|
||||
@@ -630,7 +630,7 @@ mod tunnel_rows {
|
||||
use super::common::{fixture, identity};
|
||||
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
|
||||
/// recipe), TCP-only and happy-path only: the decline/collision
|
||||
/// machinery stays the mapping binaries'.
|
||||
@@ -775,7 +775,7 @@ mod tunnel_rows {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// dynamic-address producer), mints the correlation token per
|
||||
/// 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.
|
||||
async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) {
|
||||
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
|
||||
/// `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).
|
||||
#[tokio::test]
|
||||
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();
|
||||
assert!(
|
||||
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);
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// data leg; the `w3-tunnel-listen` flow re-landed against a
|
||||
/// real OpenSSH client).
|
||||
@@ -1224,13 +1224,13 @@ mod tunnel_rows {
|
||||
assert!(
|
||||
stdout.trim_end() == body.trim_end(),
|
||||
"vanilla ssh -R + curl: fabric arrival → forwarded-tcpip → client-dialed endpoint returns \
|
||||
the body — got {stdout:?}"
|
||||
the body; got {stdout:?}"
|
||||
);
|
||||
support::cleanup(&work);
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// corrected posture re-landed with real curl).
|
||||
#[tokio::test]
|
||||
@@ -1291,7 +1291,7 @@ mod tunnel_rows {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
assert!(
|
||||
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:?}"
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
/// (auth intact, no hang), with the door's decline event on record.
|
||||
#[cfg(unix)]
|
||||
@@ -1363,11 +1363,11 @@ mod unfeatured_session_decline {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
assert!(
|
||||
!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!(
|
||||
!stderr.trim().is_empty(),
|
||||
"the decline surfaces a clean diagnostic — stderr: {stderr:?}"
|
||||
"the decline surfaces a clean diagnostic; stderr: {stderr:?}"
|
||||
);
|
||||
if cfg!(feature = "git") {
|
||||
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 -L`'s curls fail per connection without hanging the
|
||||
/// 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:
|
||||
/// 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
|
||||
/// `ExitOnForwardFailure`). The -R row is gated here
|
||||
/// (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();
|
||||
assert!(
|
||||
!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!(
|
||||
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(&[
|
||||
("row", "tcpip_forward"),
|
||||
@@ -1496,7 +1496,7 @@ mod unfeatured_tunnel_declines {
|
||||
}
|
||||
|
||||
/// - *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,
|
||||
/// no hang).
|
||||
#[tokio::test]
|
||||
@@ -1572,12 +1572,12 @@ mod unfeatured_tunnel_declines {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Row placement note: the sftp slot gates nothing, and the row can
|
||||
/// 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).
|
||||
#[cfg(unix)]
|
||||
#[cfg(any(feature = "session", feature = "git"))]
|
||||
@@ -1647,11 +1647,11 @@ mod subsystem_decline {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
assert!(
|
||||
!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!(
|
||||
!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(&[
|
||||
("row", "subsystem_request"),
|
||||
@@ -1665,7 +1665,7 @@ mod subsystem_decline {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// (no existence oracle) and exit 128.
|
||||
#[cfg(unix)]
|
||||
@@ -1735,16 +1735,16 @@ mod unknown_repo_decline {
|
||||
assert_eq!(
|
||||
outcome.status.code(),
|
||||
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!(
|
||||
stderr.contains("access denied") && stderr.contains("owner/absent"),
|
||||
"the denial names the collapsed `access denied` band with the normalized id, no existence \
|
||||
oracle — stderr: {stderr:?}"
|
||||
oracle; stderr: {stderr:?}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("not found"),
|
||||
"no repository-existence oracle leaks — stderr: {stderr:?}"
|
||||
"no repository-existence oracle leaks; stderr: {stderr:?}"
|
||||
);
|
||||
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).
|
||||
//!
|
||||
//! The vectors are the wave-1 test-key fixture set
|
||||
@@ -7,8 +7,8 @@
|
||||
//! byte-verbatim from `ssh-keygen -lf` (OpenSSH 9.6p1, at
|
||||
//! vector-generation time; see `tests/vectors/fingerprints.txt`). The
|
||||
//! crate's single fingerprint path must reproduce those strings
|
||||
//! exactly from every issuer shape — the committed public blob, the
|
||||
//! private key's derived public half, and the committed `.pub` file —
|
||||
//! exactly from every issuer shape (the committed public blob, the
|
||||
//! private key's derived public half, and the committed `.pub` file)
|
||||
//! so registration values from any standard issuer compare equal,
|
||||
//! without cross-issuer drift.
|
||||
|
||||
@@ -21,7 +21,7 @@ use common::FIXTURE_KEYS;
|
||||
const VECTOR_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors");
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// - *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
|
||||
/// (`decode_secret_key` → sign/serve) — yields a public half whose
|
||||
/// The private-key path, the shape every auth/mapping test uses
|
||||
/// (`decode_secret_key` → sign/serve), yields a public half whose
|
||||
/// fingerprint is the same recorded value.
|
||||
///
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint string
|
||||
@@ -66,7 +66,7 @@ fn private_derived_fingerprints_match_recorded_values() {
|
||||
/// value.
|
||||
///
|
||||
/// - *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
|
||||
/// is exactly the at-rest contract: unencrypted OpenSSH PEM).
|
||||
#[test]
|
||||
@@ -106,7 +106,7 @@ fn committed_fixture_files_match_the_recorded_keys() {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// - *Contract stamp:* wave-1 test-double convention (recorded
|
||||
@@ -130,7 +130,7 @@ fn recorded_manifest_covers_every_fixture() {
|
||||
/// url-safe-alphabet drift (`+`/`/`, no `=`).
|
||||
///
|
||||
/// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded
|
||||
/// base64, standard alphabet — byte-identical comparison by
|
||||
/// base64, standard alphabet; byte-identical comparison by
|
||||
/// contract).
|
||||
#[test]
|
||||
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
|
||||
//! seam over the russh channel duplex (ADR-006 decision 2), the
|
||||
//! process-mode dialect, the denial byte-order, the exit-status
|
||||
@@ -6,7 +6,7 @@
|
||||
//! harness against the hand-rolled in-memory alkgit doubles
|
||||
//! (`tests/common/git_doubles.rs`). The decline-matrix rows *per
|
||||
//! 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'
|
||||
//! compile-time feature isolation).
|
||||
|
||||
@@ -63,8 +63,8 @@ mod serving {
|
||||
session
|
||||
}
|
||||
|
||||
/// Opens a session channel and routes one git exec — the rows'
|
||||
/// serving driver. Asserts the routed exec's Success reply (the
|
||||
/// Opens a session channel and routes one git exec (the rows'
|
||||
/// serving driver). Asserts the routed exec's Success reply (the
|
||||
/// routing decision, on the wire).
|
||||
pub(super) async fn route_git(session: &LoopbackSession, command: &str) -> Channel<Msg> {
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
@@ -83,7 +83,7 @@ mod serving {
|
||||
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
|
||||
/// posture; replicated from `dispatch_core.rs`).
|
||||
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(),
|
||||
"version 2\n",
|
||||
"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();
|
||||
@@ -269,7 +269,8 @@ mod serving {
|
||||
/// `serve_duplex_resolved`) + the exit_status row (clean serve
|
||||
/// end → exit-status(0) **before** close, from the spawned task)
|
||||
/// + 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.
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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*
|
||||
/// 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
|
||||
/// evidence, then close (status before close, spawned task).
|
||||
#[tokio::test]
|
||||
@@ -366,7 +367,7 @@ mod serving {
|
||||
let band = match next_packet(&mut channel, &mut framer, "the denial band").await {
|
||||
Packet::Payload(payload) => payload,
|
||||
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!(
|
||||
@@ -388,10 +389,10 @@ mod serving {
|
||||
/// convention 7) + the identity-ride decision (`w2-auth-bridge`'s
|
||||
/// parked item, discharged `w3-git-mapping`): the resolved
|
||||
/// 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
|
||||
/// 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]
|
||||
async fn identity_ride_witnessed_by_the_authorize_gate() {
|
||||
let granted = Arc::new(gd::MemGit::new());
|
||||
@@ -428,9 +429,9 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* mapping.md §the seams (the git row:
|
||||
/// `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
|
||||
/// (`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).
|
||||
#[tokio::test]
|
||||
async fn packless_receive_pack_applies_the_cas_and_reports() {
|
||||
@@ -508,7 +509,7 @@ mod serving {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// reaches the seam.
|
||||
#[tokio::test]
|
||||
@@ -546,7 +547,7 @@ mod serving {
|
||||
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.
|
||||
#[tokio::test]
|
||||
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
|
||||
/// decline at the git row, never resolve): charset, segments,
|
||||
/// 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]
|
||||
async fn invalid_repo_ids_decline_before_the_seam() {
|
||||
let doubles = Arc::new(gd::MemGit::new());
|
||||
@@ -626,7 +627,7 @@ mod serving {
|
||||
/// - *Contract stamp:* mapping.md §Data-plane/limits +
|
||||
/// `Limits::max_exec_command_len` (the per-length cap enforced
|
||||
/// **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
|
||||
/// failure, alive channel, nothing parsed.
|
||||
#[tokio::test]
|
||||
@@ -688,18 +689,18 @@ mod serving {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// serve's *answer volume* exceeds the whole outbound pipeline
|
||||
/// (the harness client's packet loop is frozen on the armed
|
||||
/// data gate → the transport duplex `DUPLEX_CAPACITY` fills →
|
||||
/// the server's flush pends → russh's bounded message queue —
|
||||
/// `event_buffer_size`, default 10, pinned below — fills → the
|
||||
/// the server's flush pends → russh's bounded message queue
|
||||
/// (`event_buffer_size`, default 10, pinned below) fills → the
|
||||
/// serve's remaining answer writes pend). The gate's release
|
||||
/// then drives the serve tail through the just-unwinding
|
||||
/// contention: the backlogged answer data drains **through the
|
||||
/// 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
|
||||
/// close-contracting sender; sequential awaited sends on the
|
||||
/// one handle), pinning every wire row of this class against
|
||||
@@ -710,7 +711,7 @@ mod serving {
|
||||
russh::server::Config::default().event_buffer_size,
|
||||
10,
|
||||
"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"
|
||||
);
|
||||
|
||||
@@ -823,13 +824,13 @@ mod serving {
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 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.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// - *Contract stamp:* POC-3 §A.4 (the accepted-but-held Channel →
|
||||
/// `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.
|
||||
#[tokio::test]
|
||||
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
|
||||
// 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
|
||||
// gate's booked finding 2.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// - *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
|
||||
/// `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
|
||||
/// (`src/observe.rs`: class `request`, reason
|
||||
/// `channel-not-held`, route `git`; the advertisement drains
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
async fn second_exec_on_a_served_git_channel_declines_channel_not_held() {
|
||||
@@ -913,7 +914,7 @@ mod serving {
|
||||
doubles.resolved_ids(),
|
||||
vec!["owner/demo".to_owned()],
|
||||
"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'")
|
||||
@@ -957,7 +958,7 @@ mod serving {
|
||||
#[cfg(feature = "git")]
|
||||
mod real_tool {
|
||||
//! 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
|
||||
//! (recorded): the smoke requires `git` ≥ 2.43 and `ssh` in the
|
||||
//! environment; when absent it is **skipped with a printed reason**
|
||||
@@ -977,11 +978,11 @@ mod real_tool {
|
||||
async fn real_git_clone_smoke() {
|
||||
let (git_ok, git_reason) = git_available();
|
||||
if !git_ok {
|
||||
eprintln!("SKIPPED (with reason): real-tool clone smoke — {git_reason}");
|
||||
eprintln!("SKIPPED (with reason): real-tool clone smoke: {git_reason}");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1053,7 +1054,7 @@ mod real_tool {
|
||||
let stderr = String::from_utf8_lossy(&outcome.stderr).to_string();
|
||||
assert!(
|
||||
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!(
|
||||
target.join(".git").is_dir(),
|
||||
@@ -1061,7 +1062,7 @@ mod real_tool {
|
||||
);
|
||||
assert!(
|
||||
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!(
|
||||
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
|
||||
//! `MemIdentityProvider`) → authenticated-session observation through
|
||||
//! the wave-2 stack, plus the observation shapes (auth failure,
|
||||
@@ -6,7 +6,7 @@
|
||||
//! tests speak through the same helpers.
|
||||
//!
|
||||
//! 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
|
||||
//! `Limits` keepalive/wall-clock posture) are the harness's own
|
||||
//! decisions, exercised here.
|
||||
@@ -46,7 +46,7 @@ fn permissive_policy() -> Arc<MemHostKeyPolicy> {
|
||||
/// in-process transport) + ADR-004 decision 1 (the fixture key's
|
||||
/// fingerprint accepts through `MemIdentityProvider`) + ADR-005
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// serving over the mount) + `mapping.md` §Decline matrix (the
|
||||
/// 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
|
||||
/// a session-serving feature this row never sees a decline, so it
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// - *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)
|
||||
/// + ADR-004 decision 4 (the remaining-methods set is exactly
|
||||
/// `{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
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
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
|
||||
//! capability declines, the seam-stub serving declines with their
|
||||
//! spawned-path lifecycle, and the auth dispositions) emits its
|
||||
@@ -7,17 +7,17 @@
|
||||
//!
|
||||
//! **Test convention (recorded):** the family's only tracing-test
|
||||
//! 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
|
||||
//! [`tracing::Subscriber`] capturing events into a shared buffer,
|
||||
//! installed once as the global default (the server side runs on
|
||||
//! spawned tasks — a thread-local default cannot see them; a global
|
||||
//! capture needs no extra dependency). The install is up front — the
|
||||
//! spawned tasks, which a thread-local default cannot see; a global
|
||||
//! capture needs no extra dependency). The install is up front; the
|
||||
//! mount helpers call [`capture`] before any wire exchange, so no
|
||||
//! event is emitted below the not-yet-installed global. Rows assert
|
||||
//! presence by field-matching (`row` + `decline_class` + `reason` +
|
||||
//! 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
|
||||
//! failure arm) are pinned in `src/observe.rs`'s own unit rows.
|
||||
//!
|
||||
@@ -44,7 +44,7 @@ use russh::{Channel, ChannelMsg};
|
||||
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)]
|
||||
@@ -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.
|
||||
fn captured() -> Vec<Captured> {
|
||||
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
|
||||
/// same convention as `src/observe.rs`'s cfg-derived unit row).
|
||||
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).
|
||||
fn is_global_row(row: Option<&str>) -> bool {
|
||||
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
|
||||
/// 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
|
||||
/// caller matched on.
|
||||
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).
|
||||
#[tokio::test]
|
||||
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`
|
||||
/// 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
|
||||
/// build's gate: `unfeatured` where the feature is off,
|
||||
/// `seam-unconfigured` where the tunnel feature is on but no
|
||||
/// 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"))]
|
||||
#[tokio::test]
|
||||
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
|
||||
/// decline-matrix's config-driven notes (the `w3-tunnel-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.
|
||||
#[cfg(feature = "tunnel")]
|
||||
#[tokio::test]
|
||||
@@ -310,7 +310,7 @@ async fn tcpip_forward_decline_emits_the_seam_unconfigured_event() {
|
||||
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
|
||||
/// decline without exposing who).
|
||||
#[tokio::test]
|
||||
@@ -363,7 +363,7 @@ async fn auth_none_decline_emits_the_auth_event() {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// message; `w3-decline-tracing`'s safety posture, pinned by
|
||||
/// scanning every captured event across the whole binary's rows).
|
||||
@@ -426,7 +426,7 @@ mod default_build {
|
||||
use common::harness::open_session_channel;
|
||||
|
||||
/// - *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).
|
||||
#[tokio::test]
|
||||
async fn unfeatured_session_open_emits_the_open_event() {
|
||||
@@ -453,11 +453,11 @@ mod git_build {
|
||||
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 /
|
||||
/// decline / ended ride the spawned task, in wiring order). The
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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,
|
||||
/// `w3-tty-session`).
|
||||
#[tokio::test]
|
||||
@@ -669,7 +669,7 @@ mod session_build {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// spawned task).
|
||||
#[tokio::test]
|
||||
@@ -703,7 +703,7 @@ mod tunnel_build {
|
||||
|
||||
/// - *Contract stamp:* the unset-declines posture (`w3-seam-config`,
|
||||
/// 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
|
||||
/// double's real decline events ride tests/tunnel_mapping.rs).
|
||||
#[tokio::test]
|
||||
@@ -766,12 +766,13 @@ async fn authenticated(username: &str) -> common::harness::LoopbackSession {
|
||||
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
|
||||
/// before any acceptance exists, so these rows must not pre-derive
|
||||
/// themselves through [`authenticated`] (a second auth exchange on an
|
||||
/// authenticated session is not a supported russh surface — mounted
|
||||
/// fresh is the honest shape, matching `auth_bridge.rs`'s rows).
|
||||
/// authenticated session is not a supported russh surface, so it is
|
||||
/// mounted fresh; that is the honest shape, matching
|
||||
/// `auth_bridge.rs`'s rows).
|
||||
async fn mounted() -> common::harness::LoopbackSession {
|
||||
capture();
|
||||
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"
|
||||
);
|
||||
// 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.
|
||||
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
|
||||
//! the real wire.
|
||||
//!
|
||||
@@ -7,7 +7,7 @@
|
||||
//! peer's follow-up bytes (POC-3 proved by construction with
|
||||
//! receive-pack). The crate's plumbing
|
||||
//! ([`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
|
||||
//! mechanical: the handler hook clones `session.handle()`, spawns the
|
||||
//! serving future, and returns.
|
||||
@@ -16,10 +16,10 @@
|
||||
//! proof handler stands in for the crate's own auth/dispatch hooks
|
||||
//! (their correctness is `w2-auth-bridge`/`w2-dispatch-decline-core`
|
||||
//! 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
|
||||
//! *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
|
||||
//! routing the connection's follow-up traffic.
|
||||
|
||||
@@ -41,14 +41,14 @@ use russh::{Channel, ChannelId, ChannelMsg, Disconnect, Error, MethodKind, Metho
|
||||
|
||||
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)`).
|
||||
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";
|
||||
|
||||
/// 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.
|
||||
/// `watch`-based (no lost-wakeup race: a late subscriber still sees
|
||||
/// the release).
|
||||
@@ -56,7 +56,7 @@ fn serving_gate() -> tokio::sync::watch::Sender<bool> {
|
||||
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
|
||||
/// lost or replayed.
|
||||
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)]
|
||||
struct ProofRecord {
|
||||
handler_returned_at: Option<Instant>,
|
||||
@@ -74,7 +74,7 @@ struct 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
|
||||
/// session channels, and serves every exec by *the crate's own
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// (not the bridge contract; that is `w2-auth-bridge`'s suite).
|
||||
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
|
||||
/// (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.
|
||||
async fn exec_request(
|
||||
&mut self,
|
||||
@@ -151,7 +151,7 @@ impl russh::server::Handler for ProofServer {
|
||||
/// server task (russh's own stack over the proof handler), and the
|
||||
/// connected client. Transport and posture mirror the loopback
|
||||
/// 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.
|
||||
struct ProofSession {
|
||||
client: russh::client::Handle<HarnessClientHandler>,
|
||||
@@ -210,7 +210,7 @@ async fn mount() -> ProofSession {
|
||||
}
|
||||
|
||||
impl ProofSession {
|
||||
/// Releases the gate — the spawned serving paths complete
|
||||
/// Releases the gate; the spawned serving paths complete
|
||||
/// asynchronously from here on.
|
||||
fn release_gate(&self) {
|
||||
let _ = self.gate.send(true);
|
||||
@@ -228,7 +228,7 @@ impl ProofSession {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
async fn exec_hook_has_returned(&self) {
|
||||
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
|
||||
/// 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) {
|
||||
let record = self.record.lock();
|
||||
assert!(
|
||||
@@ -250,7 +250,7 @@ impl ProofSession {
|
||||
);
|
||||
assert!(
|
||||
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:
|
||||
/// 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
|
||||
/// constraint: spawn-and-return (the follow-up-bytes deadlock
|
||||
/// 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.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
|
||||
// the first serving path is still pending.
|
||||
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
|
||||
// after — the ordering contract verbatim.
|
||||
// after, the ordering contract verbatim.
|
||||
let first = next_message(&mut channel)
|
||||
.await
|
||||
.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
|
||||
//! alktty seam (`drive_session_pre_negotiated`) over the translation
|
||||
//! bridge, the locally-built negotiation, the control rows, the exit
|
||||
//! 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
|
||||
//! (`tests/common/tty_doubles.rs`). The decline-matrix rows *per
|
||||
//! feature build* stay in `tests/dispatch_core.rs`; this binary adds
|
||||
//! the session rows' serving surface, the unset-field declines (which
|
||||
//! 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).
|
||||
|
||||
#![allow(dead_code)]
|
||||
@@ -37,7 +37,7 @@ mod serving {
|
||||
use super::common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity};
|
||||
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
|
||||
/// until the deadline).
|
||||
#[derive(Debug)]
|
||||
@@ -50,7 +50,7 @@ mod serving {
|
||||
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).
|
||||
pub(super) async fn next_observation(channel: &mut Channel<Msg>) -> Option<Observed> {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
@@ -86,7 +86,7 @@ mod serving {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// order).
|
||||
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
|
||||
/// drive Success paths.
|
||||
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");
|
||||
}
|
||||
|
||||
/// 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),
|
||||
/// overridable per row through the mutator. The identity resolves
|
||||
/// 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
|
||||
/// — 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 {
|
||||
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).
|
||||
pub(super) async fn session_with_limits(
|
||||
mutate: impl FnOnce(&mut ServeConfig),
|
||||
@@ -235,13 +235,13 @@ mod serving {
|
||||
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) {
|
||||
let seam = td::tty_seam(shim);
|
||||
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).
|
||||
|
||||
#[tokio::test]
|
||||
@@ -338,7 +338,7 @@ mod serving {
|
||||
assert_eq!(
|
||||
params.cmd,
|
||||
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!(
|
||||
params.terminal.is_none(),
|
||||
@@ -421,13 +421,13 @@ mod serving {
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 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
|
||||
// gate's booked finding 2.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// - *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
|
||||
/// `channel_failure`, inline, the first serve rides on) + the
|
||||
/// event vocabulary (`src/observe.rs`: class `request`, reason
|
||||
@@ -485,12 +485,12 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (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`,
|
||||
/// inline, the first serve rides on) + the event vocabulary
|
||||
/// (`src/observe.rs`: class `request`, reason
|
||||
/// `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]
|
||||
async fn second_shell_request_on_a_served_shell_channel_declines_channel_not_held() {
|
||||
ec::install();
|
||||
@@ -543,7 +543,7 @@ mod serving {
|
||||
// ------------------------------------------------------------------
|
||||
// The channel_close-fired per-channel cancel path on the wire
|
||||
// (`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
|
||||
// witness the established-channel-close site, `channel_cap.rs`
|
||||
// release site 1).
|
||||
@@ -553,9 +553,9 @@ mod serving {
|
||||
/// `channel_cap.rs` release site 1 + `handler.rs`
|
||||
/// §the control rows' per-channel surface (a client close on a
|
||||
/// 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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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!(
|
||||
!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, \
|
||||
so this is the channel_close path, not the teardown)"
|
||||
);
|
||||
@@ -622,7 +622,7 @@ mod serving {
|
||||
|
||||
let mut recovered = open_session_channel(&session.client)
|
||||
.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")
|
||||
.await
|
||||
.expect("the recovered channel routes");
|
||||
@@ -634,19 +634,19 @@ mod serving {
|
||||
// ------------------------------------------------------------------
|
||||
// The Closed/Break no-exit-chunk close shape on the wire
|
||||
// (`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
|
||||
// Closed/Break classification rows live in `src/tty_map.rs`'s test
|
||||
// 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// half down, and the bridge's next read is EOF (`Closed`) with
|
||||
/// the scripted exit never forwarded. Nothing fabricated.
|
||||
@@ -754,13 +754,13 @@ mod serving {
|
||||
}
|
||||
assert!(
|
||||
!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 session never resolves its exit (script exit = None);
|
||||
// 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.
|
||||
session.unmount().await;
|
||||
for _ in 0..100 {
|
||||
@@ -771,7 +771,7 @@ mod serving {
|
||||
}
|
||||
assert!(
|
||||
!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"
|
||||
);
|
||||
}
|
||||
@@ -787,7 +787,7 @@ mod serving {
|
||||
let session = session_with(shim_seam(&shim)).await;
|
||||
|
||||
// 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)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
@@ -840,7 +840,7 @@ mod serving {
|
||||
.await
|
||||
.expect("the signal rides (fire-and-forget)");
|
||||
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;
|
||||
}
|
||||
@@ -853,7 +853,7 @@ mod serving {
|
||||
/// ≥ 0 → `exit-status(N)`; negative `−N`, N ≥ 2 →
|
||||
/// `exit-signal(name)` via the table; POC-3 §D.2's `−15` →
|
||||
/// `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).
|
||||
#[tokio::test]
|
||||
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!(
|
||||
!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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
async fn unknown_backend_answers_255_then_close() {
|
||||
@@ -958,7 +958,7 @@ mod serving {
|
||||
assert_eq!(
|
||||
ids,
|
||||
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"
|
||||
);
|
||||
session.unmount().await;
|
||||
@@ -1009,7 +1009,7 @@ mod unset_declines {
|
||||
use russh::client::Msg;
|
||||
use russh::{Channel, ChannelMsg};
|
||||
|
||||
/// An authenticated loopback over a mutated config — the
|
||||
/// An authenticated loopback over a mutated config, the
|
||||
/// unset-field rows' driver.
|
||||
async fn session_with(mutate: impl FnOnce(&mut ServeConfig)) -> LoopbackSession {
|
||||
let provider = Arc::new(MemIdentityProvider::new());
|
||||
@@ -1049,7 +1049,7 @@ mod unset_declines {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// task).
|
||||
#[tokio::test]
|
||||
@@ -1129,7 +1129,7 @@ mod unset_declines {
|
||||
mod real_tool {
|
||||
//! The real-tool smoke (`w3-tty-session`): one OpenSSH pty
|
||||
//! 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
|
||||
//! row cannot observe what vanilla OpenSSH actually sends; POC-3
|
||||
//! 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::{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
|
||||
/// test graph only).
|
||||
fn local_seam() -> SessionSeam {
|
||||
@@ -1171,11 +1171,11 @@ mod real_tool {
|
||||
#[tokio::test]
|
||||
async fn real_openssh_pty_session_smoke() {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1265,7 +1265,7 @@ mod real_tool {
|
||||
assert!(
|
||||
success,
|
||||
"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
|
||||
//! `direct-tcpip`/`direct-streamlocal` through the published alktunnels
|
||||
//! 0.5.0 seam (alkssh 0.2.0's W2 adoption: `TunnelSession::open_direct`
|
||||
//! → accept only after the fabric open succeeds → `take_halves()`
|
||||
//! 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
|
||||
//! 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
|
||||
//! dial/dial-fail seam, hand-rolled here, never a sibling-checkout
|
||||
//! path).
|
||||
@@ -21,19 +21,19 @@
|
||||
//! the release mechanism: an admitted-but-rejected bridge releases its
|
||||
//! count, a closed serving channel recovers), the composed identity
|
||||
//! 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
|
||||
//! witness (the POC-3 §C.3 shape), the OQ-11 off-Unix gate, and the
|
||||
//! real-tool `ssh -L` smoke.
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! are fully deterministic (no sockets on the target substrate), while
|
||||
//! the real-tool smoke hand-rolls a real-TCP dial closure against a
|
||||
//! 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).
|
||||
|
||||
#![allow(dead_code)]
|
||||
@@ -85,7 +85,7 @@ mod serving {
|
||||
/// tunnel-featured serve-config holds. The accept side's channel-0
|
||||
/// identity (`caller`) is what the fabric's
|
||||
/// `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
|
||||
/// CF-006) saw.
|
||||
pub(super) async fn wire_fabric(
|
||||
@@ -109,7 +109,7 @@ mod serving {
|
||||
return;
|
||||
};
|
||||
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
|
||||
// `set_identity` mechanics decision reads (the identity
|
||||
// the serving dispatch resolves for the mapped opens'
|
||||
@@ -159,7 +159,7 @@ mod serving {
|
||||
|
||||
/// The leg-2 fabric double (`w3-tunnel-listen`'s recorded
|
||||
/// 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
|
||||
/// alktunnels pieces: a `bindfirst_establisher`-shaped
|
||||
/// establisher, `make_bindfirst_accept_handler`, and the
|
||||
@@ -175,7 +175,7 @@ mod serving {
|
||||
/// wire-mapped resource string as the bind endpoint directly
|
||||
/// (`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
|
||||
/// 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`
|
||||
/// renders to `127.0.0.1` so the real-tool smoke's curl is
|
||||
/// deterministic about the family.
|
||||
@@ -184,7 +184,7 @@ mod serving {
|
||||
pub(super) registry: Arc<OperationRegistry>,
|
||||
opener: Arc<tokio::sync::OnceCell<ForwardedOpener>>,
|
||||
/// 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).
|
||||
minted: tokio::sync::Mutex<mpsc::UnboundedReceiver<u32>>,
|
||||
}
|
||||
@@ -192,7 +192,7 @@ mod serving {
|
||||
impl FabricDouble {
|
||||
/// The fabric's `ForwardedOpener` (the call plane), exported
|
||||
/// 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).
|
||||
pub(super) async fn forwarded_opener(&self) -> ForwardedOpener {
|
||||
for _ in 0..600 {
|
||||
@@ -206,13 +206,13 @@ mod serving {
|
||||
|
||||
/// Drives the fabric's forwarded-open closure directly:
|
||||
/// `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
|
||||
/// `recv` half), `Err` is the door-side refusal (the
|
||||
/// 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,
|
||||
/// in mint order — the rows' real-token vehicle).
|
||||
/// in mint order (the rows' real-token vehicle).
|
||||
pub(super) async fn minted_token(&self) -> u32 {
|
||||
self.minted
|
||||
.lock()
|
||||
@@ -240,7 +240,7 @@ mod serving {
|
||||
/// The double's establisher mint discipline: `Counter` is the
|
||||
/// ADR-008 shape the real fabric carries (a non-zero-seeded
|
||||
/// 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
|
||||
/// discipline prevents), then the counter continues from where it
|
||||
/// left.
|
||||
@@ -306,7 +306,7 @@ mod serving {
|
||||
|
||||
/// The double's real-socket bind seam: real TCP/unix binds of 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
|
||||
/// unlink a stale socket file first (`bind_unix`'s caller
|
||||
/// posture).
|
||||
@@ -357,7 +357,7 @@ mod serving {
|
||||
/// invocation (the dispatcher's per-accept contract), downcasting
|
||||
/// the plan's opaque listener to the bind seam's own wrappers. The
|
||||
/// 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).
|
||||
fn real_accept_loop() -> BindFirstAcceptLoop {
|
||||
Arc::new(move |listener: Arc<dyn std::any::Any + Send + Sync>| {
|
||||
@@ -412,7 +412,7 @@ mod serving {
|
||||
.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
|
||||
/// composition, against a leg-1 fail dial the -R rows never
|
||||
/// exercise). `door_scoped_caller` composes the fabric-side
|
||||
@@ -420,7 +420,7 @@ mod serving {
|
||||
/// `arrival_scoped` composes the door-side `ServingConfig.identity`
|
||||
/// with or without the `tunnel:forwarded` scope (the composed-gate
|
||||
/// 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(
|
||||
door_scoped_caller: bool,
|
||||
arrival_scoped: bool,
|
||||
@@ -428,9 +428,9 @@ mod serving {
|
||||
) -> FabricDouble {
|
||||
// The two sides' registries are independent (the fabric's own
|
||||
// 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
|
||||
// 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
|
||||
// the DOOR's dispatch surface alone.
|
||||
let door_registry = Arc::new(OperationRegistry::new());
|
||||
@@ -499,7 +499,7 @@ mod serving {
|
||||
.await;
|
||||
// The real opener maps the wire refusal's
|
||||
// reason through the `From<TunnelEstablish
|
||||
// Error> for EstablishmentError` table — the
|
||||
// Error> for EstablishmentError` table; the
|
||||
// typed `unknown_resource` (the gate's miss
|
||||
// class) must survive the client side here,
|
||||
// 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
|
||||
/// other half when the establisher renders a matching target.
|
||||
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`,
|
||||
/// doubled): an exact `host:port` / socket-path key pops the
|
||||
/// 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
|
||||
/// witnesses the "no fabric call" assertions.
|
||||
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
|
||||
/// admits the mapped opens.
|
||||
fn scoped_caller() -> Identity {
|
||||
@@ -707,7 +707,7 @@ mod serving {
|
||||
/// (kept near the top of the module for the rows' readability).
|
||||
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.
|
||||
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
assert!(
|
||||
@@ -717,7 +717,7 @@ mod serving {
|
||||
}
|
||||
|
||||
/// 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:
|
||||
/// zero framing, both directions live).
|
||||
async fn round_trip(
|
||||
@@ -773,7 +773,7 @@ mod serving {
|
||||
/// - *Contract stamp:* ADR-006 decision 1 (the route-1 bridge: the
|
||||
/// `direct-tcpip` open → `TunnelSession::open_direct` over a held
|
||||
/// `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).
|
||||
#[tokio::test]
|
||||
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 →
|
||||
/// 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
|
||||
/// the real decline; POC-3 §C.2's failure shape).
|
||||
#[tokio::test]
|
||||
@@ -876,7 +876,7 @@ mod serving {
|
||||
assert_eq!(
|
||||
calls.load(Ordering::SeqCst),
|
||||
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;
|
||||
@@ -885,10 +885,10 @@ mod serving {
|
||||
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded cap release
|
||||
/// mechanism (the admission is taken at the hook and held across
|
||||
/// 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
|
||||
/// 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
|
||||
/// `AdministrativelyProhibited` if the count had leaked).
|
||||
#[tokio::test]
|
||||
@@ -922,7 +922,7 @@ mod serving {
|
||||
// of this same session observes the released count.
|
||||
let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36002)
|
||||
.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(
|
||||
&mut channel,
|
||||
target_side,
|
||||
@@ -938,7 +938,7 @@ mod serving {
|
||||
/// - *Contract stamp:* AGENTS.md convention 9 / `overview.md`
|
||||
/// §Bounded-resources policy + `ChannelCap`'s recorded
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
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
|
||||
// once: the moment the cap admits again, the open proceeds
|
||||
// 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.
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
let mut attempts = 0u32;
|
||||
@@ -1003,11 +1003,11 @@ mod serving {
|
||||
Err(Error::ChannelOpenFailure(
|
||||
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(_)) => {
|
||||
// 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.
|
||||
return;
|
||||
}
|
||||
@@ -1023,13 +1023,13 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
|
||||
/// 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
|
||||
/// scope is refused by the fabric's dispatch (`FORBIDDEN`), the
|
||||
/// establisher never runs, and the door answers the SSH open
|
||||
/// with the explicit `ConnectFailed`. The auth bridge's
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
async fn the_fabric_acl_refuses_an_unscoped_channels_identity() {
|
||||
@@ -1056,7 +1056,7 @@ mod serving {
|
||||
assert_eq!(
|
||||
calls.load(Ordering::SeqCst),
|
||||
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;
|
||||
@@ -1064,7 +1064,7 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip`'s
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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
|
||||
/// 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
|
||||
/// witness at zero. On-Unix builds ride the serve row above.
|
||||
#[cfg(not(unix))]
|
||||
@@ -1125,7 +1125,7 @@ mod serving {
|
||||
assert_eq!(
|
||||
calls.load(Ordering::SeqCst),
|
||||
0,
|
||||
"the runtime gate precedes any fabric call — no partial bridge"
|
||||
"the runtime gate precedes any fabric call; no partial bridge"
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
@@ -1188,7 +1188,7 @@ mod serving {
|
||||
|
||||
/// The arrival row's pump legs: the arrival's request reaches the
|
||||
/// 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,
|
||||
/// reversed).
|
||||
async fn arrival_pump(
|
||||
@@ -1231,8 +1231,8 @@ mod serving {
|
||||
/// (`tcpip_forward` → the family-ward listen dial) + §Channel-type
|
||||
/// rows, the `forwarded-tcpip` row (the data leg: per fabric
|
||||
/// arrival the door opens a `forwarded-tcpip` channel **toward
|
||||
/// the vanilla client**, echoing the listen request's own args —
|
||||
/// the OpenSSH matcher parity — and pumping the fabric
|
||||
/// the vanilla client**, echoing the listen request's own args
|
||||
/// (the OpenSSH matcher parity) and pumping the fabric
|
||||
/// `BiStream` ↔ the SSH channel). Observed end to end through the
|
||||
/// harness's accept hook.
|
||||
#[tokio::test]
|
||||
@@ -1302,10 +1302,10 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* `client.md` §Lifecycle primitives (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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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`):
|
||||
/// 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
|
||||
/// re-connect is refused), and a second cancel of the same
|
||||
/// (address, port) pair answers the not-found denial
|
||||
/// (`RequestDenied` — the sshd cancel-of-unknown parity).
|
||||
/// (`RequestDenied`, the sshd cancel-of-unknown parity).
|
||||
#[tokio::test]
|
||||
async fn cancel_tcpip_forward_tears_the_fabric_listener_down() {
|
||||
let double = wire_fabric_with_listen(true, true).await;
|
||||
@@ -1385,7 +1385,7 @@ mod serving {
|
||||
|
||||
// The fabric listener is gone (the teardown cascaded: the
|
||||
// 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.
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
@@ -1415,10 +1415,10 @@ mod serving {
|
||||
/// upstream guarantee the door defends in depth against: the
|
||||
/// test forces what the discipline prevents (the double
|
||||
/// 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
|
||||
/// 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
|
||||
/// live adapter's arrival routes.
|
||||
#[tokio::test]
|
||||
@@ -1434,7 +1434,7 @@ mod serving {
|
||||
.await;
|
||||
// The ceiling of 2 both admits the collided mint past 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
|
||||
// would be cap-exceeded.
|
||||
let limits = Limits {
|
||||
@@ -1475,16 +1475,16 @@ mod serving {
|
||||
.expect("the refused listen's fabric listener was torn down");
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
.await
|
||||
.expect("the collision released the listener count");
|
||||
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
|
||||
// forwarded-tcpip channel and the bytes ride the pump.
|
||||
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
|
||||
// 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())
|
||||
.await
|
||||
.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
|
||||
/// this door) fails the open at the call plane as a typed
|
||||
/// `channel:open_failed` (`TunnelEstablishError::UnknownResource`
|
||||
/// — the wire's `unknown_resource` reason) **before any channel
|
||||
/// is born** — the phantom-channel shape (call-plane success,
|
||||
/// the wire's `unknown_resource` reason) **before any channel
|
||||
/// is born**; the phantom-channel shape (call-plane success,
|
||||
/// then handler-side decline) is closed at the open: no channel
|
||||
/// allocation, no adopted halves, no acceptor row.
|
||||
#[tokio::test]
|
||||
@@ -1552,7 +1552,7 @@ mod serving {
|
||||
let outcome = double.open_forwarded_directly(params).await;
|
||||
let err = match outcome {
|
||||
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,
|
||||
};
|
||||
@@ -1561,7 +1561,7 @@ mod serving {
|
||||
"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.
|
||||
tokio::time::timeout(Duration::from_millis(500), opens.recv())
|
||||
.await
|
||||
@@ -1573,7 +1573,7 @@ mod serving {
|
||||
|
||||
/// - *Contract stamp:* the forwarded gate's pass posture (alktunnels
|
||||
/// 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
|
||||
/// channel at the acceptor, and the fabric-side halves are the
|
||||
/// 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
|
||||
/// invariant's alkcall half on the `-R` leg): the door-side
|
||||
/// 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
|
||||
/// openable's own spec) and no SSH channel opens, even for a
|
||||
/// genuinely minted token. The genuinely-minted-token shape
|
||||
/// double-pins the gate ordering now (the establisher variant's
|
||||
/// 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.
|
||||
#[tokio::test]
|
||||
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
|
||||
// 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.
|
||||
let token = double.minted_token().await;
|
||||
let params = alktunnels::params::TunnelForwardedParams {
|
||||
@@ -1693,7 +1693,7 @@ mod serving {
|
||||
/// - *Contract stamp:* AGENTS.md convention 9 (the listener count
|
||||
/// is a bounded resource): the per-session listener map is
|
||||
/// 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
|
||||
/// free, and the cancel releases the count again.
|
||||
#[tokio::test]
|
||||
@@ -1739,10 +1739,10 @@ mod serving {
|
||||
/// - *Contract stamp:* AGENTS.md convention 9 / the acceptor
|
||||
/// vocabulary's cap rows (the wave-2 gate's booked shape): the
|
||||
/// 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
|
||||
/// 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).
|
||||
#[tokio::test]
|
||||
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");
|
||||
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
|
||||
// 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,
|
||||
// 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.
|
||||
drop(first_arrival);
|
||||
let _ = first_channel.close().await;
|
||||
@@ -1807,7 +1807,7 @@ mod serving {
|
||||
third_arrival.write_all(b"again").await.expect("rides");
|
||||
let third_open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
||||
.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");
|
||||
let ForwardedOpen::Tcpip {
|
||||
channel: mut third_channel,
|
||||
@@ -1836,7 +1836,7 @@ mod serving {
|
||||
/// - *Contract stamp:* OQ-11 on the listen flow (the runtime-Unix
|
||||
/// gate riding the `streamlocal_forward` serving flow) + the unix
|
||||
/// 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
|
||||
/// `forwarded-streamlocal@openssh.com` toward the client, and the
|
||||
/// cancel form tears it down 1:1 (path-matched).
|
||||
@@ -1908,7 +1908,7 @@ mod serving {
|
||||
}
|
||||
|
||||
/// - *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
|
||||
/// a fabric leg but no `tunnel_listen_seam` declines every listen
|
||||
/// form with the vanilla `RequestDenied`, while the leg-1 rows
|
||||
@@ -1952,7 +1952,7 @@ mod serving {
|
||||
assert!(matches!(err, Error::RequestDenied), "got {err:?}");
|
||||
|
||||
// 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`).
|
||||
let err = open_direct_tcpip(&session.client, "127.0.0.1", 36005)
|
||||
.await
|
||||
@@ -1970,7 +1970,7 @@ mod serving {
|
||||
/// one-per-op-per-registry (mapping.md §Global-request rows / the
|
||||
/// `install_forwarded_surface` claim): alkcall's `register`
|
||||
/// 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**
|
||||
/// (`AdapterError::ListenRegistration`), never a silent steal of
|
||||
/// the first adapter's arrival routes.
|
||||
@@ -2011,7 +2011,7 @@ mod serving {
|
||||
mod client_smoke {
|
||||
//! The full-circle smoke (`w4-reverse-listen`'s recorded posture):
|
||||
//! 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
|
||||
//! alkcall fabric double (the composition the alkforge assembly
|
||||
//! makes). A connection to the minted bound port round-trips
|
||||
@@ -2174,7 +2174,7 @@ mod client_smoke {
|
||||
#[cfg(all(feature = "tunnel", unix))]
|
||||
mod real_tool {
|
||||
//! 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
|
||||
//! git task's): the smoke requires `ssh`/`curl` in the environment;
|
||||
//! 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 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;
|
||||
/// alktunnels' `local` feature stays off).
|
||||
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.
|
||||
async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
@@ -2248,7 +2248,7 @@ mod real_tool {
|
||||
.output()
|
||||
.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;
|
||||
}
|
||||
if std::process::Command::new("curl")
|
||||
@@ -2256,7 +2256,7 @@ mod real_tool {
|
||||
.output()
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -2371,7 +2371,7 @@ mod real_tool {
|
||||
let stdout = String::from_utf8_lossy(&through.stdout).to_string();
|
||||
assert!(
|
||||
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);
|
||||
@@ -2409,7 +2409,7 @@ mod real_tool {
|
||||
.output()
|
||||
.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;
|
||||
}
|
||||
if std::process::Command::new("curl")
|
||||
@@ -2417,7 +2417,7 @@ mod real_tool {
|
||||
.output()
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -2524,7 +2524,7 @@ mod real_tool {
|
||||
let stdout = String::from_utf8_lossy(&through.stdout).to_string();
|
||||
assert!(
|
||||
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
|
||||
# fixture set. Each entry pairs a committed OpenSSH ed25519 keypair (the
|
||||
# .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.
|
||||
#
|
||||
# The keys are throwaway test fixtures minted for the test suite only;
|
||||
# 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).
|
||||
#
|
||||
# <fixture> <expected fingerprint>
|
||||
@@ -16,7 +16,7 @@ charlie.pub SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U
|
||||
delta.pub SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs
|
||||
ca.pub SHA256:mPAq1aRKBuyH3gZs9IcLWtc1X3AhYi50P80mpo7XPSw
|
||||
# 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
|
||||
# certificate's embedded public half is alpha.pub (the same fingerprint
|
||||
# as alpha.pub; `ssh-keygen -lf alpha-cert.pub` reports it with the
|
||||
|
||||
Reference in new issue
Block a user