docs: remove em dashes from repo prose and forbid them going forward

- Rewrote em dash prose across the whole tree outside docs/reviews/
  (the historical reviews stay verbatim, matching the alktunnels and
  alktty precedent): src doc comments and inline comments (587),
  docs/architecture (319), docs/research + sdd_process (484), tests
  (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77),
  AGENTS.md (85), and the .opencode/agents prompts (109)
- Fenced content follows the precedent: comments, ASCII-diagram label
  separators, and template placeholders rewritten; wire data, JSON/BAST
  documents, byte diagrams, and transcripts survive verbatim; assert,
  expect, panic, error-display, and format message literals rewritten
  as prose; no code, fixture, or test data change
- src/rfc1928/LICENSE: the two dashes in alksocks' own provenance
  header rewritten; the MIT legal text untouched
- AGENTS.md adds Project Convention 18: no em dashes in prose; varied
  restructure, no spaced-hyphen substitutes, fenced prose rule, en
  dash numeric ranges exempt, docs/reviews/ exempt from the greps
- tasks/*.md frontmatter: the name values rewritten by the sweep are
  now quoted (a raw colon inside an unquoted YAML scalar parsed as
  null); taskgraph validate reports 37 tasks, no issues

Verification: cargo test; cargo clippy --all-targets -D warnings;
cargo fmt --check; cargo doc --no-deps; fuzz corpus replay
(cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/ and the bast.md BAST
JSON data fence
This commit is contained in:
glm-5.3-flash committed 2026-11-03 19:31:53 +00:00
1 parent 9ca8c52601
commit e196ed43e3
115 files changed
+3593 -3531

No files matched your search

+30 -30
View File
@@ -1,4 +1,4 @@
//! The cross-cutting assembly integration suite — the whole assembly
//! The cross-cutting assembly integration suite: the whole assembly
//! layer's behavior pinned in one place (the batch-2 closing task; the
//! architecture-sync check's test half).
//!
@@ -8,23 +8,23 @@
//!
//! - **The full `-D` story end to end**: a vanilla tun2proxy-shaped
//! client → the cred-bearing TCP door → the channels-native session
//! → the producer with `local` providers → real echo targets —
//! → the producer with `local` providers → real echo targets:
//! CONNECT + UDP (the RFC-header'd datagram through the UDP door) +
//! BIND (the vanilla BIND conversation through the door) all through
//! one door process against one producer registration (the
//! one-registration-three-commands regression, now through the
//! doors).
//! - **The composed story end to end**: the producer with
//! `composed::direct_egress` + `composed::bindfirst_accept` —
//! `composed::direct_egress` + `composed::bindfirst_accept`:
//! CONNECT through the direct op, ASSOCIATE through the lazy table,
//! BIND through the bind-first listener (the ADR-008 composed
//! variant set, all three commands).
//! - **Mixed assembly**: one producer registration serving
//! channels-native sessions AND door clients AND composed seams
//! concurrently (the producer/consumer-roles-not-sides posture —
//! concurrently (the producer/consumer-roles-not-sides posture:
//! both shapes on one connection).
//! - **The teardown matrix, assembly edition**: door-client disconnect
//! tears the association/channel down (no leaks — the channel count
//! tears the association/channel down (no leaks: the channel count
//! witnesses); the idle bounds fire; fresh sessions succeed
//! immediately after each teardown (no connection poisoning).
#![cfg(all(feature = "local", feature = "composed"))]
@@ -170,7 +170,7 @@ async fn vanilla_no_auth_connect(door: SocketAddr, target: SocketAddr) -> (TcpSt
(stream, head[1], addr)
}
/// An RFC-header'd datagram (the vanilla client's sendto form — the
/// An RFC-header'd datagram (the vanilla client's sendto form, the
/// door boundary's codec, hand-rolled per the RFC wire form).
fn rfc_datagram(target: SocketAddr, payload: &[u8]) -> Vec<u8> {
let SocketAddr::V4(v4) = target else {
@@ -268,7 +268,7 @@ async fn vanilla_with_creds(door: SocketAddr) -> TcpStream {
/// The composed topology: the SOCKS5 producer serves the direct op
/// (CONNECT + ASSOCIATE's egress) AND the bind-first op (BIND's
/// listener) on one connection — the ADR-008 composed variant set.
/// listener) on one connection: the ADR-008 composed variant set.
/// The direct-op dial is a real TCP/UDP-capable stand-in: a TCP dial
/// dials the real echo target (CONNECT rides it); a UDP dial yields a
/// framed echo pipe (ASSOCIATE's destinations echo datagrams).
@@ -394,7 +394,7 @@ async fn wire_composed_suite() -> ComposedSuite {
// The SOCKS5 producer: the composed accept seam (BIND) + the
// composed direct-op egress (ASSOCIATE) + a real TCP dial
// (CONNECT — the direct-op dial's TCP arm shares the real target;
// (CONNECT: the direct-op dial's TCP arm shares the real target;
// the CONNECT dial here is the crate's own local dial over the
// SOCKS5 seam, distinct from the tunnel dial).
let factory = bindfirst_accept(topo.opener.clone(), registry, COMPOSED_RESOURCE);
@@ -448,7 +448,7 @@ async fn wire_composed_suite() -> ComposedSuite {
///
/// - CONNECT: the RFC 1929 door conversation → the CONNECT request →
/// the real TCP echo target round-trips.
/// - BIND: the door's BIND conversation (its own RFC session — the
/// - BIND: the door's BIND conversation (its own RFC session, the
/// vanilla client's one-command-per-connection shape) → the app
/// server dials the advertised address → reply#2's real peer → the
/// data plane.
@@ -458,7 +458,7 @@ async fn wire_composed_suite() -> ComposedSuite {
/// All three sessions are live simultaneously mid-life of each other;
/// each teardown ends only its own session. The two doors ride two
/// channels connections to the same producer registration (the
/// assembly story: one registration, many connections — the cred-
/// assembly story: one registration, many connections. The cred-
/// bearing door's `TokenClient` takes its client's call connection,
/// the door-only contract, so the UDP door's association rides its
/// own).
@@ -472,7 +472,7 @@ async fn full_dash_d_story_one_registration_three_commands_through_the_doors() {
// bearing TCP door rides one client (its TokenClient takes the
// call connection); the UDP door's association rides a second
// connection to the same registration (the UDP relay is a
// separate datagram plane by construction — one relay socket = one
// separate datagram plane by construction: one relay socket = one
// association).
let tcp_dial = alksocks::local::tcp_dial();
let tcp_topo = wire_consumer_side_with_provider(
@@ -498,7 +498,7 @@ async fn full_dash_d_story_one_registration_three_commands_through_the_doors() {
.await;
// The cred-bearing TCP door (the CONNECT + BIND shape) + the UDP
// door (the datagram plane) — one door process per plane, the
// door (the datagram plane): one door process per plane, the
// assembly wiring consumer.md documents.
let tcp_listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let door_addr = tcp_listener.local_addr().expect("door addr");
@@ -563,7 +563,7 @@ async fn full_dash_d_story_one_registration_three_commands_through_the_doors() {
// --- BIND through the door (live, mid-life of the CONNECT) ------
// Its own door connection (the vanilla one-command-per-connection
// shape — the door's RFC half reads one command per conversation).
// shape: the door's RFC half reads one command per conversation).
let mut bind_stream = timeout(BOUND, vanilla_with_creds(door_addr))
.await
.expect("the second cred conversation");
@@ -762,7 +762,7 @@ async fn door_client_disconnect_tears_the_association_down_no_leaks() {
);
// The door client vanishes: the idle bound (the constructor knob)
// is the door's teardown trigger for a silent relay — with the
// is the door's teardown trigger for a silent relay: with the
// socket gone, no datagrams arrive, the bound fires, the
// association ends (association lifetime = channel lifetime,
// ADR-003), and the door's task returns.
@@ -815,8 +815,8 @@ async fn door_client_disconnect_tears_the_association_down_no_leaks() {
/// The composed story end to end (the ADR-008 composed variant set,
/// all three commands): the SOCKS5 producer serves
/// `composed::direct_egress` (ASSOCIATE — the lazy destination→tunnel
/// table) + `composed::bindfirst_accept` (BIND — the bind-first
/// `composed::direct_egress` (ASSOCIATE, the lazy destination→tunnel
/// table) + `composed::bindfirst_accept` (BIND, the bind-first
/// listener) + a real TCP dial (CONNECT). The channels-native
/// sessions drive all three against the one registration; the
/// direct-op dial count witnesses the table's lazy shape.
@@ -824,7 +824,7 @@ async fn door_client_disconnect_tears_the_association_down_no_leaks() {
async fn composed_story_all_three_commands_on_one_registration() {
let suite = wire_composed_suite().await;
// CONNECT through the dial seam (a real TCP echo target —
// CONNECT through the dial seam (a real TCP echo target,
// dialed by the producer's local dial).
let tcp_target = echo_tcp_server().await;
let mut session = timeout(
@@ -912,7 +912,7 @@ async fn composed_story_all_three_commands_on_one_registration() {
assert_eq!(&bbuf, b"composed-bind");
// All three sessions were live simultaneously; the teardowns end
// only their own sessions (mux/demux isolation) — exercised by
// only their own sessions (mux/demux isolation), exercised by
// the interleaving above. Drop + close.
drop(read);
drop(write);
@@ -926,7 +926,7 @@ async fn composed_story_all_three_commands_on_one_registration() {
/// producer registration serving channels-native sessions AND door
/// clients AND composed seams concurrently. The composed suite's
/// producer gains a door-shaped second consumer topology over the same
/// registration — the channels-native sessions and the door clients
/// registration. The channels-native sessions and the door clients
/// interleave on the one registration (both shapes, one connection).
#[tokio::test]
async fn mixed_assembly_channels_native_and_door_clients_concurrent() {
@@ -967,7 +967,7 @@ async fn mixed_assembly_channels_native_and_door_clients_concurrent() {
// The door fronts (the vanilla shape) on their own connections to
// the same registration (the cred-bearing door's TokenClient takes
// its client's call connection — the door-only contract; the UDP
// its client's call connection: the door-only contract; the UDP
// door's association rides a second connection).
let tcp_listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let door_addr = tcp_listener.local_addr().expect("door addr");
@@ -1151,7 +1151,7 @@ async fn mixed_assembly_channels_native_and_door_clients_concurrent() {
/// The mixed-composed variant of the mixed-assembly pin: channels-
/// native sessions AND composed seams AND door clients on one
/// registration — the composed suite's producer + a door consumer
/// registration: the composed suite's producer + a door consumer
/// topology, both shapes live on one connection (roles-not-sides: the
/// producer serves every shape through its seams; the connection
/// direction is independent of the service direction).
@@ -1254,7 +1254,7 @@ async fn mixed_assembly_composed_seams_beside_channels_native_sessions() {
fresh.close().await;
// The handler-exit teardowns reap asynchronously (the open
// wrapper's post-handler teardown) — settle past the race, then
// wrapper's post-handler teardown); settle past the race, then
// probe (the composed suites' shape).
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(100)).await;
@@ -1264,7 +1264,7 @@ async fn mixed_assembly_composed_seams_beside_channels_native_sessions() {
}
}
// No channel leaks (the composed suites' probe; the producer side
// may retain the adopted forwarded entries — the documented
// may retain the adopted forwarded entries: the documented
// concession).
harness::assert_composed_channels(&suite.topo, &[0], &[]).await;
}
@@ -1274,7 +1274,7 @@ async fn mixed_assembly_composed_seams_beside_channels_native_sessions() {
/// own front door): a vanilla NoAuth client drives CONNECT + BIND +
/// ASSOCIATE through one `ProducerDoor` against one producer
/// registration (the `local` provider set), all three live
/// simultaneously — one accepted connection per command (the RFC
/// simultaneously, one accepted connection per command (the RFC
/// conversation is one command per accepted socket, the vanilla
/// server's shape).
#[tokio::test]
@@ -1303,7 +1303,7 @@ async fn producer_door_one_registration_three_commands() {
.expect("connect round trip");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
// BIND (live, mid-life of the CONNECT) — its own door connection.
// BIND (live, mid-life of the CONNECT): its own door connection.
let mut bind_stream = TcpStream::connect(door_addr).await.expect("door connect");
bind_stream
.write_all(&[0x05, 0x01, 0x00])
@@ -1356,9 +1356,9 @@ async fn producer_door_one_registration_three_commands() {
"reply#2 carries the real peer"
);
// ASSOCIATE (live, mid-life of both) — its own door connection:
// the sentinel reply + the in-band framed datagram through the
// real egress (the producer door path speaks the in-band wire —
// ASSOCIATE (live, mid-life of both): its own door connection.
// It gets the sentinel reply + the in-band framed datagram through
// the real egress (the producer door path speaks the in-band wire;
// the ASSOCIATE datagram plane has no door-side RFC codec here;
// the vanilla client is wrapper-aware on this path).
let mut assoc_stream = TcpStream::connect(door_addr).await.expect("door connect");
@@ -1429,7 +1429,7 @@ async fn producer_door_one_registration_three_commands() {
assert_eq!(&inner[addr_len..], b"prod-udp", "the datagram echoed");
// The teardown: the client drops; the door stays serving (the
// loop continues — the per-accept isolation); a fresh session of
// loop continues, the per-accept isolation); a fresh session of
// every command succeeds (fresh CONNECT here; the door is
// healthy).
drop(connect_stream);
@@ -1446,7 +1446,7 @@ async fn producer_door_one_registration_three_commands() {
/// The `-D` story's vanilla-client-fidelity pin (tun2proxy-shaped): a
/// domain-form CONNECT through the cred-bearing door resolves at the
/// producer's seam (the ssh `-D` semantic, ADR-012) — `localhost` is
/// producer's seam (the ssh `-D` semantic, ADR-012); `localhost` is
/// reachable as the echo target.
#[tokio::test]
async fn dash_d_domain_targets_resolve_at_the_producer_seam() {
+15 -15
View File
@@ -1,8 +1,8 @@
//! Shared test helpers — the harness shapes single-sourced for both
//! Shared test helpers: the harness shapes single-sourced for both
//! the `src/` unit suites and the integration suite (review 002 N-5;
//! the standard `mod common` pattern). The lib tests include this
//! file via `#[path = "../tests/common/mod.rs"]` (unit tests in `src/`
//! cannot import `tests/harness.rs` — separate crates); the
//! cannot import `tests/harness.rs`, since the crates are separate); the
//! integration suite reaches it through `harness`'s re-exports.
//!
//! Single-sourced here (the byte-near-identical shapes): the echo
@@ -37,9 +37,9 @@ use tokio::sync::Mutex;
// --- The dial seam stand-ins -------------------------------------------------
/// An echo dial closure that records every dialed target (the
/// interception-point probe: domain form arrives unresolved —
/// ADR-012; IP-string forms arrive as parsed IP targets). One duplex
/// per dial — the "target" echoes bytes until EOF. The producer-side
/// interception-point probe: domain form arrives unresolved
/// (ADR-012); IP-string forms arrive as parsed IP targets). One duplex
/// per dial; the "target" echoes bytes until EOF. The producer-side
/// pump gets split `DuplexStream` halves; a spawned task echoes the
/// other end. No sockets, no `local` feature.
pub fn echo_dial_witnessed(targets: Arc<Mutex<Vec<TargetAddr>>>) -> DialFn {
@@ -77,7 +77,7 @@ pub fn echo_dial() -> DialFn {
}
/// A dial that always refuses with the given [`SocksDialError`]
/// variant (the refusal-mapping tests' dial — the refusal rides
/// variant (the refusal-mapping tests' dial; the refusal rides
/// in-stream as the mapped RFC reply code). The variant is
/// reconstructed per call (the seam errors are not `Clone`).
pub fn refusing_dial(err: SocksDialError) -> DialFn {
@@ -96,9 +96,9 @@ pub fn refusing_dial(err: SocksDialError) -> DialFn {
// --- The egress seam stand-ins ------------------------------------------------
/// The queue egress behind [`echo_egress_factory`]: `send` queues the
/// inner bytes for echo (`addr + data` — the egress speaks inner
/// inner bytes for echo (`addr + data`; the egress speaks inner
/// bytes, no length prefix); `next_reply` parks on the queue
/// (`Pending` until a datagram arrives — the readiness contract,
/// (`Pending` until a datagram arrives, which is the readiness contract,
/// never an instantly-ready `Ok(None)`). The receiver rides a mutex
/// so `&mut self` methods can lock it (the shape both the harness's
/// and the producer's inline copies had converged on).
@@ -123,7 +123,7 @@ impl Egress for QueueEgress {
fn next_reply(&mut self) -> BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move {
// Real async park: `Pending` until a datagram arrives (the
// readiness contract — never an instantly-ready `Ok(None)`,
// readiness contract; never an instantly-ready `Ok(None)`,
// which would starve a biased `select!` into livelock).
match self.outbound.lock().await.recv().await {
Some(inner) => Ok(Some(inner)),
@@ -149,7 +149,7 @@ pub fn echo_egress_factory() -> EgressFactory {
}
/// A doubled-echo egress factory: every datagram echoes back **twice
/// back-to-back** (two frames queued as one reply burst — the
/// back-to-back** (two frames queued as one reply burst; the
/// producer's frame writer puts both length-prefixed frames on the
/// channel; the consumer's read may batch them into one chunk). The
/// reader-state probe for the front doors' bridge (review 003 R-1):
@@ -217,9 +217,9 @@ pub fn refusing_egress(err: EgressError) -> EgressFactory {
// --- The accept seam stand-ins -------------------------------------------------
/// A parked accept factory: the accept future never resolves (the
/// deterministic quiet-wait probe — the client-gone exit is the only
/// deterministic quiet-wait probe: the client-gone exit is the only
/// end this session can take). The advertised bind address matches
/// the harness's `ECHO_BIND_PORT` (2121 — the reply#1 carrier only).
/// the harness's `ECHO_BIND_PORT` (2121, the reply#1 carrier only).
pub fn parked_accept_factory() -> BindAcceptFactory {
Arc::new(move |_target: TargetAddr| {
Box::pin(async move {
@@ -233,7 +233,7 @@ pub fn parked_accept_factory() -> BindAcceptFactory {
// --- The raw RFC 1928 client trio ----------------------------------------------
/// Greeting (`05 01 00` — version 5, one method, no-auth) → the
/// Greeting (`05 01 00`: version 5, one method, no-auth) → the
/// method-selection read. Returns the selected method byte; asserts
/// the method-selection reply's version byte is 5.
pub async fn raw_greet<S>(stream: &mut S) -> Result<u8, std::io::Error>
@@ -271,7 +271,7 @@ where
}
/// Read one reply (the 4-byte head + the BND.ADDR per its ATYP).
/// Returns `(reply_code, atyp, addr_bytes)` — the address bytes as
/// Returns `(reply_code, atyp, addr_bytes)`: the address bytes as
/// the RFC form carries them (v4/v6: addr + port; domain: len +
/// name + port).
pub async fn raw_read_reply<S>(stream: &mut S) -> Result<(u8, u8, Vec<u8>), std::io::Error>
@@ -346,7 +346,7 @@ where
// --- The datagram frame helpers -------------------------------------------------
/// Frame one datagram the way the in-band wire carries it
/// (`[len: u16 BE][addr + data]` — the wire codec's shape).
/// (`[len: u16 BE][addr + data]`, the wire codec's shape).
pub fn datagram_frame(addr: &TargetAddr, payload: &[u8]) -> Vec<u8> {
alksocks::wire::frame(addr, payload)
.expect("frame")
+23 -23
View File
@@ -1,15 +1,15 @@
//! The composed accept integration tests (ADR-007 variant 2 — the
//! The composed accept integration tests (ADR-007 variant 2, the
//! BIND accept whose listener lives on an alktunnels 0.2.0 bind-first
//! listen resource, end to end over channels): the graduation's
//! vanilla fidelity (reply#1 = the real `bound`; reply#2 = the
//! forwarded op's real `peer` — no stand-ins), the raw pass-through
//! forwarded op's real `peer`; no stand-ins), the raw pass-through
//! data plane, the client-gone teardown (drop-witness), two concurrent
//! BIND sessions' distinct tokens, and the per-accept isolation.
//!
//! The topology: the opener (the SOCKS5 consumer side) dials the
//! transport; the tunnel producer serves BOTH the alktunnels bind-first
//! open op and the SOCKS5 open op on one registry. The forwarded op
//! serves on the opener side (post-hoc `register_forwarded_serving` —
//! serves on the opener side (post-hoc `register_forwarded_serving`,
//! the correlation registry the composed accept factories ride).
#![cfg(feature = "composed")]
@@ -54,12 +54,12 @@ async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Su
resource_registry,
composed_test_bind(),
composed_queue_loop(queue.clone()),
None, // the SOCKS5 openable registers post-hoc, below — the composed factory needs the opener's client
None, // the SOCKS5 openable registers post-hoc, below; the composed factory needs the opener's client
identity,
)
.await;
// The correlation registry rides the opener's serving registry
// (the forwarded op's serving half — the provider's opener half).
// (the forwarded op's serving half, the provider's opener half).
let registry = register_forwarded_serving(
&alkcall::channels::operations::ChannelCore::new(
topo.opener.manager().clone(),
@@ -70,7 +70,7 @@ async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Su
)
.expect("register forwarded openable");
// The SOCKS5 openable registers post-hoc (W2's shape) on the
// producer's registry — with the REAL composed factory, which
// producer's registry, with the REAL composed factory, which
// closes over the opener's client + the correlation registry (both
// only exist now).
let factory = bindfirst_accept(topo.opener.clone(), registry, COMPOSED_RESOURCE);
@@ -92,7 +92,7 @@ async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Su
}
/// Open a BIND session through the composed accept (the SOCKS5 BIND
/// client over channels — the consumer session type). The session
/// client over channels (the consumer session type). The session
/// drives the whole pipeline: open → negotiate → BIND → reply#1 (the
/// factory's real `bound`) → quiet wait (the factory's accept future)
/// → reply#2 (the real peer).
@@ -105,7 +105,7 @@ async fn bind_session(suite: &Suite) -> BindSession {
.expect("the session binds through the composed accept")
}
/// THE gate: the full composed BIND story — reply#1 carries the real
/// THE gate: the full composed BIND story. Reply#1 carries the real
/// `bound` (the graduation's vanilla fidelity), an app server "dials"
/// (the queue accept), reply#2 carries the real peer (the forwarded
/// op's `peer` param), and the pass-through data plane round-trips
@@ -119,7 +119,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
let suite = wire_suite().await;
// The BIND session rides the real composed factory (the SOCKS5
// registration's accept seam — replaced post-hoc per-suite; the
// registration's accept seam, replaced post-hoc per-suite; the
// registration reads through the shared registry per dispatch, so
// the session's open dispatches with the factory in place).
let mut session = bind_session(&suite).await;
@@ -129,7 +129,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
"the SOCKS5 channel is live on the producer side"
);
// Reply#1: the REAL bound (the open-op reply's `bound` field — no
// Reply#1: the REAL bound (the open-op reply's `bound` field, no
// expected-addr stand-in; the test bind's reported address).
let bind_addr = session.reply1().expect("reply#1").bind_addr;
assert_eq!(
@@ -138,7 +138,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
"reply#1 advertises the real OS-chosen bound"
);
// The app server dials the far listener (the queue accept) — the
// The app server dials the far listener (the queue accept); the
// producer's accept loop opens the forwarded channel toward the
// opener; the correlation registry delivers it into the accept
// future; reply#2 carries the real peer.
@@ -183,15 +183,15 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
}
// The opener side holds only channel 0 (the producer-allocated
// forwarded channel's entry rides the opener's manager per its own
// adopter contract — alktunnels ADR-005). The producer side may
// retain the adopted forwarded channel (see `assert_composed_channels`
// — its adopter's session dropped with the plane; the entry reaps
// adopter contract; alktunnels ADR-005). The producer side may
// retain the adopted forwarded channel (see `assert_composed_channels`;
// its adopter's session dropped with the plane; the entry reaps
// at transport EOF, not at the listener's).
assert_composed_channels(&suite.topo, &[0], &[channel_id]).await;
}
/// The client-gone teardown (the drop-witness): dropping the BIND
/// session mid-quiet-wait releases the far listener — the accept
/// session mid-quiet-wait releases the far listener: the accept
/// future owns it end-to-end (the quiet wait's `select!` drop IS the
/// teardown).
#[tokio::test]
@@ -208,7 +208,7 @@ async fn client_gone_teardown_releases_the_listener() {
drop(session);
// The SOCKS5 channel reaps; the listen channel (the far listener's
// carrier) reaps too — no leaked listener, no leaked channel.
// carrier) reaps too: no leaked listener, no leaked channel.
let mut socks5_gone = false;
let mut listen_gone = false;
for _ in 0..50 {
@@ -258,7 +258,7 @@ async fn concurrent_bind_sessions_get_distinct_tokens() {
let _ = s2.reply1().expect("reply#2 s2");
// Two accepts: the tokens are distinct (two listen opens minted
// distinct tokens — observably: both sessions resolve their own
// distinct tokens). Observably: both sessions resolve their own
// forwarded sessions, the correlation delivered each to the right
// waiter).
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
@@ -306,8 +306,8 @@ async fn concurrent_bind_sessions_get_distinct_tokens() {
}
/// A forwarded-open failure is per-accept isolated: an opener identity
/// without `tunnel:forwarded` makes the forwarded open fail FORBIDDEN
/// — the BIND session's accept future never resolves a forwarded
/// without `tunnel:forwarded` makes the forwarded open fail FORBIDDEN:
/// the BIND session's accept future never resolves a forwarded
/// session (reply#2's mapped refusal), the listen channel stays
/// usable, and the failure does not tear the listener down.
#[tokio::test]
@@ -316,14 +316,14 @@ async fn forwarded_open_failure_is_per_accept_isolated() {
let mut session = bind_session(&suite).await;
let _ = session.reply1().expect("reply#1");
// The accept fires (the queue accept consumed — the producer's
// The accept fires (the queue accept consumed; the producer's
// accept loop is alive), but the forwarded open fails FORBIDDEN.
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
// The forwarded-open failure is per-accept isolated AT THE TUNNEL
// LAYER: the producer's accept loop drops that accepted connection
// and keeps serving. The accept future stays parked (the SOCKS5
// producer never learns of the tunnel-layer failure — the
// producer never learns of the tunnel-layer failure (the
// quiet-wait shape is unaffected), so the queue drains both
// accepts and the listener survives (the alktunnels producer
// suite's `forwarded_open_failure_isolates_the_accept` pin, end to
@@ -340,7 +340,7 @@ async fn forwarded_open_failure_is_per_accept_isolated() {
/// The dropped listener (client gone mid-quiet-wait) leaves a
/// forwarded open nowhere to go: the correlation miss drops the
/// forwarded channel (teardown-at-birth — pinned by the dead-oneshot
/// forwarded channel (teardown-at-birth, pinned by the dead-oneshot
/// probe's aggregate-leak witness).
#[tokio::test]
async fn late_forwarded_open_after_client_gone_drops_the_channel() {
@@ -356,7 +356,7 @@ async fn late_forwarded_open_after_client_gone_drops_the_channel() {
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
tokio::time::sleep(Duration::from_millis(500)).await;
// Either the listener was already gone (the accept queued
// unconsumed) or the forwarded channel dropped at birth — either
// unconsumed) or the forwarded channel dropped at birth; either
// way: no leaked channels on the opener side.
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
+15 -15
View File
@@ -1,6 +1,6 @@
//! The composed egress integration tests (ADR-008's composed variant,
//! POC #2's `ComposedEgress` shape, end to end over channels): a
//! tunnel-producing peer (the alktunnels registry shape —
//! tunnel-producing peer (the alktunnels registry shape:
//! `register_tunnel_direct_openable` over a UDP echo dial), the
//! composed egress against it, datagram round-trips through the
//! tunnel (multiple destinations → multiple tunnels), the LRU
@@ -8,7 +8,7 @@
//! closed-queue → association-end shape, and framing-once.
//!
//! The topology: the SOCKS5-style harness (consumer calls the
//! producer's open ops) carrying alktunnels' direct op — the producer
//! producer's open ops) carrying alktunnels' direct op; the producer
//! side registers `register_tunnel_direct_openable` (the
//! alktunnels 0.2.0 registry shape), the consumer's
//! `ChannelClient` is what the composed egress rides
@@ -34,7 +34,7 @@ use alksocks::egress::{Egress, EgressError};
use alksocks::rfc1928::util::target_addr::TargetAddr;
/// An echo dial closure over the alktunnels direct-op `DialFn` shape:
/// an in-process framed UDP pipe per dial (the framed-adapter shape —
/// an in-process framed UDP pipe per dial (the framed-adapter shape:
/// the "target" decodes the tunnel codec and echoes each datagram).
/// Counts the dialed destinations (the multiple-tunnels assertion).
fn counting_udp_echo_dial(dial_count: Arc<AtomicUsize>) -> TunnelDialFn {
@@ -104,7 +104,7 @@ async fn egress_for(
}
/// A datagram round-trip through the tunnel: send to `addr`, expect
/// the echo back (the reply carries `addr + data` — inner bytes).
/// the echo back (the reply carries `addr + data`, i.e. inner bytes).
async fn round_trip(egress: &mut (dyn Egress + Send), addr: &TargetAddr, payload: &[u8]) {
egress
.send(addr.clone(), Bytes::from(payload.to_vec()))
@@ -161,8 +161,8 @@ async fn datagrams_round_trip_per_destination_multiple_tunnels() {
.expect_err("next_reply stays Pending when nothing is ready");
}
/// A fresh association (no tunnel ever opened) parks in `next_reply`
/// — the stage's select is governed by the channel branch until the
/// A fresh association (no tunnel ever opened) parks in `next_reply`:
/// the stage's select is governed by the channel branch until the
/// first datagram opens a tunnel (the assembly suite's drift finding:
/// the ended-relay `Err` fired on the never-opened table too, killing
/// every quiescent association at birth). Pinned on its own.
@@ -208,7 +208,7 @@ async fn lru_eviction_reopens_evicted_destinations() {
);
// The LRU (dest 53) was evicted: re-touching it re-dials (the
// cap stayed — no fourth tunnel above it).
// cap stayed; no fourth tunnel above it).
round_trip(egress.as_mut(), &dest(53), b"redial").await;
assert_eq!(
dial_count.load(Ordering::SeqCst),
@@ -234,7 +234,7 @@ async fn lru_eviction_reopens_evicted_destinations() {
}
/// The framing-once probe: the reply the stage would frame is exactly
/// `addr.to_be_bytes() + payload` — no double prefix. Pinned through
/// `addr.to_be_bytes() + payload`, with no double prefix. Pinned through
/// the egress's own reply (the round-trip asserts equality with the
/// inner form); here the composition is asserted at the wire boundary:
/// the egress's reply, fed through `frame_inner` (the wire layer's
@@ -281,7 +281,7 @@ async fn framing_once_replies_decode_through_the_wire_layer_exactly_once() {
/// The closed-queue → association-end shape: when the tunnel's
/// producer side ends (the dial's target EOFs), the relay task exits
/// (EOF on the read half) and — with every relay ended — the egress's
/// (EOF on the read half) and, with every relay ended, the egress's
/// `next_reply` returns `Err` (never a hot `Ok(None)`).
#[tokio::test]
async fn closed_queue_yields_err_not_a_hot_none() {
@@ -341,7 +341,7 @@ async fn closed_queue_yields_err_not_a_hot_none() {
assert_eq!(reply, Bytes::from(expected));
// The tunnel ended → the relay exited → the queue closed. The
// next `next_reply` returns `Err` (the association-end shape) —
// next `next_reply` returns `Err` (the association-end shape);
// awaited to prove it is an `Err`, not an instantly-ready
// `Ok(None)` spinning the stage.
let result = tokio::time::timeout(std::time::Duration::from_secs(10), egress.next_reply())
@@ -354,7 +354,7 @@ async fn closed_queue_yields_err_not_a_hot_none() {
);
}
/// The domain form passes through unresolved (ADR-012 — the ssh `-D`
/// The domain form passes through unresolved (ADR-012, the ssh `-D`
/// semantic): a domain destination renders its domain into the direct
/// op's target verbatim (the dial closure sees the unresolved host).
#[tokio::test]
@@ -434,14 +434,14 @@ fn composed_shapes_compile() {
/// The eviction-drives-association-end composition (review 003 D-2):
/// a cap-1 table whose only entry is evicted by a send whose
/// replacement dial FAILS — the table ends with zero entries and the
/// replacement dial FAILS: the table ends with zero entries and the
/// evicted tunnel's EOF drains its relay to `Ended`, so the parked
/// `next_reply` hits the closed-queue condition
/// (`ever_opened && live_relays == 0`) and returns the
/// association-end `Err` — the eviction path's own way to reach the
/// association-end `Err`: the eviction path's own way to reach the
/// shape `closed_queue_yields_err_not_a_hot_none` pins by far-side
/// dial. Also pinned: the count's `saturating_sub` discipline (the
/// stage keeps serving a *live* relay beside the ended one — the
/// stage keeps serving a *live* relay beside the ended one (the
/// failing send does not poison the association).
#[tokio::test]
async fn eviction_to_a_failed_dial_ends_the_association_when_the_relay_ends() {
@@ -474,7 +474,7 @@ async fn eviction_to_a_failed_dial_ends_the_association_when_the_relay_ends() {
// The association still serves while the relay is live (the
// saturating_sub discipline: a live relay keeps the stage in the
// queue branch — a second datagram round-trips on the same
// queue branch; a second datagram round-trips on the same
// tunnel).
round_trip(egress.as_mut(), &dest(53), b"again").await;
+17 -17
View File
@@ -1,4 +1,4 @@
//! The consumer UDP ASSOCIATE integration tests — `AssociateSession`
//! The consumer UDP ASSOCIATE integration tests: `AssociateSession`
//! end to end through the harness's consumer-side topology (the
//! producer's real command-dispatch path over channels; the framed
//! datagram stage, ADR-003, driven by the wrapper-aware client, POC
@@ -7,7 +7,7 @@
//! Covered: associate → sentinel reply (received, never used for
//! addressing) → send/recv round-trips (multiple in-order datagrams,
//! one association); the split-datagram persistence test (reader
//! state persists across `recv_datagram` calls — the POC's
//! state persists across `recv_datagram` calls (the POC's
//! 2-byte-prefix chunk finding); empty-payload datagram round-trip
//! (F-2, never EOF); shutdown → the producer's stage ends and a
//! fresh associate succeeds immediately; Drop teardown (drop
@@ -31,7 +31,7 @@ use harness::{echo_dial, echo_egress, refusing_egress, wire_consumer_side_with};
/// The no-leaked-channels probe (the connect/bind suites' shape,
/// re-pinned for the ASSOCIATE session): both managers hold only
/// channel 0 — the producer's entry reaps by its open-op wrapper;
/// channel 0 (the producer's entry reaps by its open-op wrapper);
/// the probe polls past the reap race.
async fn no_leaked_channels(topo: &harness::ConsumerSideTopology) {
let deadline = std::time::Duration::from_secs(5);
@@ -55,7 +55,7 @@ async fn no_leaked_channels(topo: &harness::ConsumerSideTopology) {
/// The happy path: `associate` → the reply's sentinel `BND.ADDR` is
/// received (RFC fidelity) and never used for addressing → send/recv
/// round-trips through the echo egress — multiple datagrams, in
/// round-trips through the echo egress: multiple datagrams, in
/// order, on one association; the establisher witnessed the opener
/// identity (CF-006).
#[tokio::test]
@@ -72,7 +72,7 @@ async fn associate_round_trips_multiple_datagrams_one_association() {
.await
.expect("the association associates");
// The reply's BND.ADDR is the producer's sentinel (127.0.0.1:0 —
// The reply's BND.ADDR is the producer's sentinel (127.0.0.1:0,
// ADR-003): received for RFC fidelity, never used for addressing.
let relay = assoc
.relay_addr()
@@ -120,13 +120,13 @@ async fn associate_round_trips_multiple_datagrams_one_association() {
no_leaked_channels(&topo).await;
}
/// The split-datagram persistence test (POC #2's finding — the
/// The split-datagram persistence test (POC #2's finding: the
/// reader state must persist across `recv_datagram` calls): a large
/// datagram's frame (under the u16 joint bound but well over the 16
/// KB read buffer) spans multiple channel reads AND multiple
/// `recv_datagram` calls — the first call returns the earlier small
/// `recv_datagram` calls; the first call returns the earlier small
/// datagram while the large one is mid-flight, the next call
/// reassembles it across its internal reads — and a trailing small
/// reassembles it across its internal reads, and a trailing small
/// datagram batched in the same read batch drains through the
/// pending queue.
#[tokio::test]
@@ -139,7 +139,7 @@ async fn recv_datagram_state_persists_across_calls() {
let target = TargetAddr::Ip("10.0.0.2:53".parse().unwrap());
// The order tells the story: "first" completes early; the 60 KiB
// datagram (60007 inner bytes — legal, but over the 16 KB read
// datagram (60007 inner bytes, legal but over the 16 KB read
// buffer) is mid-flight when the first call returns; "third"
// batches after it.
assoc
@@ -166,7 +166,7 @@ async fn recv_datagram_state_persists_across_calls() {
assert_eq!(&first[..], b"first", "the first datagram, in order");
// The large datagram reassembles across the call boundary (its
// mid-frame state persisted into this call) — the POC finding's
// mid-frame state persisted into this call), the POC finding's
// shape.
let (src, second) =
tokio::time::timeout(std::time::Duration::from_secs(10), assoc.recv_datagram())
@@ -192,7 +192,7 @@ async fn recv_datagram_state_persists_across_calls() {
no_leaked_channels(&topo).await;
}
/// An empty-payload datagram is a legal datagram — never EOF (the
/// An empty-payload datagram is a legal datagram (never EOF; the
/// F-2 invariant, end to end through the echo egress), and the
/// association survives it (pre → empty → post, in order).
#[tokio::test]
@@ -250,7 +250,7 @@ async fn empty_payload_datagram_round_trips_and_survives() {
/// Shutdown half-closes the stream: the mux's implicit-EOF sentinel
/// reaches the producer's stage as EOF and the association ends
/// (association lifetime = channel lifetime) — and a fresh associate
/// (association lifetime = channel lifetime), and a fresh associate
/// succeeds immediately after (the POC's test 4, from the consumer
/// side; the same registration is unpoisoned).
#[tokio::test]
@@ -304,7 +304,7 @@ async fn shutdown_ends_the_association_and_a_fresh_associate_succeeds() {
/// Drop teardown: dropping a live association mid-stream reaps the
/// adopted channel entry (no leak), the producer's stage sees the
/// structural EOF (the mux's implicit-EOF sentinel on the dropped
/// write half), and the connection stays healthy — a fresh associate
/// write half), and the connection stays healthy: a fresh associate
/// succeeds immediately after.
#[tokio::test]
async fn drop_teardown_reaps_and_connection_stays_healthy() {
@@ -355,7 +355,7 @@ async fn drop_teardown_reaps_and_connection_stays_healthy() {
}
/// A missing egress seam: the ASSOCIATE refusal surfaces the RFC
/// reply code through the session type (`CommandNotSupported` 0x07 —
/// reply code through the session type (`CommandNotSupported` 0x07,
/// ADR-008's missing-callback class), and no phantom channel remains
/// on either side (teardown-at-birth).
#[tokio::test]
@@ -364,7 +364,7 @@ async fn missing_egress_refusal_surfaces_rfc_reply_code_no_phantom_channel() {
let err = AssociateSession::associate(topo.consumer.as_ref())
.await
.expect_err("no egress wired — the ASSOCIATE must refuse");
.expect_err("no egress wired; the ASSOCIATE must refuse");
match &err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => {
@@ -396,7 +396,7 @@ async fn missing_egress_refusal_surfaces_rfc_reply_code_no_phantom_channel() {
}
/// An egress-factory failure: the mapped RFC reply code surfaces from
/// `associate` (reply#1's code — the factory refused before the
/// `associate` (reply#1's code, since the factory refused before the
/// reply), and no phantom channel remains.
#[tokio::test]
async fn egress_factory_refusal_surfaces_the_mapped_reply_code() {
@@ -423,7 +423,7 @@ async fn egress_factory_refusal_surfaces_the_mapped_reply_code() {
}
/// CONNECT + ASSOCIATE on the same registration (the regression
/// guard extended): one ALPN, both commands, interleaved — an
/// guard extended): one ALPN, both commands, interleaved: an
/// association and a CONNECT session live concurrently on the same
/// registration, both serving.
#[tokio::test]
+23 -23
View File
@@ -1,4 +1,4 @@
//! The consumer BIND integration tests — `BindSession` end to end
//! The consumer BIND integration tests: `BindSession` end to end
//! through the harness's consumer-side topology (the producer's real
//! command-dispatch path over channels; the BIND phase model, ADR-007,
//! driven by the wrapper-aware client, POC #5's `read_reply2` shape).
@@ -27,7 +27,7 @@ use harness::{
};
/// Both scopes: the open gate (`socks5:open`) AND the command gate
/// (`socks5:bind`) — a BIND session needs both grants explicitly
/// (`socks5:bind`); a BIND session needs both grants explicitly
/// (ADR-006: never implied).
fn bind_scoped() -> alkcall::core::auth::Identity {
harness::both_scopes_identity()
@@ -35,7 +35,7 @@ fn bind_scoped() -> alkcall::core::auth::Identity {
/// The no-leaked-channels probe (the consumer-connect suite's shape,
/// re-pinned for the BIND sessions): both managers hold only channel
/// 0 — the producer's entry reaps by its open-op wrapper; the probe
/// 0 (the producer's entry reaps by its open-op wrapper; the probe
/// polls past the reap race.
async fn no_leaked_channels(topo: &harness::ConsumerSideTopology) {
let deadline = std::time::Duration::from_secs(5);
@@ -67,9 +67,9 @@ fn reply_code_of(err: &SocksSessionError) -> Option<u8> {
}
/// The happy path: `bind` → reply#1 carries the harness accept's
/// advertised BND.ADDR (`ECHO_BIND_PORT` — the reply#1 carrier); the
/// advertised BND.ADDR (`ECHO_BIND_PORT`, the reply#1 carrier); the
/// app server's out-of-band dial shape is the `BindReply1` handle;
/// `await_inbound` reads reply#2 (the peer address, 10.0.0.7:44444 —
/// `await_inbound` reads reply#2 (the peer address, 10.0.0.7:44444,
/// the harness's fixed stand-in); the data plane two-pumps raw bytes
/// both directions; `close` reaps the adopted channel.
#[tokio::test]
@@ -135,7 +135,7 @@ async fn bind_round_trip_reply1_reply2_then_pass_through() {
/// The `BindDataPlane` accessors: `into_inner` yields the owned
/// forked client type (read/write rides it directly); the session
/// still owns the teardown — `close` after the plane is drained reaps
/// still owns the teardown; `close` after the plane is drained reaps
/// the entry.
#[tokio::test]
async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
@@ -167,7 +167,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
.expect("reply#2 read")
.expect("the plane");
// `get_socket_mut` is the borrowed socket accessor (the POC
// shape) — the same stream, reached through the plane.
// shape): the same stream, reached through the plane.
plane
.get_socket_mut()
.write_all(b"raw")
@@ -190,7 +190,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
}
/// The command gate (ADR-006): a `socks5:open`-only identity cannot
/// bind — the refusal surfaces as a typed session error carrying the
/// bind; the refusal surfaces as a typed session error carrying the
/// RFC reply code (`ConnectionNotAllowed` 0x02), and no phantom
/// channel remains on either side.
#[tokio::test]
@@ -222,7 +222,7 @@ async fn bind_command_gate_refusal_is_a_typed_reply_code_error() {
}
/// Factory failure (pre-reply#1) maps to reply#1's code, surfaced
/// through the session type — no session lives past the refusal, no
/// through the session type: no session lives past the refusal, no
/// phantom listener.
#[tokio::test]
async fn bind_factory_failure_surfaces_reply1_code() {
@@ -251,7 +251,7 @@ async fn bind_factory_failure_surfaces_reply1_code() {
no_leaked_channels(&topo).await;
}
/// Accept-stage failure (post-reply#1) maps to reply#2's code —
/// Accept-stage failure (post-reply#1) maps to reply#2's code,
/// surfaced at `await_inbound`, no data plane; the session's channel
/// is still owned (close/drop reaps it).
#[tokio::test]
@@ -271,7 +271,7 @@ async fn bind_accept_stage_failure_surfaces_reply2_code() {
("accept.fail", ECHO_BIND_PORT).to_target_addr().unwrap(),
)
.await
.expect("reply#1 succeeded — the factory resolved");
.expect("reply#1 succeeded; the factory resolved");
let err = match session.await_inbound().await {
Err(err) => err,
@@ -285,13 +285,13 @@ async fn bind_accept_stage_failure_surfaces_reply2_code() {
);
// The channel entry stays adopted (reply#1 succeeded; the session
// owns it until the app closes it) — close reaps it.
// owns it until the app closes it); close reaps it.
assert!(session.close().await, "the entry existed");
no_leaked_channels(&topo).await;
}
/// Quiet-wait garbage from the client side: a control byte after
/// reply#1 is protocol garbage — the producer ends the session with
/// reply#1 is protocol garbage: the producer ends the session with
/// no data plane, so the consumer's `await_inbound` read EOFs (an I/O
/// error, not a reply#2).
#[tokio::test]
@@ -351,7 +351,7 @@ async fn bind_quiet_wait_garbage_from_the_client_ends_the_session() {
/// The three-command regression guard from the consumer side (POC #5
/// test 7's consumer-side mirror): one registration, all three
/// commands served through their consumer sessions — CONNECT
/// commands served through their consumer sessions. CONNECT
/// (`Socks5ChannelSession`), BIND (`BindSession`), ASSOCIATE rides
/// its task; here CONNECT + BIND coexist on one registration with the
/// both-scopes identity, fresh channel per session.
@@ -420,7 +420,7 @@ async fn bind_and_connect_serve_on_the_same_registration() {
}
/// Drop teardown: dropping a live BIND session mid-pipe reaps the
/// adopted channel entry (no leak) and the connection stays healthy —
/// adopted channel entry (no leak) and the connection stays healthy;
/// a fresh bind succeeds immediately after (the CONNECT session's
/// drop posture, re-pinned for BIND).
#[tokio::test]
@@ -445,7 +445,7 @@ async fn bind_drop_teardown_reaps_and_connection_stays_healthy() {
TargetAddr::Ip(std::net::SocketAddr::from(([127, 0, 0, 1], ECHO_BIND_PORT)))
);
// Drop mid-pipe (before reply#2 — the quiet-wait phase).
// Drop mid-pipe (before reply#2, the quiet-wait phase).
drop(session);
no_leaked_channels(&topo).await;
@@ -489,7 +489,7 @@ fn bind_session_type_is_send() {
}
/// The C-1 re-entry guard (review 002): a second `await_inbound` on
/// the same session cannot re-read the data plane — it returns the
/// the same session cannot re-read the data plane; it returns the
/// spent shape (`Ok(None)`), and the data-plane bytes survive
/// untouched (the first plane's echo round-trip is intact after the
/// second call; the pinning test for the silent-corruption path).
@@ -510,7 +510,7 @@ async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
.await
.expect("the session binds");
// The first read: the plane exists (and consumes the phase —
// The first read: the plane exists (and consumes the phase:
// the plane owns the stream; the session stays the teardown
// owner).
let mut plane = session
@@ -531,7 +531,7 @@ async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
assert_eq!(&buf, b"ping", "the first plane's round-trip");
drop(plane);
// The second call: the spent shape — `Ok(None)`, never a
// The second call: the spent shape (`Ok(None)`, never a
// re-read of the live data plane.
let second = session
.await_inbound()
@@ -540,8 +540,8 @@ async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
assert!(second.is_none(), "the phase was consumed");
// The data-plane bytes survive: the first plane's echo
// round-trip was complete and no re-read corrupted the pipe —
// the channel is closed with the reap (the session owner's
// round-trip was complete and no re-read corrupted the pipe; the
// channel is closed with the reap (the session owner's
// `close`), and a fresh bind on the same registration
// round-trips with its own bytes intact.
assert!(session.close().await, "the entry existed");
@@ -575,8 +575,8 @@ async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
}
/// The `reply1()`-twice re-entry path (C-1's other door, closed by
/// the D-1 one-reader decision): `reply1()` is a view — fresh handles
/// per call, no read phase — so repeated `reply1()` calls carry the
/// the D-1 one-reader decision): `reply1()` is a view, with fresh
/// handles per call and no read phase, so repeated `reply1()` calls carry the
/// same advertised address without consuming anything, and the one
/// normative reader (`await_inbound`) still yields exactly one plane.
#[tokio::test]
+9 -9
View File
@@ -1,4 +1,4 @@
//! The consumer-half integration tests — `Socks5ChannelSession`
//! The consumer-half integration tests: `Socks5ChannelSession`
//! (CONNECT) end to end through the harness's consumer-side topology
//! (the producer's real command-dispatch path over channels; the
//! alkcall 0.8.0 establishment surface consumed from the consumer's
@@ -6,7 +6,7 @@
//!
//! Covered: connect → echo round-trip (payload bytes both
//! directions); the dial-witness target forms (IP-string parses to an
//! IP target, domain passes through unresolved — ADR-012); refused
//! IP target, domain passes through unresolved (ADR-012)); refused
//! dial → the RFC reply code surfaced as a typed session error; the
//! scope-less FORBIDDEN open → typed open error, no session; drop
//! teardown (no leaked channel entries, the connection stays
@@ -45,7 +45,7 @@ fn expect_forbidden(err: &SocksSessionError) -> bool {
}
/// A dial that always refuses (the refusal-riding-in-stream probe's
/// dial — the RFC reply code must surface through the session type,
/// dial; the RFC reply code must surface through the session type,
/// not as an open failure).
fn refusing_session_dial() -> alksocks::egress::DialFn {
refusing_dial(SocksDialError::Refused("target down".into()))
@@ -53,7 +53,7 @@ fn refusing_session_dial() -> alksocks::egress::DialFn {
/// Both managers hold only channel 0 (the phantom-channel property,
/// asserted from both sides). The producer's entry is reaped by its
/// open-op wrapper when the pump handler exits — an await-able
/// open-op wrapper when the pump handler exits (an await-able
/// teardown that races the consumer-side reap; this probe polls past
/// the race instead of asserting instantly.
async fn no_leaked_channels(topo: &ConsumerSideTopology) {
@@ -160,7 +160,7 @@ async fn target_construction_ip_strings_parse_domains_pass_through() {
}
/// A refused dial: the RFC reply code surfaces through the session
/// type (`SocksSessionError::Rfc` — the in-stream refusal, never an
/// type (`SocksSessionError::Rfc`, the in-stream refusal, never an
/// open failure), and no phantom channel remains on either side.
#[tokio::test]
async fn refused_dial_surfaces_rfc_reply_code_no_phantom_channel() {
@@ -171,7 +171,7 @@ async fn refused_dial_surfaces_rfc_reply_code_no_phantom_channel() {
("down.test", 80u16).to_target_addr().unwrap(),
)
.await
.expect_err("the dial refused — the reply code rides the session error");
.expect_err("the dial refused; the reply code rides the session error");
match &err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => {
@@ -197,7 +197,7 @@ async fn refused_dial_surfaces_rfc_reply_code_no_phantom_channel() {
}
/// A scope-less identity: the registry's FORBIDDEN open failure is
/// the typed `SocksOpenError` through `SocksSessionError::Open` —
/// the typed `SocksOpenError` through `SocksSessionError::Open`;
/// no session, no phantom channel (the open gate is the boundary).
#[tokio::test]
async fn forbidden_open_is_a_typed_open_error_with_no_session() {
@@ -223,7 +223,7 @@ async fn forbidden_open_is_a_typed_open_error_with_no_session() {
}
/// Drop teardown: dropping a live session mid-pipe reaps the adopted
/// channel entry (no leak) and the connection stays healthy — a
/// channel entry (no leak) and the connection stays healthy: a
/// fresh connect succeeds immediately after.
#[tokio::test]
async fn drop_teardown_reaps_and_connection_stays_healthy() {
@@ -282,7 +282,7 @@ async fn drop_teardown_reaps_and_connection_stays_healthy() {
/// Half-close semantics: shutting the write half EOFs the producer's
/// pump cleanly (the EOF sentinel), while the read half stays alive
/// until the target's data flows. The pump's completion is the
/// producer-side evidence (the handler tracks the data plane — R-02).
/// producer-side evidence (the handler tracks the data plane; R-02).
#[tokio::test]
async fn half_close_write_eof_ends_the_producer_pump() {
let topo = wire_consumer_side(echo_dial()).await;
+21 -21
View File
@@ -1,4 +1,4 @@
//! The cross-cutting integration suite — the whole protocol core's
//! The cross-cutting integration suite: the whole protocol core's
//! behavior pinned in one place (the batch-1 closing task; the
//! architecture-sync check's test half).
//!
@@ -11,7 +11,7 @@
//! served on the same `channels/socks5/sub` op, interleaved live
//! sessions on one channels connection.
//! - **Concurrency**: mixed concurrent sessions over one channels
//! connection — the mux/demux under load (the POCs tested
//! connection (the mux/demux under load; the POCs tested
//! sequentially), pinned in both allocation directions (the raw
//! topology where the producer's serving side allocates the channel
//! ids, and the consumer-side topology where the consumer does).
@@ -23,7 +23,7 @@
//! - **The identity/scope matrix** (ADR-006, end to end):
//! `socks5:open`-only (CONNECT + ASSOCIATE serve, BIND refused 0x02
//! at the command gate), `socks5:bind`-only (fail-closed at the
//! open gate — all three commands; ADR-006's open gate requires
//! open gate, all three commands; ADR-006's open gate requires
//! `socks5:open` unconditionally and the two grants are never
//! implied), both scopes (everything, granted explicitly).
//! - **The readiness contract** re-pinned under concurrent load (the
@@ -252,13 +252,13 @@ use harness::{parked_accept_factory as parked_accept, udp_frame, udp_unframe};
// --- Seam stand-ins ----------------------------------------------------------
// The parked accept factory rides the common module (imported as
// `parked_accept` above — the deterministic quiet-wait probe; the
// `parked_accept` above, the deterministic quiet-wait probe; the
// advertised bind address is the common module's 2121).
/// The hot egress (the readiness contract's violator, the
/// producer-associate unit test's shape): `next_reply` drains one
/// queued reply if any, then returns an instantly-ready `Ok(None)`
/// instead of parking. The stage must tolerate this under load — a
/// instead of parking. The stage must tolerate this under load: a
/// biased select starved into livelock would hang every session here.
struct HotNoneEgress {
echo: tokio::sync::mpsc::UnboundedSender<bytes::Bytes>,
@@ -310,7 +310,7 @@ fn hot_none_egress() -> EgressFactory {
/// The no-leaked-channels probe (the per-command suites' shape): both
/// managers hold only channel 0; the producer's entry reaps by its
/// open-op wrapper — the probe polls past the reap race.
/// open-op wrapper; the probe polls past the reap race.
async fn no_leaked_channels(topo: &ConsumerSideTopology) {
let deadline = std::time::Duration::from_secs(5);
let consumer_ids = topo.consumer.manager().channel_ids();
@@ -469,14 +469,14 @@ async fn consumer_bind_round_trip(
/// one channels connection (the POC #2/#5 regression guard,
/// complete): all three sessions are opened back to back and are live
/// simultaneously; each is exercised mid-life of the others; a
/// teardown ends only its own session (the others keep serving — the
/// teardown ends only its own session (the others keep serving, the
/// mux/demux isolation).
#[tokio::test]
async fn one_registration_all_three_commands_interleaved() {
let topo = full_raw_topology().await;
// Three interleaved opens on the one op; all three sessions live
// at once (CONNECT, ASSOCIATE, BIND — fresh channel per session,
// at once (CONNECT, ASSOCIATE, BIND: fresh channel per session,
// convention 10).
let (mut connect_raw, connect_pump) = open_adopt_pump(&topo).await;
let (mut assoc_raw, assoc_pump) = raw_associate(&topo).await;
@@ -551,8 +551,8 @@ async fn one_registration_all_three_commands_interleaved() {
}
/// Concurrency over one channels connection (the mux/demux under
/// load): nine concurrent sessions of mixed command types — three
/// CONNECTs, three ASSOCIATEs, three BINDs — interleaved by the
/// load): nine concurrent sessions of mixed command types, three
/// CONNECTs, three ASSOCIATEs, three BINDs, interleaved by the
/// executor, all riding the one registration. The producer's serving
/// side allocates the channel ids (the raw topology's direction).
#[tokio::test]
@@ -588,7 +588,7 @@ async fn concurrent_mixed_sessions_over_one_connection() {
/// connection: six concurrent sessions (two of each command) driven
/// by the real session types. The consumer dials here (the
/// consumer-side topology), so the consumer's side allocates the
/// channel ids — the mux/demux pinned in the other allocation
/// channel ids (the mux/demux pinned in the other allocation
/// direction. No channel entry leaks on either side.
#[tokio::test]
async fn concurrent_consumer_sessions_over_one_connection() {
@@ -627,7 +627,7 @@ async fn concurrent_consumer_sessions_over_one_connection() {
/// The teardown matrix, CONNECT row (wire.md §Sentinels and
/// lifecycle): a client half-close ends the data plane cleanly (the
/// producer's pump exits) and a fresh session of every command
/// succeeds immediately after — no connection poisoning.
/// succeeds immediately after (no connection poisoning).
#[tokio::test]
async fn teardown_connect_half_close_then_every_command_serves_fresh() {
let topo = full_raw_topology().await;
@@ -737,7 +737,7 @@ async fn teardown_associate_eof_then_every_command_serves_fresh() {
/// The teardown matrix, BIND row: the client vanishing in the quiet
/// wait releases the accept (the parked accept future drops with the
/// `select!` scope — the client-gone teardown, ADR-004/ADR-007) and
/// `select!` scope: the client-gone teardown, ADR-004/ADR-007) and
/// fresh sessions succeed immediately after.
#[tokio::test]
async fn teardown_bind_client_gone_releases_the_accept_then_serves_fresh() {
@@ -799,7 +799,7 @@ async fn teardown_bind_client_gone_releases_the_accept_then_serves_fresh() {
/// The identity/scope matrix, `socks5:open`-only row (ADR-006):
/// CONNECT and ASSOCIATE serve; BIND is refused `ConnectionNotAllowed`
/// (0x02) at the command gate while CONNECT still serves the same
/// identity — ingress is never implied by egress.
/// identity; ingress is never implied by egress.
#[tokio::test]
async fn identity_matrix_open_only_connect_associate_serve_bind_refused_0x02() {
let topo = wire_with(
@@ -824,7 +824,7 @@ async fn identity_matrix_open_only_connect_associate_serve_bind_refused_0x02() {
.await
.expect("ASSOCIATE serves the open-only identity");
// BIND: the command gate refuses 0x02 — RFC-faithful, no data
// BIND: the command gate refuses 0x02, RFC-faithful; no data
// plane (the accept seam never ran).
let (mut raw, pump) = open_adopt_pump(&topo).await;
raw.greet().await.expect("greet");
@@ -858,7 +858,7 @@ async fn identity_matrix_open_only_connect_associate_serve_bind_refused_0x02() {
/// The identity/scope matrix, `socks5:bind`-only row (ADR-006, as
/// landed): the open op's `required_scopes` is `socks5:open`
/// unconditionally — a bind-only identity fails closed at the open
/// unconditionally, so a bind-only identity fails closed at the open
/// gate for all three commands (FORBIDDEN; the establisher never
/// runs; no channel anywhere). `socks5:bind` alone grants nothing
/// here; the two grants are never implied, in either direction.
@@ -899,7 +899,7 @@ async fn identity_matrix_bind_only_fails_closed_at_the_open_gate() {
.all(|id| id == 0));
assert!(
topo.identity_witness.lock().await.is_none(),
"the establisher never ran — fail-closed"
"the establisher never ran; fail-closed"
);
// The same posture through the typed consumer sessions: the BIND
@@ -950,8 +950,8 @@ async fn identity_matrix_both_scopes_serve_every_command() {
/// The readiness contract under concurrent load (the integration
/// level pin; the unit-level test is the producer-associate task's):
/// a hot `Ok(None)` egress across concurrent associations — with a
/// CONNECT interleaved — must not starve the datagram stage's channel
/// a hot `Ok(None)` egress across concurrent associations (with a
/// CONNECT interleaved) must not starve the datagram stage's channel
/// branch. Datagrams still egress and echo.
#[tokio::test]
async fn readiness_contract_holds_under_concurrent_load() {
@@ -986,7 +986,7 @@ async fn readiness_contract_holds_under_concurrent_load() {
}
/// The spec'd assembly shape end to end (review 002 D-2): the full
/// `Socks5Producer::builder()` chain — `dial` + `egress` + `accept` —
/// `Socks5Producer::builder()` chain (`dial` + `egress` + `accept`)
/// through `.register(...)` (the builder's delegated registration, not
/// the free function), serving all three commands over the harness
/// topology. The one assembly path producer.md §Wiring documents is
@@ -1018,7 +1018,7 @@ async fn producer_builder_registers_and_serves_all_three_commands() {
.expect("ASSOCIATE serves through the builder registration");
// BIND over the builder's accept seam (reply#1 + reply#2 +
// pass-through — the full phase model).
// pass-through: the full phase model).
bind_round_trip(&topo, "builder.test", b"bind")
.await
.expect("BIND serves through the builder registration");
+30 -30
View File
@@ -29,7 +29,7 @@ use harness::consumer_identity;
const BOUND: Duration = Duration::from_secs(10);
/// The token→identity provider the cred-bearing door's producer runs
/// (CF-005 precedence (1)): the token is the mapped identity's id —
/// (CF-005 precedence (1)): the token is the mapped identity's id,
/// the resolution seam's vocabulary; the password never rides it.
/// Resolves the credential-table identity ids (the `door-` prefixed
/// forms the tests configure) to identities carrying `socks5:open`
@@ -63,7 +63,7 @@ impl IdentityProvider for DoorTokenProvider {
}
/// An echo TCP server on an ephemeral loopback port (the vanilla
/// client's CONNECT target — the POC #1 curl shape).
/// client's CONNECT target, the POC #1 curl shape).
async fn echo_tcp_server() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local_addr");
@@ -105,14 +105,14 @@ impl VanillaClient {
}
/// The dial without the panic (the `serve` wrapper's bind races
/// the test's connect — the retry loop's probe).
/// the test's connect, the retry loop's probe).
async fn try_dial(addr: SocketAddr) -> std::io::Result<Self> {
Ok(Self {
stream: TcpStream::connect(addr).await?,
})
}
/// Greeting `05 02 00` (password auth offered — the cred-bearing
/// Greeting `05 02 00` (password auth offered, the cred-bearing
/// door's shape) → the method-selection read.
async fn greet_password(&mut self) -> u8 {
self.stream
@@ -217,7 +217,7 @@ impl VanillaClient {
/// The NoAuth door end to end (the POC #1 curl shape): a vanilla RFC
/// 1928 client through the raw pipe → CONNECT round-trip to a real
/// TCP echo target. Zero protocol translation pinned: the door adds
/// nothing — the producer runs the whole RFC conversation through the
/// nothing; the producer runs the whole RFC conversation through the
/// pipe (the vanilla client's greeting crosses it verbatim).
#[tokio::test]
async fn noauth_door_raw_pipe_vanilla_connect_round_trip() {
@@ -226,7 +226,7 @@ async fn noauth_door_raw_pipe_vanilla_connect_round_trip() {
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
// The door takes a pre-bound listener (`serve_on`), so the test
// owns the port (the assembly seam — the same accept loop without
// owns the port (the assembly seam: the same accept loop without
// the door's own bind).
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
@@ -339,7 +339,7 @@ async fn cred_door_good_creds_connect_round_trip_with_mapped_identity() {
assert_eq!(&buf, b"cred-echo");
// The establisher witness saw the mapped identity (CF-006 via the
// token resolution — CF-005 precedence (1)), never the password.
// token resolution, CF-005 precedence (1)), never the password.
timeout(BOUND, async {
loop {
let seen = topo.identity_witness.lock().await.clone();
@@ -355,7 +355,7 @@ async fn cred_door_good_creds_connect_round_trip_with_mapped_identity() {
/// The cred-bearing door: bad creds → the RFC 1929 rejection (status
/// 0xff), and no channel opened (the identity witness never fires;
/// the consumer's manager holds only channel 0 — the ACL boundary
/// the consumer's manager holds only channel 0 (the ACL boundary
/// held door-side).
#[tokio::test]
async fn cred_door_bad_creds_rejected_no_channel_opened() {
@@ -429,12 +429,12 @@ async fn cred_door_bad_creds_rejected_no_channel_opened() {
/// The pin: a vanilla client's command bytes must not reach the
/// producer as greeting bytes (the door never pipes raw post-auth).
/// The producer's in-band conversation is re-originated — the
/// The producer's in-band conversation is re-originated: the
/// producer's `accept_no_auth` reads a fresh greeting written by the
/// *consumer session* (`Socks5Stream::use_stream`), not the vanilla
/// client's consumed bytes. Observable as: the cred-bearing CONNECT
/// round-trip works at all (raw post-auth piping would misparse the
/// command bytes as a greeting — the producer would reply 0xff-shaped
/// command bytes as a greeting (the producer would reply 0xff-shaped
/// garbage), and the witness carries the mapped identity (the
/// password never crossed).
#[tokio::test]
@@ -478,8 +478,8 @@ async fn cred_door_never_pipes_raw_post_auth() {
}
});
// The vanilla client speaks greeting → password-auth → command —
// if the door piped these bytes raw post-auth, the producer's
// The vanilla client speaks greeting → password-auth → command.
// If the door piped these bytes raw post-auth, the producer's
// in-band greeting read would consume `05 01 00 01 ...` (the
// command request) as a method list and the conversation would
// fail. The round-trip succeeding IS the pin (the re-origination
@@ -507,7 +507,7 @@ async fn cred_door_never_pipes_raw_post_auth() {
}
/// The BIND door (cred-bearing variant): a vanilla BIND conversation
/// through the door — reply#1's advertised address passes through
/// through the door; reply#1's advertised address passes through
/// unchanged (the door adds nothing), reply#2 carries the real peer,
/// and the data plane round-trips (the POC #5 door shape).
#[tokio::test]
@@ -565,7 +565,7 @@ async fn bind_door_vanilla_conversation_reply_fidelity() {
.expect("creds");
assert_eq!(status, 0x00, "accepted");
// BIND request: 127.0.0.1:0 (ephemeral — the local accept
// BIND request: 127.0.0.1:0 (ephemeral; the local accept
// provider binds the real listener).
let (code1, advertised) = timeout(BOUND, vanilla.bind_v4([127, 0, 0, 1], 0))
.await
@@ -617,7 +617,7 @@ async fn bind_door_vanilla_conversation_reply_fidelity() {
/// The in-band refusal's reply code is re-encoded to the vanilla
/// client (the RFC codec at the door boundary): an identity carrying
/// `socks5:open` only issues BIND through the cred-bearing door —
/// `socks5:open` only issues BIND through the cred-bearing door;
/// the in-band command gate refuses 0x02, and the door re-encodes
/// that code as reply#1 (never a bare EOF; the refused reply carries
/// the placeholder zero address).
@@ -672,8 +672,8 @@ async fn cred_door_in_band_refusal_surfaces_the_reply_code() {
assert_eq!(status, 0x00, "accepted");
// BIND through the open-only identity: the in-band command gate's
// 0x02 rides the door's re-encoded reply (the placeholder BND.ADDR
// — a refused reply carries a zero address).
// 0x02 rides the door's re-encoded reply (the placeholder BND.ADDR:
// a refused reply carries a zero address).
let (code, advertised) = timeout(BOUND, vanilla.bind_v4([127, 0, 0, 1], 0))
.await
.expect("the refusal reply arrives (never a bare EOF)");
@@ -746,7 +746,7 @@ async fn idle_first_byte_timeout_closes_a_silent_connection() {
}
/// The session cap (ADR-014 knob 2): an accept beyond the bound is
/// refused door-side — no channel opened for the refused accept, and
/// refused door-side: no channel opened for the refused accept, and
/// the door keeps serving (the loop continues).
#[tokio::test]
async fn session_cap_refuses_beyond_the_bound() {
@@ -774,7 +774,7 @@ async fn session_cap_refuses_beyond_the_bound() {
let mut holder = TcpStream::connect(addr).await.expect("first dial");
let _ = &mut holder;
// Session 2: beyond the cap — refused. The socket closes without
// Session 2: beyond the cap, so refused. The socket closes without
// a reply (the door-side refusal; no RFC bytes ever flow).
let refused = TcpStream::connect(addr).await.expect("second dial");
let mut probe = [0u8; 1];
@@ -789,11 +789,11 @@ async fn session_cap_refuses_beyond_the_bound() {
Ok(Ok(n)) => panic!("no bytes for a beyond-cap accept, got {n}"),
}
// Exactly one channel exists (the holder's — opened when it went
// Exactly one channel exists (the holder's, opened when it went
// silent-past... wait, no: the holder sent no bytes either, so the
// idle timeout governs it; the cap test needs the holder to stay
// within the window while the second dial arrives. The holder's
// channel opens only after its first byte — silence keeps it
// channel opens only after its first byte (silence keeps it
// channel-less. Assert: no channels at all for either connection
// (both silent), and the door is still serving (a fresh dial
// succeeds).
@@ -814,7 +814,7 @@ async fn session_cap_refuses_beyond_the_bound() {
}
/// Per-connection failure isolation (the alktunnels per-accept
/// posture): one dead client doesn't kill the door — a client that
/// posture): one dead client doesn't kill the door. A client that
/// speaks garbage (an invalid greeting) gets its connection ended,
/// and the door keeps serving fresh clients end to end.
#[tokio::test]
@@ -839,7 +839,7 @@ async fn per_connection_failure_isolation() {
});
// Client A: protocol garbage (a bad version byte through the raw
// pipe — the producer's typestate refuses in-band; the door's
// pipe; the producer's typestate refuses in-band, and the door's
// pipe ends with the session).
let mut garbage = VanillaClient::dial(addr).await;
garbage.stream.write_all(&[0x04, 0x01, 0x00]).await.unwrap();
@@ -869,9 +869,9 @@ async fn per_connection_failure_isolation() {
/// The channels-native sessions still work beside the door (the
/// regression guard: the door's `TokenClient::wrap` took the client's
/// call connection — a NoAuth door never wraps, so the client's own
/// call connection: a NoAuth door never wraps, so the client's own
/// sessions keep serving; a cred-bearing door's client is door-only
/// by design — the wrap's contract, asserted here at the surface
/// by design (the wrap's contract, asserted here at the surface
/// level via a NoAuth topology).
#[tokio::test]
async fn native_sessions_work_beside_a_noauth_door() {
@@ -970,7 +970,7 @@ async fn bind_session_with_token_round_trip() {
/// The ASSOCIATE token twin driven directly (review 003 D-1: the
/// `associate_with_token` constructor is the session family's
/// completeness twin — no door calls it, so it needs its own probe;
/// completeness twin: no door calls it, so it needs its own probe;
/// the same shape as `bind_session_with_token_round_trip`): the
/// open rides the payload `auth_token` (CF-005 precedence (1)),
/// the ASSOCIATE's DST.ADDR is the unspecified address (the
@@ -1027,8 +1027,8 @@ async fn associate_session_with_token_round_trip() {
session.shutdown().await.expect("shutdown");
}
/// The `serve()` wrapper (review 003 N-8: every test drove `serve_on`
/// — the bind-then-serve wrapper round-trips too; the NoAuth raw-pipe
/// The `serve()` wrapper (review 003 N-8: the other tests all drove
/// `serve_on`; the bind-then-serve wrapper round-trips too; the NoAuth raw-pipe
/// shape, the port probed up front so the wrapper's own bind lands on
/// it).
#[tokio::test]
@@ -1038,7 +1038,7 @@ async fn front_door_serve_wrapper_round_trips() {
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
// A free loopback port, probed up front (the door binds exactly
// it — the wrapper's own bind decision). The connect retries:
// it, the wrapper's own bind decision). The connect retries:
// the wrapper's bind races the probe's teardown (the OS may hold
// the freed port briefly).
let mut addr = None;
@@ -1088,7 +1088,7 @@ async fn front_door_serve_wrapper_round_trips() {
}
/// The credential table's observability accessors (review 003 N-8:
/// `len`/`is_empty` had no caller — one probe pins both).
/// `len`/`is_empty` had no caller; one probe pins both).
#[test]
fn credential_table_len_and_is_empty_accessors() {
let empty = alksocks::CredentialTable::new(vec![]);
+26 -26
View File
@@ -1,5 +1,5 @@
//! The UDP front door's integration tests (feature `local`): the POC
//! #2 test-8 shape end to end — a real UDP echo target, the producer
//! #2 test-8 shape end to end: a real UDP echo target, the producer
//! with `local::udp_egress()`, the door's relay socket, a vanilla
//! RFC-header'd datagram in → the echo back RFC-header'd (the RFC
//! codec boundary pinned: the in-band wire stays `{addr, data}`
@@ -32,7 +32,7 @@ use harness::echo_dial;
const BOUND: Duration = Duration::from_secs(10);
/// A UDP echo server on a real kernel socket; returns its address
/// (the vanilla client's datagram target — the POC #2 shape).
/// (the vanilla client's datagram target, the POC #2 shape).
async fn udp_echo_server() -> SocketAddr {
let sock = UdpSocket::bind(("127.0.0.1", 0))
.await
@@ -52,7 +52,7 @@ async fn udp_echo_server() -> SocketAddr {
addr
}
/// Build an RFC-header'd datagram (the vanilla client's sendto form —
/// Build an RFC-header'd datagram (the vanilla client's sendto form;
/// the codec appears at the door boundary, so the test hand-rolls the
/// same header: RSV(2) FRAG(1) ATYP(1) addr port).
fn rfc_datagram(target: SocketAddr, payload: &[u8]) -> Vec<u8> {
@@ -83,7 +83,7 @@ fn parse_rfc_datagram(wire: &[u8]) -> (SocketAddr, &[u8]) {
/// the same boundary (F-2: zero-length payload round-trips, never
/// EOF), and the in-band codec boundary is pinned (the door's relay
/// carries only RFC-header'd datagrams; the in-band wire carries only
/// `{addr, data}` frames — the round-trip through the real echo
/// `{addr, data}` frames: the round-trip through the real echo
/// target proves the two vocabularies never mixed).
#[tokio::test]
async fn udp_door_vanilla_client_round_trip_and_empty_datagram() {
@@ -125,7 +125,7 @@ async fn udp_door_vanilla_client_round_trip_and_empty_datagram() {
assert_eq!(payload, b"pre");
// The empty datagram (F-2): a zero-length payload round-trips,
// never EOF — the association stays healthy after it.
// never EOF: the association stays healthy after it.
client
.send_to(&rfc_datagram(echo_addr, b""), relay_addr)
.await
@@ -141,7 +141,7 @@ async fn udp_door_vanilla_client_round_trip_and_empty_datagram() {
"the empty datagram round-trips (never EOF)"
);
// Post: the association survived — another datagram round-trips.
// Post: the association survived; another datagram round-trips.
client
.send_to(&rfc_datagram(echo_addr, b"post"), relay_addr)
.await
@@ -155,7 +155,7 @@ async fn udp_door_vanilla_client_round_trip_and_empty_datagram() {
// The in-band vocabulary pin: the association's adopted channel
// exists (the door opened it in-band via AssociateSession) and the
// producer's stage saw the real echo target — the datagrams were
// producer's stage saw the real echo target (the datagrams were
// carried as `{addr, data}` frames (the echo server, which speaks
// only raw kernel datagrams, answered), never RFC-header'd bytes.
assert!(
@@ -170,7 +170,7 @@ async fn udp_door_vanilla_client_round_trip_and_empty_datagram() {
/// The reader-state pin through the door's bridge (review 003 R-1,
/// the POC #2 finding carried to the doors): two replies arriving
/// back-to-back (the doubled echo — both frames ride the producer's
/// back-to-back (the doubled echo: both frames ride the producer's
/// frame writer; the door's channel read may batch them into one
/// chunk) both reach the vanilla client. A bridge that re-created
/// its decoder per read lost the second datagram.
@@ -198,7 +198,7 @@ async fn batched_in_band_replies_both_reach_the_vanilla_client() {
});
// One datagram in → two echo datagrams out (both from the relay
// socket, both carrying the target as their source address — the
// socket, both carrying the target as their source address; the
// bridge's pending queue drained in order, none lost).
client
.send_to(&rfc_datagram(echo_addr, b"once"), relay_addr)
@@ -228,7 +228,7 @@ async fn batched_in_band_replies_both_reach_the_vanilla_client() {
}
/// FRAG ≠ 0 is refused at the door (the v1 posture): the fragment is
/// dropped — nothing is egressed — and the association stays healthy
/// dropped (nothing is egressed), and the association stays healthy
/// (a following normal datagram round-trips).
#[tokio::test]
async fn frag_nonzero_refused_and_association_stays_healthy() {
@@ -254,7 +254,7 @@ async fn frag_nonzero_refused_and_association_stays_healthy() {
});
// A FRAG=1 datagram: dropped at the door's codec boundary (no
// echo comes back — the datagram never went in-band).
// echo comes back, because the datagram never went in-band).
let SocketAddr::V4(v4) = echo_addr else {
panic!("v4 target")
};
@@ -268,7 +268,7 @@ async fn frag_nonzero_refused_and_association_stays_healthy() {
.expect("send frag");
// A normal datagram right after: the FRAG drop left the door
// serving — the association is healthy.
// serving: the association is healthy.
client
.send_to(&rfc_datagram(echo_addr, b"healthy"), relay_addr)
.await
@@ -297,7 +297,7 @@ async fn frag_nonzero_refused_and_association_stays_healthy() {
/// The client idle bound (ADR-014's door-side defense, constructor
/// config): a client that sends no datagrams within the window gets
/// its association closed door-side — the door's task ends, the
/// its association closed door-side: the door's task ends, the
/// association's channel is reaped (association lifetime = channel
/// lifetime, ADR-003), and a fresh association succeeds after the
/// teardown (the connection is healthy).
@@ -312,7 +312,7 @@ async fn idle_bound_closes_an_idle_association_and_a_fresh_one_succeeds() {
)
.await;
// First association: never sends — the idle bound fires within
// First association: never sends, so the idle bound fires within
// (window + slack) and the door's task ends.
let relay = UdpSocket::bind("127.0.0.1:0").await.expect("bind relay");
let door = UdpFrontDoor::builder()
@@ -348,7 +348,7 @@ async fn idle_bound_closes_an_idle_association_and_a_fresh_one_succeeds() {
);
// A fresh association succeeds after the teardown (the connection
// is healthy — the POC's strongest observable).
// is healthy, the POC's strongest observable).
let relay2 = UdpSocket::bind("127.0.0.1:0").await.expect("bind relay 2");
let relay2_addr = relay2.local_addr().unwrap();
let door2 = UdpFrontDoor::builder()
@@ -379,7 +379,7 @@ async fn idle_bound_closes_an_idle_association_and_a_fresh_one_succeeds() {
assert_eq!(payload, b"fresh");
}
/// A fresh association succeeds after teardown — driven through the
/// A fresh association succeeds after teardown, driven through the
/// same relay-port shape as the happy path (the idle test's follow-up,
/// pinned on its own so the "connection healthy" observable stands
/// alone).
@@ -456,8 +456,8 @@ async fn fresh_association_succeeds_after_teardown() {
serve2.abort();
}
/// The `serve()` wrapper (review 003 N-8: every test drove `serve_on`
/// — the bind-then-serve wrapper round-trips too; the port is
/// The `serve()` wrapper (review 003 N-8: every other test drove
/// `serve_on`; the bind-then-serve wrapper round-trips too; the port is
/// configured explicitly so the vanilla client can address the
/// relay).
#[tokio::test]
@@ -473,7 +473,7 @@ async fn udp_door_serve_wrapper_round_trips() {
.await;
// A free loopback port, probed up front (the door binds exactly
// it — the wrapper's own bind decision).
// it, the wrapper's own bind decision).
let probe = UdpSocket::bind("127.0.0.1:0").await.expect("probe bind");
let relay_addr = probe.local_addr().expect("probe addr");
drop(probe);
@@ -504,7 +504,7 @@ async fn udp_door_serve_wrapper_round_trips() {
/// The NAT port-rebind shape (OQ-SK2-09's option-2 fix): the client's
/// source IP pins on the first datagram, and a later datagram from the
/// same IP on a *new source port* moves the reply path — the rebind
/// same IP on a *new source port* moves the reply path (the rebind
/// keeps receiving replies. The RFC drop rule's complement: replies go
/// to the latest-seen port from the pinned IP.
#[tokio::test]
@@ -545,7 +545,7 @@ async fn nat_port_rebind_keeps_receiving_replies() {
// The "NAT rebind": a fresh socket, same IP, new source port. The
// datagram is accepted (same IP) and the reply goes to the *new*
// port — the rebind keeps receiving.
// port; the rebind keeps receiving.
let client_b = UdpSocket::bind("127.0.0.1:0").await.expect("client B");
client_b
.send_to(&rfc_datagram(echo_addr, b"after"), relay_addr)
@@ -569,8 +569,8 @@ async fn nat_port_rebind_keeps_receiving_replies() {
}
/// The RFC 1928 peer posture's drop rule (OQ-SK2-09's option-2 fix):
/// a datagram from a *different source IP* is dropped — never
/// egressed — and the association stays healthy for the pinned peer.
/// a datagram from a *different source IP* is dropped (never
/// egressed), and the association stays healthy for the pinned peer.
/// (Loopback-only IPs are used: the pinned peer keeps receiving, the
/// foreign-IP sender's datagram disappears.)
#[tokio::test]
@@ -609,9 +609,9 @@ async fn foreign_source_ip_datagrams_dropped_and_association_stays_healthy() {
.expect("recv");
assert_eq!(parse_rfc_datagram(&rx[..n]).1, b"pinned");
// A "foreign-IP" sender: 127.0.0.2 (loopback, a different IP —
// A "foreign-IP" sender: 127.0.0.2 (loopback, a different IP,
// bindable without privileges). Its datagram is dropped at the
// door's peer gate — no echo comes back to it, and nothing is
// door's peer gate: no echo comes back to it, and nothing is
// egressed to the echo target on its behalf.
let foreign = UdpSocket::bind("127.0.0.2:0").await.expect("foreign bind");
foreign
@@ -627,7 +627,7 @@ async fn foreign_source_ip_datagrams_dropped_and_association_stays_healthy() {
);
// The pinned peer is unaffected: its next datagram round-trips
// (the drop left the association healthy — the gate never evicts).
// (the drop left the association healthy; the gate never evicts).
client
.send_to(&rfc_datagram(echo_addr, b"still"), relay_addr)
.await
+94 -94
View File
@@ -1,15 +1,15 @@
//! Test harness — the producer↔consumer wiring over a
//! Test harness: the producer↔consumer wiring over a
//! `tokio::io::duplex` carrying the channels 8-byte chunk format.
#![allow(dead_code)]
//!
//! Topology (the alktunnels harness shape, adapted to this crate's
//! seams; the alkcall 0.8.0 establishment surface under test — this
//! seams; the alkcall 0.8.0 establishment surface under test; this
//! is the crate's first consumption of it):
//!
//! - **producer** (connect side of the channels connection — it runs
//! - **producer** (connect side of the channels connection; it runs
//! `ChannelClient::from_connection_with_serving`): serves the socks5
//! open op + the generic channel ops on its serving registry. Its
//! manager allocates the channel IDs (`ChannelSide::Connect` — the
//! manager allocates the channel IDs (`ChannelSide::Connect`, whose
//! serving side allocates; the initiator adopts).
//! - **consumer** (accept side): the `ChannelsAdapter` + capturing
//! install hook. The hook captures the consumer's channel-0
@@ -20,13 +20,13 @@
//! The POC #1 wiring requirement is load-bearing here
//! (producer.md §Harness wiring requirement): the accept-side
//! install hook must call `channel0_conn.set_identity(resolved)`
//! before building the `CallConnection` — without it, scope-gated
//! before building the `CallConnection`; without it, scope-gated
//! opens fail closed with `FORBIDDEN "authentication required"`
//! (correct behavior, implicit wiring). The harness makes the
//! requirement explicit and tested.
//!
//! Everything is in-process: no sockets, no `local` feature. The
//! harness is dev-only (`tests/`) — it stays out of the wasm build.
//! harness is dev-only (`tests/`), so it stays out of the wasm build.
//! The `allow(dead_code)` rides the alktunnels posture: each test
//! binary compiles the harness and uses a different subset.
@@ -64,7 +64,7 @@ use alksocks::Socks5Params;
// --- Shared helpers (the single-sourced shapes; review 002 N-5) -----------
//
// The dial/egress/accept stand-ins and the raw RFC 1928 client trio
// live in `tests/common/mod.rs` — the same file the lib test crate
// live in `tests/common/mod.rs`, the same file the lib test crate
// includes via `#[path]` (unit tests in `src/` cannot import
// `tests/harness.rs`, but they can share this module). The harness
// declares the module (path-included from `tests/`) and re-exports
@@ -95,7 +95,7 @@ pub fn consumer_identity() -> Identity {
}
}
/// The BIND-scope identity: `socks5:bind` ONLY — no `socks5:open`.
/// The BIND-scope identity: `socks5:bind` ONLY; no `socks5:open`.
/// The command-gate probe's identity.
pub fn bind_identity() -> Identity {
Identity {
@@ -106,7 +106,7 @@ pub fn bind_identity() -> Identity {
}
/// Both scopes: `socks5:open` AND `socks5:bind` (a deployment
/// granting both grants both explicitly — never implied, ADR-006).
/// granting both grants both explicitly; never implied, ADR-006).
pub fn both_scopes_identity() -> Identity {
Identity {
id: "consumer-both".to_string(),
@@ -117,12 +117,12 @@ pub fn both_scopes_identity() -> Identity {
/// The BIND-capable identity the BIND integration tests open with:
/// both scopes (the open gate needs `socks5:open`, the command gate
/// needs `socks5:bind` — a session needs both grants explicitly).
/// needs `socks5:bind`; a session needs both grants explicitly).
pub fn bind_scoped_identity() -> Identity {
both_scopes_identity()
}
/// The scoped token — resolves to [`consumer_identity`] via
/// The scoped token; it resolves to [`consumer_identity`] via
/// [`TestIdProvider`] (the hub-forwarding path's shape).
pub const TEST_AUTH_TOKEN: &str = "test-token-with-socks5-open-scope";
@@ -154,7 +154,7 @@ impl IdentityProvider for TestIdProvider {
// producer-side registrations.
/// The advertised BND.ADDR of [`echo_accept`] (in-process, no real
/// listener — the address is the reply#1 carrier only).
/// listener; the address is the reply#1 carrier only).
pub const ECHO_BIND_PORT: u16 = 2121;
/// An echo accept factory: reply#1 advertises a static in-process
@@ -236,14 +236,14 @@ pub fn refusing_accept(err: BindAcceptError) -> BindAcceptFactory {
// --- The producer registration helper --------------------------------------
/// What the establisher's per-call auth carried as the opener
/// identity id (the CF-006 probe — `None` until an open runs).
/// identity id (the CF-006 probe; `None` until an open runs).
pub type IdentityWitness = Arc<tokio::sync::Mutex<Option<String>>>;
/// The session-gate establisher: records the opener identity (the
/// CF-006 seam the harness witnesses); returns
/// `Establishment::default()` — no plan needed (the dial is
/// `Establishment::default()`, since no plan is needed (the dial is
/// in-stream, in the pump handler). A scope refusal is the registry
/// ACL gate's job (it runs before this ever runs — ADR-006).
/// ACL gate's job (it runs before this ever runs; ADR-006).
pub fn socks5_session_establisher(
witness: IdentityWitness,
) -> alkcall::channels::operations::OpenEstablisher {
@@ -259,7 +259,7 @@ pub fn socks5_session_establisher(
/// The smoke-test's minimal CONNECT producer path (the POC #1
/// `serve_socks5_over` shape, inline): negotiate no-auth → read the
/// command → dial via the injected closure → reply → `pump_bidi`
/// inline (R-02 — the handler's `JoinHandle` tracks the data-plane
/// inline (R-02: the handler's `JoinHandle` tracks the data-plane
/// lifetime; early return = teardown-at-birth). The producer task
/// replaces this with the real command-dispatch pump; the harness
/// keeps the smoke test producer-independent.
@@ -307,7 +307,7 @@ where
}
/// The pump handler over the minimal CONNECT path (the smoke test's
/// producer stand-in — the POC #1 shape). Awaited inline (R-02).
/// producer stand-in, the POC #1 shape). Awaited inline (R-02).
fn make_minimal_connect_handler(dial: DialFn) -> alkcall::channels::operations::OpenHandler {
Arc::new(
move |_input: serde_json::Value,
@@ -329,7 +329,7 @@ fn make_minimal_connect_handler(dial: DialFn) -> alkcall::channels::operations::
}
/// The producer-side registration mode (what [`wire_with`] registers
/// on the producer's serving registry post-hoc — W2's shape; the
/// on the producer's serving registry post-hoc, W2's shape; the
/// single place the producer-side wiring lives, so the per-command
/// tasks only write tests).
#[derive(Clone)]
@@ -353,25 +353,25 @@ pub enum RegisterMode {
/// registration rides [`alksocks::producer::Socks5Producer::
/// builder`] + [`alksocks::producer::Socks5Producer::register`]
/// (the builder's `dial`/`egress`/`accept` setters and the
/// delegated registration — the surface D-2 guards).
/// delegated registration: the surface D-2 guards).
ProducerBuilder {
dial: DialFn,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
bind_policy: Option<bool>,
},
/// No socks5 openable — only the generic channel ops (the
/// No socks5 openable; only the generic channel ops (the
/// unknown-op / registry-gate probe's shape).
NoSocks5,
}
/// Register the SOCKS5 open op on a `ChannelCore` + the session's
/// dispatch registry — the alkcall 0.8.0 establishment surface
/// dispatch registry: the alkcall 0.8.0 establishment surface
/// (`register_openable_with_establisher`) consumed for this crate's
/// shape. The producer task's `register_socks5_openable` succeeds
/// this helper (the smoke test's stand-in handler is replaced then;
/// the registration shape — spec, establisher, handler, auth,
/// timeout — stays).
/// the registration shape (spec, establisher, handler, auth,
/// timeout) stays).
pub fn register_socks5_openable_minimal(
core: &ChannelCore,
on_registry: &Arc<OperationRegistry>,
@@ -395,20 +395,20 @@ pub struct Topology {
/// The producer's client (connect side, serving). Its manager
/// allocated the socks5 channels (`ChannelSide::Connect`).
pub producer: Arc<ChannelClient>,
/// The consumer's manager (accept side) — the initiator adopts
/// The consumer's manager (accept side): the initiator adopts
/// the producer-allocated IDs here; probes reap via it.
pub consumer_manager: ChannelManager,
/// The consumer's channel-0 call connection — its surface for
/// The consumer's channel-0 call connection: its surface for
/// calling the producer's open op (and channel/close).
pub consumer_call: Arc<CallConnection>,
/// The producer's serving registry (the socks5 openable +
/// generic channel ops live here — probes).
/// generic channel ops live here; probes).
pub producer_registry: Arc<OperationRegistry>,
/// The consumer's serving registry (empty by default — the
/// The consumer's serving registry (empty by default; the
/// consumer serves no ops in the default topology).
pub consumer_registry: Arc<OperationRegistry>,
/// What the establisher's per-call auth carried (the CF-006
/// probe — `None` until an open runs).
/// probe; `None` until an open runs).
pub identity_witness: IdentityWitness,
}
@@ -416,7 +416,7 @@ pub struct Topology {
///
/// Identity wiring (the load-bearing POC #1 requirement): the
/// accept-side install hook calls `channel0_conn.set_identity`
/// before building the `CallConnection` — the serving dispatch reads
/// before building the `CallConnection`; the serving dispatch reads
/// it as the caller identity (CF-005 (b)'s accept-side analogue;
/// without it, scope-gated opens fail closed with `FORBIDDEN`). The
/// transport identity attaches to the dialing connection before
@@ -424,7 +424,7 @@ pub struct Topology {
/// propagates automatically on the connect side.
///
/// `serving_identity` overrides the dispatch-resolved identity per
/// `ServingConfig.identity` (CF-005 (a) probe shape — `None` in the
/// `ServingConfig.identity` (CF-005 (a) probe shape; `None` in the
/// primary path). `provider` resolves payload tokens (the
/// hub-forwarding path).
pub async fn wire_with(
@@ -464,7 +464,7 @@ pub async fn wire_with(
let (writer, reader) = split_single_stream(channel0_bidi);
// The load-bearing POC #1 wiring requirement: attach
// the resolved identity to channel 0 BEFORE building
// the `CallConnection` — the serving dispatch reads
// the `CallConnection`; the serving dispatch reads
// it as the caller identity; without it, scope-gated
// opens fail closed (`FORBIDDEN "authentication
// required"`). Explicit here; tested below.
@@ -475,7 +475,7 @@ pub async fn wire_with(
));
// The consumer serves the ops pre-registered on its
// serving registry (empty in the default topology —
// serving registry (empty in the default topology;
// the dispatcher keeps the call loop alive either
// way; its read loop resolves the producer's
// responses to the consumer's pending entries).
@@ -494,7 +494,7 @@ pub async fn wire_with(
// --- transport ------------------------------------------------------
// The producer dials the transport (connect side): its connection
// gets the transport identity BEFORE `from_connection_with_serving`
// (the mTLS/QUIC posture; CF-005 (b) — the primary caller-identity
// (the mTLS/QUIC posture; CF-005 (b), the primary caller-identity
// path). The accept side resolves it out-of-band (modeled in the
// hook above).
let (producer_end, consumer_end) = tokio::io::duplex(64 * 1024);
@@ -534,7 +534,7 @@ pub async fn wire_with(
let producer_client = Arc::new(producer_client);
// The generic channel ops ride the producer's serving registry
// (channel/close tears a channel down out-of-band — review 007's
// (channel/close tears a channel down out-of-band per review 007's
// recipe).
let ops = ChannelOperations::with_default_policy(producer_client.manager().clone());
ops.register_on(&producer_op_registry)
@@ -645,7 +645,7 @@ pub async fn wire(dial: DialFn) -> Topology {
/// The consumer-session topology (`Socks5ChannelSession::connect` is
/// normative for this shape): the consumer dials the transport
/// (connect side — `ChannelClient::from_connection`, a pure consumer
/// (connect side; `ChannelClient::from_connection`, a pure consumer
/// serving nothing) and the producer accepts (the adapter + an
/// install hook registering the SOCKS5 openable over the injected
/// seams on the producer's serving registry).
@@ -654,19 +654,19 @@ pub struct ConsumerSideTopology {
/// manager adopted the producer-allocated channel IDs; the
/// session reaps via it.
pub consumer: Arc<ChannelClient>,
/// The producer's manager (accept side, serving) — for asserting
/// The producer's manager (accept side, serving): for asserting
/// the handler-exit reaping (no leaked channel entries).
pub producer_manager: ChannelManager,
/// What the establisher's per-call auth carried (the CF-006
/// probe — `None` until an open runs).
/// probe; `None` until an open runs).
pub identity_witness: IdentityWitness,
}
/// Wire the consumer-side topology over one duplex transport:
/// consumer dials (identity attached before `from_connection` — the
/// consumer dials (identity attached before `from_connection`: the
/// CF-005 (b) transport-identity path), producer accepts (the hook
/// attaches the resolved identity to channel 0 before the
/// `CallConnection` — the load-bearing POC #1 wiring requirement).
/// `CallConnection`: the load-bearing POC #1 wiring requirement).
pub async fn wire_consumer_side_with(
dial: DialFn,
egress: Option<EgressFactory>,
@@ -684,7 +684,7 @@ pub async fn wire_consumer_side_with(
}
/// The consumer-side topology with the producer-side dispatch's
/// identity provider configured (the CF-005 precedence (1) seam — the
/// identity provider configured (the CF-005 precedence (1) seam: the
/// payload `auth_token` → `identity_provider` resolution path the
/// cred-bearing door's token sessions ride). Everything else matches
/// [`wire_consumer_side_with`].
@@ -791,16 +791,16 @@ pub async fn wire_consumer_side(dial: DialFn) -> ConsumerSideTopology {
// --- The composed suite's topology (the alktunnels direct op) -------
/// The tunnel-direct topology (the composed suite's shape): the
/// consumer dials the transport (a full `ChannelClient` — the
/// consumer dials the transport (a full `ChannelClient`, the
/// composed egress's direct-op caller) and the producer accepts (the
/// adapter + an install hook registering the alktunnels direct op —
/// `register_tunnel_direct_openable` over the given dial — on the
/// adapter + an install hook registering the alktunnels direct op,
/// `register_tunnel_direct_openable` over the given dial, on the
/// producer's serving registry). The resolved identity carries
/// `tunnel:direct` (the direct op's scope gate).
pub struct TunnelDirectTopology {
/// The consumer's client — `TunnelSession::open_direct` rides it.
/// The consumer's client: `TunnelSession::open_direct` rides it.
pub consumer: Arc<ChannelClient>,
/// The producer's manager (accept side, serving) — probes.
/// The producer's manager (accept side, serving): probes.
pub producer_manager: ChannelManager,
}
@@ -904,13 +904,13 @@ impl<S> RawRfcClient<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
/// Wrap a stream (a channel's `BiStream`, a duplex half —
/// Wrap a stream (a channel's `BiStream`, a duplex half, anything
/// substrate-agnostic).
pub fn new(stream: S) -> Self {
Self { stream }
}
/// Greeting (`05 01 00` — version 5, one method, no-auth) → the
/// Greeting (`05 01 00`: version 5, one method, no-auth) → the
/// method-selection read (`05 00`). Returns the selected method
/// byte.
pub async fn greet(&mut self) -> Result<u8, std::io::Error> {
@@ -928,7 +928,7 @@ where
}
/// Read one reply (the 4-byte head + the BND.ADDR per its ATYP).
/// Returns `(reply_code, bnd_addr_bytes)` — the address bytes as
/// Returns `(reply_code, bnd_addr_bytes)`: the address bytes as
/// the RFC form carries them (ATYP + addr + port).
pub async fn read_reply(&mut self) -> Result<(u8, Vec<u8>), std::io::Error> {
let (code, _atyp, addr) = raw_read_reply(&mut self.stream).await?;
@@ -953,15 +953,15 @@ where
// --- The composed accept's topology (the bind-first tunnel producer) ---
/// The bind-first topology (the composed accept suite's shape —
/// The bind-first topology (the composed accept suite's shape,
/// alktunnels' `wire_listener_bindfirst` adapted): the opener dials
/// the transport (the SOCKS5 consumer side — its `ChannelClient` is
/// the transport (the SOCKS5 consumer side; its `ChannelClient` is
/// what `TunnelListener::open` and the SOCKS5 BIND client ride) and
/// the tunnel producer accepts (the adapter + an install hook
/// registering the alktunnels bind-first openable over the given
/// bind/accept-loop injections AND the SOCKS5 producer openable with
/// the composed accept factory). One connection carries both
/// protocols' ops — the producer serves `alk/tunnel`'s open op and the
/// protocols' ops; the producer serves `alk/tunnel`'s open op and the
/// forwarded op, plus the SOCKS5 open op, on one registry.
///
/// The opener identity carries `tunnel:open` + `tunnel:forwarded` +
@@ -969,13 +969,13 @@ where
/// story crosses: the listen open, the forwarded ingress, the SOCKS5
/// session open, the BIND command gate).
pub struct BindFirstTopology {
/// The opener's client — `TunnelListener::open` + the SOCKS5 BIND
/// The opener's client: `TunnelListener::open` + the SOCKS5 BIND
/// session + the composed accept factory all ride it.
pub opener: Arc<ChannelClient>,
/// The opener's serving registry — the forwarded op lands here
/// The opener's serving registry: the forwarded op lands here
/// (via `register_forwarded_serving`).
pub opener_registry: Arc<OperationRegistry>,
/// The producer's manager (accept side, serving) — reaping probes.
/// The producer's manager (accept side, serving): reaping probes.
pub producer_manager: ChannelManager,
/// The producer's serving registry (probes).
pub producer_registry: Arc<OperationRegistry>,
@@ -985,7 +985,7 @@ pub struct BindFirstTopology {
}
/// Wire the bind-first topology over one duplex transport. The
/// producer's install hook registers — on its serving registry — the
/// producer's install hook registers, on its serving registry, the
/// alktunnels bind-first openable (over the given `bind` + the given
/// accept loop) and the SOCKS5 openable over a composed accept factory
/// built from `register_forwarded_serving` on the OPENER side... but
@@ -993,7 +993,7 @@ pub struct BindFirstTopology {
/// exists after the opener's client is built. The hook therefore
/// defers the forwarded registration: the test wires it post-hoc via
/// [`alksocks::composed::register_forwarded_serving`] on
/// `opener_registry` (W2's post-hoc posture — the dispatcher reads
/// `opener_registry` (W2's post-hoc posture: the dispatcher reads
/// through the shared `Arc<OperationRegistry>` per dispatch).
pub async fn wire_bindfirst_composed(
registry: alktunnels::producer::ResourceRegistry,
@@ -1044,7 +1044,7 @@ pub async fn wire_bindfirst_composed(
ops.register_on(&registry_arc)
.expect("register channel ops on producer");
// The forwarded-open caller (the call plane — alktunnels
// The forwarded-open caller (the call plane, alktunnels
// harness's shape): call + adopt on the serving session's
// `CallConnection`.
let call_for_opener = Arc::clone(&call_connection);
@@ -1106,7 +1106,7 @@ pub async fn wire_bindfirst_composed(
// topology pieces that only exist after the opener's
// client is built (the composed accept's factory needs the
// opener's `ChannelClient`), the registration defers to
// the post-hoc W2 shape — the caller registers it on the
// the post-hoc W2 shape; the caller registers it on the
// returned `producer_registry` (the dispatcher reads
// through the shared `Arc<OperationRegistry>` per
// dispatch).
@@ -1196,7 +1196,7 @@ pub fn composed_opener_identity() -> Identity {
}
}
/// [`composed_opener_identity`] minus the forwarded grant — the
/// [`composed_opener_identity`] minus the forwarded grant: the
/// per-accept isolation probe's identity (the forwarded open fails
/// FORBIDDEN; the accept is consumed, the listener lives).
pub fn composed_scopeless_opener_identity() -> Identity {
@@ -1213,7 +1213,7 @@ pub fn composed_scopeless_opener_identity() -> Identity {
/// The composed suite's full identity: the five gates the composed
/// three-command story crosses (`tunnel:open` + `tunnel:direct` +
/// `tunnel:forwarded` + `socks5:open` + `socks5:bind`) — the assembly
/// `tunnel:forwarded` + `socks5:open` + `socks5:bind`); the assembly
/// suite's producer registration serves the direct op (CONNECT +
/// ASSOCIATE's tunnels) beside the bind-first op (BIND's listener).
pub fn composed_full_opener_identity() -> Identity {
@@ -1233,18 +1233,18 @@ pub fn composed_full_opener_identity() -> Identity {
// --- The composed accept's shared machinery (review 002 N-5) ----------------
//
// The bind/accept-queue stand-ins and the channel-set probe the
// composed suites consume — single-sourced here now that two suites
// composed suites consume, single-sourced here now that two suites
// (composed-accept + assembly-integration) drive the same shapes.
/// The resource the bind-first producer serves (the composed suites'
/// only subscription — the registry namespace gate).
/// only subscription, the registry namespace gate).
pub const COMPOSED_RESOURCE: &str = "bind";
/// The bound address the test bind reports (the "OS-chosen" stand-in —
/// The bound address the test bind reports (the "OS-chosen" stand-in;
/// the test's queue listener owns it; fidelity is the assert's job).
pub const COMPOSED_BOUND_PORT: u16 = 42113;
/// The peer address the accepted pairs report (the "app server" — the
/// The peer address the accepted pairs report (the "app server"; the
/// forwarded op's `peer` param carries it to reply#2).
pub const COMPOSED_PEER_PORT: u16 = 55231;
@@ -1265,7 +1265,7 @@ pub fn composed_test_bind() -> alktunnels::producer::BindFn {
}
/// The accept-queue pair the test accept loop pops from (the alktunnels
/// listener test's shape: a VecDeque + Notify — one accepted pair per
/// listener test's shape: a VecDeque + Notify, one accepted pair per
/// dispatcher call).
pub type ComposedAcceptQueue = (
Arc<
@@ -1287,8 +1287,8 @@ pub fn composed_fresh_queue() -> ComposedAcceptQueue {
}
/// An accepted "connection" for the test accept loop: a duplex pair
/// with an echo task on the far end (the app server's socket stand-in
/// — the data plane's echo confirms the full round trip).
/// with an echo task on the far end (the app server's socket stand-in;
/// the data plane's echo confirms the full round trip).
fn composed_accepted_halves(
peer_addr: (&str, u16),
) -> (
@@ -1362,10 +1362,10 @@ pub async fn composed_push_accept(queue: &ComposedAcceptQueue, peer: (&str, u16)
/// The composed suites' no-leak probe: the opener side ends with
/// channel 0 (+ the live channels the test names); the producer side
/// ends with channel 0, the SOCKS5 channels the test names, AND any
/// odd id — the producer-side ADOPTED forwarded channels
/// odd id: the producer-side ADOPTED forwarded channels
/// (opener-allocated ids, adopted inside the producer's
/// `open_forwarded` caller; per-session ownership per alktunnels
/// ADR-005 — their entries reap at transport EOF, not at the
/// ADR-005; their entries reap at transport EOF, not at the
/// listener's teardown; upstream's own bind-first suites make the
/// same concession in `no_stray_channels`).
pub async fn assert_composed_channels(
@@ -1448,7 +1448,7 @@ mod tests {
.await
.expect("dial resolves");
let (mut probe_far, probe_near) = tokio::io::duplex(64 * 1024);
// The pump feeds the handle against the probe's near end —
// The pump feeds the handle against the probe's near end,
// exactly the data-plane shape the producer handler runs.
let pump = tokio::spawn(pump_bidi(probe_near, handle.read, handle.write));
probe_far.write_all(b"ping").await.expect("write");
@@ -1577,7 +1577,7 @@ mod tests {
/// A hand-rolled minimal RFC server over a duplex: greeting →
/// method selection (`05 00`) → reply read (the differential
/// suite's mock-server pattern) — the raw client's counterparty.
/// suite's mock-server pattern); it is the raw client's counterparty.
async fn hand_rolled_server(mut stream: tokio::io::DuplexStream, reply_code: u8) {
let mut greeting = [0u8; 3];
stream.read_exact(&mut greeting).await.expect("greeting");
@@ -1587,7 +1587,7 @@ mod tests {
.await
.expect("method selection");
// Read the command request head (VER CMD RSV ATYP) + the rest
// per ATYP (a domain here — the helper's test target form).
// per ATYP (a domain here, the helper's test target form).
let mut head = [0u8; 4];
stream.read_exact(&mut head).await.expect("command head");
assert_eq!(head[1], consts::SOCKS5_CMD_TCP_CONNECT);
@@ -1614,7 +1614,7 @@ mod tests {
/// The raw client round-trips greeting + command + reply against
/// a hand-rolled server over a duplex (the task's acceptance
/// criterion — the raw helper is producer-independent).
/// criterion: the raw helper is producer-independent).
#[tokio::test]
async fn raw_rfc_client_round_trips_against_a_hand_rolled_server() {
let (client_end, server_end) = tokio::io::duplex(1024);
@@ -1653,7 +1653,7 @@ mod tests {
/// The full loop: harness up → producer registered → consumer
/// opens a channel → raw RFC CONNECT conversation → echo
/// round-trip. The producer path is the minimal CONNECT stand-in
/// (the POC #1 inline shape) — the producer task replaces it.
/// (the POC #1 inline shape); the producer task replaces it.
#[tokio::test]
async fn smoke_connect_echo_round_trip_over_the_harness() {
let topo = wire(echo_dial()).await;
@@ -1723,13 +1723,13 @@ mod tests {
/// The scope-less identity fails closed: an identity with no
/// `socks5:open` gets the registry's `FORBIDDEN` (the open gate
/// is the boundary; ADR-006) — the POC #1 wiring requirement
/// is the boundary; ADR-006); this is the POC #1 wiring requirement
/// tested explicitly (the hook's `set_identity` is what made the
/// allowed path work above; omitting it yields the same
/// `FORBIDDEN`, fail-closed).
#[tokio::test]
async fn scopeless_identity_fails_closed_with_forbidden() {
// A bare identity — no scopes at all.
// A bare identity with no scopes at all.
let bare = Identity {
id: "scopeless".to_string(),
scopes: vec![],
@@ -1770,7 +1770,7 @@ mod tests {
}
/// The establisher's per-call auth witness: the opener identity
/// id arrives on the establisher's `AuthContext` (CF-006) — the
/// id arrives on the establisher's `AuthContext` (CF-006); the
/// seam the producer task maps SOCKS5 credentials/ACL onto.
#[tokio::test]
async fn establisher_witnesses_the_opener_identity() {
@@ -1796,7 +1796,7 @@ mod tests {
/// The harness topology sanity: the producer's manager allocated
/// the channel id (the serving side allocates; the initiator
/// adopts — ADR-047 §5).
/// adopts; ADR-047 §5).
#[tokio::test]
async fn producer_allocates_consumer_adopts() {
let topo = wire(echo_dial()).await;
@@ -1888,7 +1888,7 @@ mod tests {
// Half-close teardown: client EOF ends the session; the
// channel pump completes (the handler tracked the data plane
// to the end — R-02).
// to the end; R-02).
drop(raw);
tokio::time::timeout(std::time::Duration::from_secs(10), pump)
.await
@@ -1899,7 +1899,7 @@ mod tests {
/// A refusing dial surfaces 0x05 in-stream with no phantom data
/// plane: the reply comes back refused; bytes sent after the
/// refusal are not echoed (nothing was dialed). The channel
/// stays healthy — a fresh open succeeds immediately.
/// stays healthy; a fresh open succeeds immediately.
#[tokio::test]
async fn producer_dial_refusal_is_0x05_and_no_phantom_data_plane() {
let topo = wire(refusing_dial(SocksDialError::Refused("target down".into()))).await;
@@ -1932,7 +1932,7 @@ mod tests {
}
/// The scope-class dial refusal (`NotAllowed`) surfaces 0x02
/// (the mapped code — the dial seam's policy refusal path).
/// (the mapped code: the dial seam's policy refusal path).
#[tokio::test]
async fn producer_scope_class_refusal_is_0x02() {
let topo = wire(refusing_dial(SocksDialError::NotAllowed("policy".into()))).await;
@@ -1949,7 +1949,7 @@ mod tests {
}
/// ASSOCIATE refuses RFC-faithfully when the egress seam is not
/// wired (`CommandNotSupported` 0x07 — ADR-008's missing-callback
/// wired (`CommandNotSupported` 0x07, ADR-008's missing-callback
/// class) while CONNECT still serves the same registration (the
/// missing-callback posture, over channels).
#[tokio::test]
@@ -1987,7 +1987,7 @@ mod tests {
// `datagram_frame`, `udp_unframe` is `decode_datagram`.)
/// The ASSOCIATE conversation over a channel: open + adopt +
/// pump, greet, request, reply — asserting the sentinel
/// pump, greet, request, reply, asserting the sentinel
/// `127.0.0.1:0` BND.ADDR (ADR-003's RFC-compatibility surface).
async fn raw_associate(
topo: &Topology,
@@ -2091,7 +2091,7 @@ mod tests {
.expect("read resolves");
match outcome {
Ok(0) | Err(_) => {}
Ok(n) => panic!("EOF expected after an undecodable frame — got {n} bytes"),
Ok(n) => panic!("EOF expected after an undecodable frame; got {n} bytes"),
}
drop(raw);
tokio::time::timeout(std::time::Duration::from_secs(10), pump)
@@ -2147,7 +2147,7 @@ mod tests {
}
/// The egress-factory refusal maps to reply#1's mapped code over
/// channels (the pre-reply refusal path) — no phantom datagram
/// channels (the pre-reply refusal path); no phantom datagram
/// plane.
#[tokio::test]
async fn producer_udp_associate_factory_refusal_maps_to_reply_code() {
@@ -2182,7 +2182,7 @@ mod tests {
}
/// CONNECT + ASSOCIATE on the same registration over channels
/// (the regression guard, extended — one registration, both
/// (the regression guard, extended: one registration, both
/// commands).
#[tokio::test]
async fn producer_connect_and_associate_on_the_same_registration() {
@@ -2301,7 +2301,7 @@ mod tests {
/// The command gate over channels (ADR-006; POC #5 test 4): an
/// identity holding only `socks5:open` gets `ConnectionNotAllowed`
/// (0x02) on BIND — while CONNECT still serves the same identity
/// (0x02) on BIND, while CONNECT still serves the same identity
/// (the gate distinguishes ingress from egress).
#[tokio::test]
async fn producer_bind_command_gate_refuses_0x02_connect_unaffected() {
@@ -2317,7 +2317,7 @@ mod tests {
)
.await;
// BIND refused, RFC-faithfully — no phantom listener (the
// BIND refused, RFC-faithfully, with no phantom listener (the
// accept seam never ran; the session ends).
let (mut raw, pump) = open_channel_and_pump(&topo).await;
raw.greet().await.expect("greet");
@@ -2333,7 +2333,7 @@ mod tests {
drop(pump);
// CONNECT still works for the same identity (egress is not
// implied by the BIND gate's absence — separate scopes).
// implied by the BIND gate's absence; separate scopes).
let (mut raw, _pump) = open_channel_and_pump(&topo).await;
let code = raw
.connect(&TargetAddr::Domain("echo.test".into(), 443))
@@ -2353,7 +2353,7 @@ mod tests {
}
/// The BIND-scope-only identity over channels: `socks5:bind`
/// alone serves BIND (the open gate is `socks5:open` — the
/// alone serves BIND (the open gate is `socks5:open`, so the
/// registration's identity must carry it; the command gate only
/// adds `socks5:bind` on top of a granted session). The both-
/// scopes identity is the deployment shape that grants both
@@ -2397,7 +2397,7 @@ mod tests {
}
/// Quiet-wait garbage over channels: a control byte after
/// reply#1 ends the session with no data plane — the raw client
/// reply#1 ends the session with no data plane: the raw client
/// reads EOF (no reply#2, no echo), and the channel pump
/// completes.
#[tokio::test]
@@ -2451,10 +2451,10 @@ mod tests {
.expect("read resolves");
match outcome {
Ok(0) | Err(_) => {}
Ok(n) => panic!("no reply#2 after garbage — got {n} bytes"),
Ok(n) => panic!("no reply#2 after garbage; got {n} bytes"),
}
// The client vanishes (EOF both ways through the raw pipe);
// the session pump ends (R-02 — the handler tracked the data
// the session pump ends (R-02: the handler tracked the data
// plane to its end).
drop(raw);
tokio::time::timeout(std::time::Duration::from_secs(10), pump)
@@ -2464,7 +2464,7 @@ mod tests {
}
/// The three-command regression guard (POC #5 test 7): one
/// registration, all three commands — CONNECT serves, BIND serves
/// registration, all three commands; CONNECT serves, BIND serves
/// under its gate, ASSOCIATE serves with the echo egress. Fresh
/// channel per session.
#[tokio::test]
+13 -13
View File
@@ -1,9 +1,9 @@
//! The producer-side local backend's integration tests (feature
//! `local`; tasks/implementation/local-backend-producer.md): a vanilla
//! RFC 1928 client against the locally-served producer — the whole
//! RFC 1928 client against the locally-served producer: the whole
//! stack on real sockets (`ProducerDoor` → `serve_socks5_over` →
//! `local::tcp_dial()` / `local::bind_accept()`), BIND through the
//! local backend (the faithful-RFC variant — real reply#1/reply#2
//! local backend (the faithful-RFC variant, with real reply#1/reply#2
//! addresses), the idle-first-byte timeout, the session cap,
//! per-connection failure isolation, and the `bind_policy(false)`
//! gate's RFC-faithful BIND refusal (0x02).
@@ -24,7 +24,7 @@ const BOUND: Duration = Duration::from_secs(10);
const IDLE: Duration = Duration::from_millis(300);
/// Spawn a real echo TCP server on an ephemeral loopback port
/// (CONNECT's target — the dial seam dials it for real).
/// (CONNECT's target; the dial seam dials it for real).
async fn echo_tcp_server() -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local_addr");
@@ -146,7 +146,7 @@ async fn vanilla_client_connect_round_trips_through_local_seams() {
_guard.close().await;
}
/// BIND through the local backend — the faithful-RFC variant: a real
/// BIND through the local backend, the faithful-RFC variant: a real
/// inbound peer connects to the reply#1-advertised listener, and
/// reply#2 carries the real peer address.
#[tokio::test]
@@ -222,7 +222,7 @@ async fn vanilla_client_bind_round_trips_with_real_addresses() {
/// The idle-first-byte defense (ADR-014 knob 1): an accepted
/// connection that sends nothing within the window is closed
/// door-side — EOF arrives, no greeting ever answered.
/// door-side: EOF arrives, no greeting ever answered.
#[tokio::test]
async fn idle_first_byte_timeout_closes_the_accepted_connection() {
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
@@ -232,7 +232,7 @@ async fn idle_first_byte_timeout_closes_the_accepted_connection() {
match timeout(IDLE * 4, stream.read(&mut buf)).await {
Err(_) => panic!("the idle bound must fire"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("EOF expected after the idle bound — got {n} bytes"),
Ok(Ok(n)) => panic!("EOF expected after the idle bound; got {n} bytes"),
}
_guard.close().await;
}
@@ -257,7 +257,7 @@ async fn session_cap_refuses_beyond_the_bound_while_live_sessions_serve() {
match timeout(BOUND, refused.read(&mut buf)).await {
Err(_) => panic!("the capped accept must end"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("the capped accept is refused — got {n} bytes"),
Ok(Ok(n)) => panic!("the capped accept is refused; got {n} bytes"),
}
// The held session still serves (isolation: the refusal did not
@@ -275,7 +275,7 @@ async fn session_cap_refuses_beyond_the_bound_while_live_sessions_serve() {
}
/// Per-connection failure isolation: a connection speaking protocol
/// garbage ends loud door-side while the loop continues — the next
/// garbage ends loud door-side while the loop continues; the next
/// connection on the same door serves normally.
#[tokio::test]
async fn per_connection_failure_is_isolated_and_the_loop_continues() {
@@ -312,15 +312,15 @@ async fn per_connection_failure_is_isolated_and_the_loop_continues() {
}
/// The `bind_policy(false)` gate is honored on this path (ADR-006's
/// posture): BIND refuses RFC-faithfully with 0x02 — the command is
/// known, the caller is not allowed — while CONNECT still serves.
/// posture): BIND refuses RFC-faithfully with 0x02 (the command is
/// known, the caller is not allowed), while CONNECT still serves.
#[tokio::test]
async fn bind_policy_false_refuses_bind_rfc_faithfully_but_connect_serves() {
let target = echo_tcp_server().await;
let (door, _guard) = serve(local_producer(Some(false)), IDLE, 16).await;
// BIND → 0x02 (ConnectionNotAllowed), through the producer's own
// in-stream gate (no open-op identity here — the builder's
// in-stream gate (no open-op identity here; the builder's
// bind_policy is the whole gate on this path).
let mut client = TcpStream::connect(door).await.expect("door connect");
client
@@ -438,7 +438,7 @@ async fn vanilla_client_udp_associate_relays_through_the_local_egress() {
.expect("echo reply")
.expect("read");
// The reply's inner carries the *sender's* address (the echo's
// source is the egress socket, not the target) — decode and match
// source is the egress socket, not the target), so decode and match
// the payload only.
let n = u16::from_be_bytes([reply[0], reply[1]]) as usize;
let inner = &reply[2..2 + n];
@@ -456,7 +456,7 @@ async fn vanilla_client_udp_associate_relays_through_the_local_egress() {
}
/// The door's address target form: a domain-form CONNECT target
/// resolves at the seam (the ssh `-D` semantic — ADR-012) — the echo
/// resolves at the seam (the ssh `-D` semantic, ADR-012); the echo
/// server is reachable as `localhost`.
#[tokio::test]
async fn domain_form_targets_resolve_at_the_seam() {
+15 -15
View File
@@ -1,7 +1,7 @@
//! The local providers' integration tests (feature `local`): the
//! three socket-backed seams driven through the producer's real
//! command-dispatch path (the harness's consumer-side topology) with
//! real targets — the CONNECT echo round-trip, the UDP datagram
//! real targets: the CONNECT echo round-trip, the UDP datagram
//! round-trip through the producer's datagram stage (the POC #2
//! shape), the BIND accept end-to-end (real addresses in reply#1/
//! reply#2), the readiness contract re-pinned with the real-socket
@@ -30,7 +30,7 @@ use harness::{both_scopes_identity, wire_consumer_side_with};
const BOUND: Duration = Duration::from_secs(10);
/// A session error carrying an RFC reply code (the refusal probe's
/// expectation shape — the mapped code rides the session error).
/// expectation shape: the mapped code rides the session error).
fn reply_code_of(err: &SocksSessionError) -> Option<u8> {
match err {
SocksSessionError::Rfc(SocksErrorKind::ReplyError(code)) => Some(code.as_u8()),
@@ -38,7 +38,7 @@ fn reply_code_of(err: &SocksSessionError) -> Option<u8> {
}
}
/// Both managers hold only channel 0 (the no-phantom-channel probe —
/// Both managers hold only channel 0 (the no-phantom-channel probe;
/// the connect suite's shape, re-pinned).
async fn no_leaked_channels(topo: &harness::ConsumerSideTopology) {
let deadline = Duration::from_secs(5);
@@ -108,7 +108,7 @@ async fn echo_udp_server() -> SocketAddr {
}
/// The local providers wired as the producer's seams (the assembly
/// layer's shape — producer.md §Wiring's constructor names).
/// layer's shape; producer.md §Wiring's constructor names).
fn local_providers() -> (
alksocks::egress::DialFn,
alksocks::egress::EgressFactory,
@@ -123,7 +123,7 @@ fn local_providers() -> (
/// CONNECT through the local dial: the consumer session's bytes
/// round-trip through the real echo server; the domain target
/// (`localhost`) resolves producer-side (the ssh `-D` semantic —
/// (`localhost`) resolves producer-side (the ssh `-D` semantic,
/// ADR-012); the dial witness saw it unresolved.
#[tokio::test]
async fn connect_round_trips_through_a_real_target() {
@@ -160,7 +160,7 @@ async fn connect_round_trips_through_a_real_target() {
/// A connect-refused target (a bound-then-dropped port): the dial
/// seam refuses with `Refused` → the producer's reply carries 0x05
/// → the session error surfaces the mapped code — the refusal rides
/// → the session error surfaces the mapped code (the refusal rides
/// in-stream (no phantom channel).
#[tokio::test]
async fn connect_refused_surfaces_reply_code_0x05_through_the_producer() {
@@ -188,7 +188,7 @@ async fn connect_refused_surfaces_reply_code_0x05_through_the_producer() {
/// ASSOCIATE through the real-socket egress: the datagram stage's
/// framed round-trip reaches a real UDP echo (the POC #2 shape
/// through the producer's stage — framing once, inner bytes at the
/// through the producer's stage: framing once, inner bytes at the
/// egress, the sender's address reported back).
#[tokio::test]
async fn associate_round_trips_through_the_real_egress() {
@@ -224,7 +224,7 @@ async fn associate_round_trips_through_the_real_egress() {
/// The readiness contract re-pinned with the real-socket egress,
/// through the producer's datagram stage: the association stays
/// parked (the egress's `next_reply` parks on `readable()`) while no
/// datagram is in flight — the consumer's `send` after a quiet
/// datagram is in flight; the consumer's `send` after a quiet
/// window still round-trips (no spin starved the channel branch; a
/// hot `Ok(None)` egress would have wedged the stage's biased
/// select into livelock and the second datagram would never
@@ -241,7 +241,7 @@ async fn the_readiness_contract_holds_through_the_producer_stage() {
let target = TargetAddr::Ip(echo);
// A datagram now, then a quiet window (the stage parked on the
// real socket's readiness), then a datagram after — the second
// real socket's readiness), then a datagram after; the second
// round-trip proves the stage never wedged.
assoc
.send_datagram(&target, b"first")
@@ -347,7 +347,7 @@ async fn bind_end_to_end_with_real_addresses_and_round_trip() {
assert_eq!(&buf, b"outbound");
// Both ends close (the producer's pump exits when the accepted
// halves' peer is gone — a real TCP stream stays open until its
// halves' peer is gone; a real TCP stream stays open until its
// owner closes it, unlike the harness duplexes).
drop(plane);
drop(client);
@@ -357,8 +357,8 @@ async fn bind_end_to_end_with_real_addresses_and_round_trip() {
/// The accept-future-owns-its-listener contract, through the
/// producer's quiet wait: a client-gone BIND session (the consumer
/// vanishes mid-quiet-wait) tears the listener down with the session
/// — the advertised address stops accepting (the drop-witness probe:
/// vanishes mid-quiet-wait) tears the listener down with the session;
/// the advertised address stops accepting (the drop-witness probe:
/// a fresh re-bind of the same port succeeds, the listener was
/// dropped, not leaked).
#[tokio::test]
@@ -383,13 +383,13 @@ async fn client_gone_mid_quiet_wait_closes_the_listener() {
};
// The client-gone teardown: drop the session mid-quiet-wait (the
// channel's write half EOFs — the producer's quiet wait exits on
// channel's write half EOFs; the producer's quiet wait exits on
// the EOF sentinel and the accept future drops, taking the
// listener with it).
drop(session);
// The producer's teardown reaps the channel entry (poll past the
// reap race), then the advertised address must be re-bindable —
// reap race), then the advertised address must be re-bindable;
// the listener closed with the quiet wait's drop.
timeout(Duration::from_secs(5), async {
loop {
@@ -408,7 +408,7 @@ async fn client_gone_mid_quiet_wait_closes_the_listener() {
.expect("the producer's entry reaped");
// A held port would refuse the re-bind; the dropped listener
// frees it. (The OS may hand the port to the re-bind — the probe
// frees it. (The OS may hand the port to the re-bind; the probe
// is that it is free either way.)
let mut rebound = None;
for _ in 0..50 {
+32 -32
View File
@@ -1,9 +1,9 @@
//! The RFC 1928 core's differential RFC edge-case suite (the wire
//! form is the point — expectations are pinned as explicit bytes).
//! form is the point; expectations are pinned as explicit bytes).
//!
//! Posture (docs/architecture/rfc1928.md §Maintenance Posture,
//! ADR-013): `/workspace/fast-socks5` (v1.0.0) is the behavioral
//! reference — consulted while writing these tests; the assertions
//! reference, consulted while writing these tests; the assertions
//! pin the expected bytes, never the reference checkout itself
//! (build-time dependency-free). Where the fork deliberately
//! diverges (the pruned error variants, the published `new_reply`),
@@ -13,7 +13,7 @@
//! Differential convention: every non-obvious expectation names the
//! upstream behavior it pins (file + line in the v1.0.0 checkout).
//!
//! Infrastructure: tokio duplex streams only — no sockets, no
//! Infrastructure: tokio duplex streams only (no sockets, no
//! `local` feature; the suite runs in the default wasm-clean crate's
//! native test build.
@@ -40,7 +40,7 @@ fn spawn_no_auth(server: tokio::io::DuplexStream) -> ServerTask<()> {
}
// ---------------------------------------------------------------------------
// Group 1 — negotiation edge cases
// Group 1: negotiation edge cases
// ---------------------------------------------------------------------------
/// An empty method list (methods_len = 0) matches nothing → 0xff.
@@ -84,7 +84,7 @@ async fn negotiation_zero_methods_offered_replies_0xff() {
/// Method-order preference: the server picks *its* first match in
/// `server_methods` order, not the client's first offer. Upstream
/// pins this as the outer-loop-in-server-order contract
/// (`src/server.rs:901-902` — "server_methods order matter!") —
/// (`src/server.rs:901-902`, "server_methods order matter!");
/// in-tree, `accept_no_auth` offers only `NoAuthentication`, so a
/// client offering password-first-then-none still gets 0x00.
#[tokio::test]
@@ -100,7 +100,7 @@ async fn negotiation_server_picks_its_own_first_match() {
/// The same contract under `accept_password_auth`: a client offering
/// none-then-password is answered with the *server's* preferred
/// method (0x02 — `negotiate_auth` walks `server_methods` in server
/// method (0x02; `negotiate_auth` walks `server_methods` in server
/// order; RFC 1928 §3 lets the server pick by its own preference, so
/// the method list is ordered more-secure/preferred first).
#[tokio::test]
@@ -128,7 +128,7 @@ async fn negotiation_password_server_prefers_password_over_no_auth() {
/// Version mismatch at the greeting (0x04) → typed refusal, no hang.
/// Upstream: `negotiate_auth` checks `version` immediately after
/// reading the 2-byte header, before any method bytes
/// (`src/server.rs:890-892`) — so a truncated greeting (no methods
/// (`src/server.rs:890-892`); so a truncated greeting (no methods
/// byte) still refuses on the version alone.
#[tokio::test]
async fn negotiation_version_mismatch_refuses_before_methods_byte() {
@@ -224,11 +224,11 @@ async fn password_auth_reject_writes_0x01_0xff_then_errors() {
}
// ---------------------------------------------------------------------------
// Group 2 — command-read edge cases
// Group 2: command-read edge cases
// ---------------------------------------------------------------------------
/// Unknown command byte (0x04+) → `UnknownCommand` → 0x07 reply. The
/// address is parsed *first* (upstream `src/server.rs:1018` — the
/// address is parsed *first* (upstream `src/server.rs:1018`: the
/// try_notify order), so the request must carry a valid target for
/// the reply to fire; the reply uses the error form (ATYP=1, zeros).
#[tokio::test]
@@ -255,7 +255,7 @@ async fn command_unknown_byte_replies_0x07() {
}
/// Reserved-byte tolerance: RSV ≠ 0 is read but never checked by
/// upstream (`src/server.rs:1001-1002` — destructured as `rsv`, only
/// upstream (`src/server.rs:1001-1002`, destructured as `rsv`, only
/// debug-logged; no validation). Pin the tolerance: a garbage RSV
/// byte still parses to a successful command read.
#[tokio::test]
@@ -340,9 +340,9 @@ async fn command_atyp_0x05_replies_0x08() {
/// Domain-length boundary: a 255-byte domain is the wire maximum and
/// parses fine (`read_address` reads a 1-byte len then that many
/// bytes — `src/util/target_addr.rs:236-245`; nothing else bounds
/// bytes (`src/util/target_addr.rs:236-245`; nothing else bounds
/// it). 256+ is rejected at *encode* time by `to_be_bytes`
/// (`src/util/target_addr.rs:113-115`) — parsing never sees it.
/// (`src/util/target_addr.rs:113-115`), so parsing never sees it.
#[tokio::test]
async fn command_domain_255_bytes_is_the_wire_maximum() {
let (mut client, server) = tokio::io::duplex(64);
@@ -363,13 +363,13 @@ async fn command_domain_255_bytes_is_the_wire_maximum() {
assert_eq!(target, TargetAddr::Domain("a".repeat(255), 443));
}
/// The decode-side bound is the wire's own 1-byte length field —
/// The decode-side bound is the wire's own 1-byte length field,
/// stated explicitly (review 001 S-1): `read_address`'s domain arm
/// allocates from an attacker-controlled byte, and the allocation is
/// safe *only* because the length field is one byte (≤ 255). This
/// test pins both faces of that invariant: (a) the 255-byte maximum
/// decodes through `read_address` (the attacker-side path), and (b)
/// the encoder rejects a 256-byte claim — a 256-byte domain cannot
/// the encoder rejects a 256-byte claim (a 256-byte domain cannot
/// enter the wire at all, so the decode arm's `len as usize` can
/// never ask for more than 255. Widening the length field (a
/// hypothetical FRAG-style extension) requires an explicit cap
@@ -399,7 +399,7 @@ async fn decode_side_domain_allocation_is_bounded_by_the_1_byte_wire_field() {
/// Truncated address fields (EOF mid-address) → typed
/// `AddrError`-wrapped error, surfaced through `try_notify!`'s
/// 0x01-coded secondary reply (AddrError non-ATYP variants map to
/// `ConnectionRefused` — `src/util/target_addr.rs:40-48`).
/// `ConnectionRefused`; `src/util/target_addr.rs:40-48`).
#[tokio::test]
async fn command_truncated_ipv4_address_is_typed_error_with_0x05_reply() {
let (mut client, server) = tokio::io::duplex(64);
@@ -453,7 +453,7 @@ async fn command_truncated_domain_content_is_typed_error() {
/// Version mismatch at the command phase (post-negotiation, the
/// 4-byte request header read) → typed refusal, no reply. Upstream
/// `read_command` checks `version` before parsing the address
/// (`src/server.rs:1010-1014`) — a distinct check from the greeting's
/// (`src/server.rs:1010-1014`), a distinct check from the greeting's
/// (`negotiate_auth`, `src/server.rs:889-892`), so this exercises the
/// command-phase arm in isolation.
#[tokio::test]
@@ -478,7 +478,7 @@ async fn command_version_mismatch_refuses_before_address_parse() {
}
// ---------------------------------------------------------------------------
// Group 3 — reply-form edge cases (the `new_reply` carried change's
// Group 3: reply-form edge cases (the `new_reply` carried change's
// regression guard)
// ---------------------------------------------------------------------------
@@ -564,7 +564,7 @@ async fn reply_error_form_carries_zero_addr() {
}
/// Success replies carry the caller's `sock_addr` verbatim *on the
/// wire* — `reply_success` does not substitute its own address
/// wire*; `reply_success` does not substitute its own address
/// (upstream `src/server.rs:931-941`; the 127.0.0.1 substitution is
/// the `run_tcp_proxy` caller's choice, `src/server.rs:1087`, not
/// the typestate's).
@@ -593,11 +593,11 @@ async fn reply_success_carries_caller_sock_addr_on_the_wire() {
}
// ---------------------------------------------------------------------------
// Group 4 — client-session edge cases
// Group 4: client-session edge cases
// ---------------------------------------------------------------------------
/// Reply#1 with a non-zero reply code surfaces as the mapped
/// `ReplyError` (the `from_u8` table — upstream
/// `ReplyError` (the `from_u8` table; upstream
/// `src/client.rs:363`). Covered per-code including the 0x06 double
/// mapping (ConnectionTimeout and TtlExpired both carry 0x06).
#[tokio::test]
@@ -713,7 +713,7 @@ async fn client_domain_request_encoding_is_len_prefixed() {
/// for non-ASSOCIATE commands was
/// `anyhow::Error::msg("target addr should be present")`
/// (`src/client.rs:285`); in-tree it is the typed
/// `SocksError::MissingTargetAddr` (rfc1928.md's adjustment list) —
/// `SocksError::MissingTargetAddr` (rfc1928.md's adjustment list),
/// pinned by the wire-vocabulary module's own test
/// (`mod.rs` tests: `socks_error_from_variants` keeps the variant
/// reachable; the private-API form of this test lives in
@@ -729,14 +729,14 @@ async fn client_udp_associate_zero_target_fallback_is_10_zero_bytes() {
let parsed = alksocks::rfc1928::util::target_addr::read_address(&mut addr_field, 0x01)
.await
.unwrap();
// The zero form decodes to the unspecified IPv4 addr, port 0 —
// The zero form decodes to the unspecified IPv4 addr, port 0;
// the wire meaning of the fallback.
assert_eq!(parsed.to_string(), "0.0.0.0:0");
assert_eq!(expected.len(), 10);
}
/// The client's v6 request encoding: ATYP=4, the full 16-byte
/// address, the 2-byte port, 22 bytes total — the `packet[4..20]`/
/// address, the 2-byte port, 22 bytes total: the `packet[4..20]`/
/// `packet[20..22]` slice boundaries pinned byte-for-byte. Upstream
/// `request_header`'s v6 arm (`src/client.rs:293-300`); the in-tree
/// arm is byte-identical (`client.rs`, `TargetAddr::Ip
@@ -780,7 +780,7 @@ async fn client_v6_request_encoding_is_22_bytes_with_atyp_4() {
/// `Socks5Stream`'s `AsyncWrite::poll_shutdown` passthrough reaches
/// the backing stream: `shutdown()` closes the write side and the
/// mock observes EOF — the half-close propagation shape the consumer
/// mock observes EOF, the half-close propagation shape the consumer
/// half's two-pump teardown relies on (the EOF-sentinel convention).
/// The passthrough is a 3-line delegate (`client.rs` impl block);
/// without this test a regression in the `poll_shutdown` arm (e.g.
@@ -802,7 +802,7 @@ async fn client_session_shutdown_propagates_to_the_backing_stream() {
.await?;
session.flush().await?;
session.shutdown().await?;
// After the shutdown the write side is closed — a further
// After the shutdown the write side is closed; a further
// write surfaces `BrokenPipe` (the passthrough's close
// semantics, tokio `DuplexStream`'s closed-writer shape).
match session.write_all(b"late").await {
@@ -863,7 +863,7 @@ async fn client_skip_auth_emits_no_greeting() {
}
// ---------------------------------------------------------------------------
// Group 5 — UDP codec edge cases
// Group 5: UDP codec edge cases
// ---------------------------------------------------------------------------
/// `parse_udp_request` with garbage RSV → `GarbageInReserved`. The
@@ -880,7 +880,7 @@ async fn udp_codec_garbage_reserved_is_rejected() {
));
}
/// FRAG ≠ 0 passes *through* the parser — the parser's contract is
/// FRAG ≠ 0 passes *through* the parser; the parser's contract is
/// to return the frag byte; refusing fragmentation is the *caller's*
/// job (upstream discards frag≠0 packets in `handle_udp_request`,
/// `src/server.rs:1236-1242`, not in `parse_udp_request`). Pin the
@@ -913,7 +913,7 @@ async fn udp_codec_round_trip_ipv6() {
}
/// The UDP header round-trip preserves a non-empty payload split
/// (header + data) — the wire frame is RSV+FRAG+target-bytes and the
/// (header + data): the wire frame is RSV+FRAG+target-bytes and the
/// parser returns the remainder as payload. Upstream's payload split
/// is the returned `req` slice (`src/lib.rs:342`).
#[tokio::test]
@@ -928,7 +928,7 @@ async fn udp_codec_round_trip_carries_payload() {
}
// ---------------------------------------------------------------------------
// Group 6 — `transfer`/`wait_on_tcp` semantics
// Group 6: `transfer`/`wait_on_tcp` semantics
// ---------------------------------------------------------------------------
/// `transfer` pumps both directions to completion. Upstream is a
@@ -972,7 +972,7 @@ async fn wait_on_tcp_returns_eof_on_clean_close() {
}
/// `wait_on_tcp` returns `UnexpectedUdpControlGarbage` on a control
/// byte — the received byte is carried in the error. Upstream
/// byte; the received byte is carried in the error. Upstream
/// `src/server.rs:1202`.
#[tokio::test]
async fn wait_on_tcp_carries_control_garbage_byte() {
@@ -990,10 +990,10 @@ async fn wait_on_tcp_carries_control_garbage_byte() {
/// stream" context. Upstream `src/server.rs:1203`
/// (`Err(err) => Err(err).err_when("waiting on UDP control stream")`).
/// Driven by a real error producer: writing to a duplex whose peer is
/// gone surfaces `BrokenPipe` on the *write* side — for the read arm
/// gone surfaces `BrokenPipe` on the *write* side; for the read arm
/// the harness swaps in a failing reader so the `Err(err)` shape is
/// exercised deterministically (a dropped-peer duplex read is a clean
/// EOF, `Ok(0)`, which is `SocksServerError::EOF` — the other arm).
/// EOF, `Ok(0)`, which is `SocksServerError::EOF` (the other arm).
#[tokio::test]
async fn wait_on_tcp_wraps_io_errors_with_context() {
struct FailingReader;