assembly: the cross-cutting integration suite + docs sync (batch 2 closes)

- tests/assembly-integration.rs (7 tests, local + composed): the full
  -D story (vanilla cred-bearing client -> TCP door + UDP door ->
  producer w/ local providers -> real echo targets; CONNECT + BIND +
  ASSOCIATE live, one-command-per-connection, teardown + fresh
  sessions), the composed story (direct_egress + bindfirst_accept +
  real TCP dial; all three commands; the lazy-table dial-count
  witness), mixed assembly (channels-native + door clients on one
  registration; composed seams beside native sessions), the assembly
  teardown matrix (UDP-door idle bound; channel reaped; fresh
  association after), the producer door driving all three commands,
  and the -D domain-target pin (resolution at the producer's seam,
  ADR-012).
- Fix the drift the suite surfaced: ComposedEgress::next_reply ended
  fresh (never-opened-table) associations at birth — the ended-relay
  Err now fires only once a tunnel existed and every relay ended; the
  fresh table parks (the readiness contract). Regression-pinned in
  tests/composed-egress.rs (fresh_association_parks_before_any_
  tunnel_is_opened).
- Harness: the composed-accept machinery single-sourced into
  tests/harness.rs (the composed suites' shapes + the five-gate
  composed identity); composed-accept.rs consumes the shared shapes.
- Docs sync: README Current State (the assembly layer landed; both
  suites named), lib.rs crate docs (the assembly-layer paragraph),
  consumer.md (the TokenClient door-only contract + the
  one-command-per-connection posture reconciled).
- Task assembly-integration-suite: completed; Notes + Summary filled
  (the local-on-wasm failure observed: mio's unsupported-wasm error).

Verification: cargo test (375), cargo test --all-features (615),
clippy --all-targets -D warnings (default + all-features), fmt --check,
doc --no-deps (0 warnings), both wasm checks (default + composed),
local-on-wasm fails as designed.
This commit is contained in:
glm-5.3-flash committed 2026-09-25 23:03:13 +00:00
1 parent 4e1673eefc
commit 1addedfdd0
9 files changed
+1921 -205

No files matched your search

+19 -10
View File
@@ -1,7 +1,8 @@
---
status: draft
last_updated: 2026-09-24 (the protocol core landed + the cross-cutting
integration suite; the rfc1928 doc's targeted fork 2026-09-22; the
last_updated: 2026-09-25 (the assembly layer landed + the cross-cutting
assembly integration suite; the protocol core landed + the cross-cutting
integration suite 2026-09-24; the rfc1928 doc's targeted fork 2026-09-22; the
vendor extraction amendment ADR-011 §Amendment 2026-09-21; Phase 1
initial draft 2026-09-20 — the OQ-SK ledger promoted into ADRs
001-012 and open-questions.md)
@@ -20,17 +21,17 @@ architecture spec docs and the ADRs.
## Current State
Phase 1 (Architecture) — the **protocol core is implemented**. Phase
Phase 1 (Architecture) — the **protocol core and the assembly layer
are implemented**. Phase
0 (`docs/research/phase-0.md`) is converged (2026-09-14) and
POC-complete (all numbered POCs passed or were dropped with the scope
decision); the OQ-SK ledger's Phase 1 inputs are collected and
promoted into ADRs 001–013. All docs are `draft` — they transition
promoted into ADRs 001–014. All docs are `draft` — they transition
to `reviewed` when their open questions resolve and the architecture
review passes.
The landed core (the protocol-only batch; no `local`/`composed` code
exists yet): the RFC 1928 core in `src/rfc1928/` (the targeted fork,
ADR-013 — the vendor group's six tasks landed and passed the
The landed surface: the RFC 1928 core in `src/rfc1928/` (the targeted
fork, ADR-013 — the vendor group's six tasks landed and passed the
extraction review), the ASSOCIATE datagram frame codec (`src/wire.rs`,
ADR-003), the open-op params + typed error surface (`src/params.rs` /
`src/error.rs`, ADR-001), the three seam types (`src/egress.rs`,
@@ -38,9 +39,17 @@ ADR-004), the producer half (`src/producer.rs` — the session-gate
establisher, the command-dispatch pump, the three commands, the scope
gates), and the consumer half (`src/consumer.rs` — the
connect/associate/bind session types with teardown ownership). The
batch-1 cross-cutting integration suite (`tests/core-integration.rs`)
pins the core's whole behavior end to end; the `local` and
`composed` provider groups follow (batch 2).
assembly layer (batch 2) landed with it: the `local` providers
(`src/local/` — tcp_dial / udp_egress / bind_accept, the producer-side
`ProducerDoor`, ADR-008/009), the front doors (`src/door.rs` — the
NoAuth raw-pipe + RFC 1929 cred-bearing TCP door, the BIND door, the
UDP door, ADR-005/003/014), and the composed providers
(`src/composed/` — the direct-op egress + the bind-first accept over
alktunnels 0.2.0, ADR-007/008). Both cross-cutting integration suites
pin the behavior end to end: `tests/core-integration.rs` (the
protocol core) and `tests/assembly-integration.rs` (the assembly
layer — the full `-D` story through the doors, the composed variant
set, mixed assembly, the assembly teardown matrix).
## Documents
+14 -2
View File
@@ -1,6 +1,8 @@
---
status: draft
last_updated: 2026-09-24 (the BIND spent posture reconciled — the
last_updated: 2026-09-25 (the cred-bearing door's call-connection
contract reconciled — TokenClient's door-only posture, one command per
accepted connection; the BIND spent posture reconciled — the
take-guarded reply#2 reader, review 002 C-1/D-1; the landed session
signatures reconciled 2026-09-24; Phase 1 initial draft 2026-09-20)
---
@@ -123,7 +125,17 @@ BIND door):
crosses the channel as a SOCKS password; the channel identity
never reaches the vanilla client. The door checks credentials
itself (no forked `AuthMethod` impl needed — the door owns the
accept loop).
accept loop). The mapped identity rides the open-op call's
`auth_token` payload (CF-005 precedence (1)) via
`TokenClient::wrap` — the client's channel-0 `CallConnection` is
taken once per door (the door-only contract: a cred-bearing
door's client is a door client; other shapes — the UDP door's
association, channels-native sessions — ride their own
connections to the same producer registration, one registration,
many connections). The vanilla conversation is
**one command per accepted connection** (the door's RFC half
consumes the greeting + one command; the data plane rides to
EOF).
Door variant selection (NoAuth vs cred-bearing; the UDP door's bind
address) is assembly-layer configuration.
+15 -2
View File
@@ -194,6 +194,14 @@ pub struct ComposedEgress {
/// The factory `Sender` alone does not close the queue (the
/// egress holds it) — the count is the design.
live_relays: usize,
/// Whether any tunnel has ever been opened. The lazy table starts
/// empty: a fresh association's `next_reply` parks until the first
/// `send` opens a tunnel (the producer's stage selects the channel
/// branch first on a quiescent association — the association
/// serves until a datagram arrives or the channel EOFs). The
/// ended-relay `Err` fires only once at least one tunnel existed
/// and every relay has ended (the closed-queue condition).
ever_opened: bool,
}
/// The shared inbound queue's message: a tunnel's datagram (keyed by
@@ -281,6 +289,7 @@ impl ComposedEgress {
inbound: rx,
inbound_tx: tx,
live_relays: 0,
ever_opened: false,
}
}
@@ -306,6 +315,7 @@ impl ComposedEgress {
// bug). The relay task owns the bare read half.
tokio::spawn(relay_read(taken.read, self.inbound_tx.clone(), key.clone()));
self.live_relays += 1;
self.ever_opened = true;
self.entries.insert(
key.clone(),
TableEntry {
@@ -368,11 +378,14 @@ impl Egress for ComposedEgress {
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
}
}
if self.live_relays == 0 {
if self.ever_opened && self.live_relays == 0 {
// Every relay ended (the tunnels ended): a closed
// relay set must NOT spin the stage's select (an
// instantly-ready None would starve the channel
// branch) — end the association instead.
// branch) — end the association instead. A fresh
// association (no tunnel ever opened) parks here —
// the channel branch governs until the first
// datagram arrives.
return Err(EgressError::General("all tunnel relays ended".into()));
}
match self.inbound.recv().await {
+20 -11
View File
@@ -40,21 +40,30 @@
//! sessions — [`Socks5ChannelSession`] (CONNECT),
//! [`AssociateSession`] (the framed datagram stage),
//! [`BindSession`] (the reply#1/reply#2 reader) — each owning its
//! channel's teardown; no `Clone`.
//! channel's teardown; no `Clone`. The cred-bearing front door's
//! re-origination rides the token-carrying surface (`TokenClient`
//! + the `*_with_token` constructor twins).
//!
//! `local` (the socket-backed assembly-layer providers —
//! [`crate::local::tcp_dial`] / [`crate::local::udp_egress`] /
//! [`crate::local::bind_accept`], ADR-008/009) is feature-gated: the
//! default crate is protocol-only and wasm-clean by construction, and
//! the composed providers ride `composed`
//! ([`crate::composed::direct_egress`] — the alktunnels direct-op
//! egress, ADR-008/009).
//! The assembly layer (feature-gated, ADR-008/009): `local` (the
//! socket-backed providers — `local::tcp_dial` / `local::udp_egress`
//! / `local::bind_accept` — plus the producer-side
//! `local::ProducerDoor`) and `door` (the consumer's front doors —
//! the NoAuth raw-pipe + RFC 1929 cred-bearing `FrontDoor`, the
//! `UdpFrontDoor` with the RFC codec at the door boundary only,
//! ADR-005/003/014) ride `local`; the composed providers
//! (`composed::direct_egress` — the alktunnels direct-op egress,
//! `composed::bindfirst_accept` — the bind-first accept,
//! ADR-007/008) ride `composed`. The default crate is protocol-only
//! and wasm-clean by construction.
//!
//! Architecture: `docs/architecture/overview.md` (spec set:
//! wire.md, producer.md, consumer.md, rfc1928.md); the cross-cutting
//! behavior is pinned end to end by `tests/core-integration.rs`
//! (one registration, all three commands; concurrency; the teardown
//! matrix; the identity/scope matrix; the readiness contract).
//! behavior is pinned end to end by `tests/core-integration.rs` (the
//! protocol core: one registration, all three commands; concurrency;
//! the teardown matrix; the identity/scope matrix; the readiness
//! contract) and `tests/assembly-integration.rs` (the assembly
//! layer: the full `-D` story through the doors, the composed
//! variant set, mixed assembly, the assembly teardown matrix).
#![forbid(unsafe_code)]
@@ -1,7 +1,7 @@
---
id: implementation/assembly-integration-suite
name: Assembly — the cross-cutting integration suite + docs sync
status: pending
status: completed
depends_on: [implementation/door-udp, implementation/local-backend-producer, implementation/composed-accept]
scope: moderate
risk: medium
@@ -61,17 +61,17 @@ Deliverables:
## Acceptance Criteria
- [ ] The full `-D` story (vanilla client → door → session →
- [x] The full `-D` story (vanilla client → door → session →
producer → real target) pinned end to end for all three
commands
- [ ] The composed variant set pinned end to end (all three commands)
- [ ] Mixed assembly (doors + channels-native + composed) concurrent
- [x] The composed variant set pinned end to end (all three commands)
- [x] Mixed assembly (doors + channels-native + composed) concurrent
on one producer — pinned
- [ ] The assembly teardown matrix pinned (door disconnects, idle
- [x] The assembly teardown matrix pinned (door disconnects, idle
bounds, fresh-session-after-teardown)
- [ ] All four verification targets green (default, all-features,
- [x] All four verification targets green (default, all-features,
wasm default, wasm composed)
- [ ] Docs synced (README Current State, lib.rs crate docs, any
- [x] Docs synced (README Current State, lib.rs crate docs, any
drift reconciled)
## References
@@ -86,8 +86,118 @@ Deliverables:
## Notes
> To be filled by implementation agent
> Filled by the implementation agent (2026-09-25).
- **The spec drift the implementation surfaced (one real drift,
reconciled in `src/composed/mod.rs`):** `ComposedEgress::next_reply`
returned the association-end `Err` ("all tunnel relays ended") on a
*fresh* association too — the lazy table starts with `live_relays ==
0`, so the producer's datagram stage (which parks on `next_reply`
when no datagram has arrived) ended every quiescent association at
birth. The composed-egress suite drove the egress send-first and
never hit it; the assembly suite's `AssociateSession` (open → the
stage parks → the first datagram) did. Fix: an `ever_opened` flag —
the ended-relay `Err` fires only once at least one tunnel existed
and every relay has ended; a never-opened table parks (the readiness
contract's `Pending`, the channel branch governs). The landed spec
wording (producer.md: "`Pending` when nothing is ready") already
stated the correct contract — the implementation was the divergent
side. Regression-pinned in `tests/composed-egress.rs`
(`fresh_association_parks_before_any_tunnel_is_opened`).
- **Two assembly-shape findings recorded in the tests (not code
changes — the landed contracts are correct):**
- The cred-bearing door's `TokenClient::wrap` takes the client's
channel-0 `CallConnection` once (the door-only contract,
door-tcp's Notes) — the UDP door's association (and any other
consumption shape) rides its own channels connection to the same
producer registration. The suite pins the assembly story as "one
registration, many connections" (consumer.md reconciled).
- The RFC conversation is **one command per connection** at every
door shape (the cred-bearing TCP door's RFC half consumes the
greeting + one command; the producer-side `ProducerDoor`'s
`serve_socks5_over` likewise serves one command per accepted
socket — a second command's bytes ride the first session's data
plane). The vanilla one-command-per-connection shape is pinned by
the suite's structure (each command opens its own door
connection).
- **Harness consolidation (review 002 N-5's discipline):** the
composed-accept machinery the assembly suite also consumes (the
test bind, the accept queue + loop, the channel-set probe, the
identities) moved from `tests/composed-accept.rs` into
`tests/harness.rs` (single-sourced; `composed-accept.rs` consumes
the same shapes via the harness re-exports — its test bodies are
unchanged in behavior). The five-gate composed identity
(`composed_full_opener_identity` — `tunnel:open` + `tunnel:direct`
+ `tunnel:forwarded` + `socks5:open` + `socks5:bind`) is new.
- **The wasm gate, observed**: `cargo check --features local --target
wasm32-unknown-unknown` fails as designed (the alktty posture) —
the first error is `error: This wasm target is unsupported by mio.
If using Tokio, disable the net feature.` (mio v1.2.3, via
`tokio/net` which `local` enables), followed by the mio `sys`
module's unresolved imports (`crate::sys::IoSourceState`,
`crate::sys::tcp`, `Selector`, `event`). Both default and
`--features composed` wasm checks are green.
- **The `-D` story's door wiring**: the cred-bearing TCP door + the
UDP door each take their own `ChannelClient` (two connections to
the one registration); the tests' `DoorTokenProvider` resolves the
mapped `door-`-prefixed identity ids (the same producer-side
resolution policy shape the door-tcp suite pins). The producer-door
test drives the producer's own front door (`ProducerDoor` →
`serve_socks5_over` over the `local` provider set) through all
three commands.
## Summary
> To be filled on completion
> Filled on completion (2026-09-25).
Landed the batch-2 closing task: `tests/assembly-integration.rs` —
seven cross-cutting integration tests pinning the whole assembly layer
end to end (feature `local` + `composed`):
1. **The full `-D` story** (`full_dash_d_story_...`): a vanilla
tun2proxy-shaped client through the cred-bearing RFC 1929 TCP door
+ the UDP door against one producer registration with the `local`
providers and real echo targets — CONNECT + BIND (the app server
dials the advertised address; reply#2 carries the real peer) +
ASSOCIATE (the RFC-header'd datagram through the relay socket)
live simultaneously, each exercised mid-life of the others, the
door-client disconnect ending only its own sessions, fresh
sessions succeeding after (no connection poisoning).
2. **The composed story** (`composed_story_...`): the producer with
`composed::direct_egress` + `composed::bindfirst_accept` + a real
TCP dial — CONNECT, ASSOCIATE (the lazy table; the direct-op dial
count witnesses the lazy open), BIND (the real `bound` + the real
`peer` + the pass-through plane) — all three commands, all live.
3. **Mixed assembly, twice**: channels-native sessions + door clients
concurrent on one registration (the doors on their own
connections), and composed seams beside channels-native sessions
(the composed suite's producer; teardowns end only their own
sessions; the channel-set probe after the settle).
4. **The teardown matrix, assembly edition**: the UDP door's idle
bound fires on the vanished door client, the association's channel
reaps (the channel count witnesses), a fresh association succeeds
immediately (no connection poisoning).
5. **The producer's own front door** (`ProducerDoor`) drives all
three commands through one door + one registration (the `-D`
story's other half; the faithful-RFC BIND variant).
6. **The `-D` domain-target pin**: a domain-form CONNECT through the
NoAuth raw-pipe door resolves at the producer's seam (ADR-012).
The reconciled drift: `ComposedEgress`'s never-opened-table association
end (fixed in `src/composed/mod.rs` — the `ever_opened` guard;
regression-pinned in `tests/composed-egress.rs`). Docs synced:
`README.md` Current State (the assembly layer landed; both suites
named), `lib.rs` crate docs (the assembly-layer paragraph + both
suites named), `consumer.md` §Door variants (the `TokenClient`
door-only contract + one-command-per-connection posture reconciled —
the drift the suite surfaced, recorded in Notes). Harness: the
composed-accept machinery single-sourced into `tests/harness.rs` (the
N-5 discipline).
Verification: `cargo test` (375 green across 12 targets), `cargo test
--all-features` (615 green), `cargo clippy --all-targets -- -D
warnings` + `cargo clippy --all-targets --all-features -- -D warnings`
clean, `cargo fmt --check` clean, `cargo doc --no-deps` clean (0
warnings), both wasm checks green (default + `--features composed`,
check + clippy), and the `local`-on-wasm build failure observed +
recorded (the alktty posture — mio's unsupported-wasm error).
File diff suppressed because it is too large. Load diff
+30 -171
View File
@@ -17,151 +17,43 @@
mod harness;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use alksocks::composed::{bindfirst_accept, register_forwarded_serving};
use alksocks::consumer::BindSession;
use alksocks::rfc1928::util::target_addr::ToTargetAddr;
use alktunnels::params::{Substrate, SubstrateAddr};
use alktunnels::producer::{
BindFirstAcceptLoop, BindFn, BoundListener, ResourceRegistry, TargetHandle,
TunnelEstablishError,
};
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
use alktunnels::params::Substrate;
use alktunnels::producer::ResourceRegistry;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Notify;
/// The resource the bind-first producer serves (the tests' only
/// subscription — the registry namespace gate).
const RESOURCE: &str = "bind";
/// 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).
const BOUND_PORT: u16 = 42113;
/// The peer address the accepted pairs report (the "app server" — the
/// forwarded op's `peer` param carries it to reply#2).
const PEER_PORT: u16 = 55231;
/// The test bind (the assembly's bind seam): reports the test's
/// bound address + an opaque listener witness.
fn test_bind() -> BindFn {
Arc::new(|_substrate, _backing| {
Box::pin(async {
Ok(BoundListener {
listener: Arc::new(()) as Arc<dyn std::any::Any + Send + Sync>,
bound: SubstrateAddr::HostPort {
host: "127.0.0.1".to_string(),
port: BOUND_PORT,
},
})
})
})
}
/// The accept-queue pair the test accept loop pops from (the alktunnels
/// listener test's shape: a VecDeque + Notify — one accepted pair per
/// dispatcher call).
type AcceptQueue = (
Arc<tokio::sync::Mutex<std::collections::VecDeque<(SubstrateAddr, TargetHandle)>>>,
Arc<tokio::sync::Notify>,
);
fn fresh_queue() -> AcceptQueue {
(
Arc::new(tokio::sync::Mutex::new(std::collections::VecDeque::new())),
Arc::new(Notify::new()),
)
}
/// 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).
fn accepted_halves(peer_addr: (&str, u16)) -> (SubstrateAddr, TargetHandle) {
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
let (mut t_read, mut t_write) = tokio::io::split(target_side);
tokio::spawn(async move {
let mut buf = vec![0u8; 8192];
loop {
match t_read.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if t_write.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (c_read, c_write) = tokio::io::split(consumer_side);
(
SubstrateAddr::HostPort {
host: peer_addr.0.to_string(),
port: peer_addr.1,
},
TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
},
)
}
/// The test accept loop (the dispatcher invokes the closure per
/// accept): pop one accepted pair, or park until notified.
fn queue_loop(queue: AcceptQueue) -> BindFirstAcceptLoop {
let (queue, notify) = queue;
Arc::new(move |_listener| {
let queue = Arc::clone(&queue);
let notify = Arc::clone(&notify);
Box::pin(async move {
let popped = {
let notified = std::pin::pin!(notify.notified());
let mut q = queue.lock().await;
if let Some(pair) = q.pop_front() {
Some(pair)
} else {
drop(q);
notified.await;
let mut q = queue.lock().await;
q.pop_front()
}
};
popped.ok_or(TunnelEstablishError::DialFailed(
"listener closed".to_string(),
))
})
})
}
/// Push an accepted pair into the test's accept queue.
async fn push_accept(queue: &AcceptQueue, peer: (&str, u16)) {
let (peer_addr, handle) = accepted_halves(peer);
queue.0.lock().await.push_back((peer_addr, handle));
queue.1.notify_waiters();
}
use harness::{
assert_composed_channels, composed_fresh_queue, composed_opener_identity, composed_push_accept,
composed_queue_loop, composed_scopeless_opener_identity, composed_test_bind,
wire_bindfirst_composed, COMPOSED_BOUND_PORT, COMPOSED_PEER_PORT, COMPOSED_RESOURCE,
};
/// The full wiring: the bind-first topology with the composed accept
/// factory + the forwarded serving registered post-hoc on the opener's
/// registry. Returns everything the tests drive.
struct Suite {
topo: harness::BindFirstTopology,
queue: AcceptQueue,
queue: harness::ComposedAcceptQueue,
}
async fn wire_suite() -> Suite {
wire_suite_with_identity(harness::composed_opener_identity()).await
wire_suite_with_identity(composed_opener_identity()).await
}
async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Suite {
let resource_registry = ResourceRegistry::new();
resource_registry
.register(RESOURCE, Substrate::Tcp, "assembly-owned")
.register(COMPOSED_RESOURCE, Substrate::Tcp, "assembly-owned")
.await;
let queue = fresh_queue();
let topo = harness::wire_bindfirst_composed(
let queue = composed_fresh_queue();
let topo = wire_bindfirst_composed(
resource_registry,
test_bind(),
queue_loop(queue.clone()),
composed_test_bind(),
composed_queue_loop(queue.clone()),
None, // the SOCKS5 openable registers post-hoc, below — the composed factory needs the opener's client
identity,
)
@@ -180,7 +72,7 @@ async fn wire_suite_with_identity(identity: alkcall::core::auth::Identity) -> Su
// 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(Arc::clone(&topo.opener), Arc::clone(&registry), RESOURCE);
let factory = bindfirst_accept(topo.opener.clone(), registry, COMPOSED_RESOURCE);
let core = alkcall::channels::operations::ChannelCore::new(
topo.producer_manager.clone(),
alkcall::channels::policy::default_policy(),
@@ -212,33 +104,6 @@ async fn bind_session(suite: &Suite) -> BindSession {
.expect("the session binds through the composed accept")
}
/// The 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 (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 listener's teardown; upstream's own bind-first
/// suites make the same concession in `no_stray_channels`).
async fn assert_only_channels(
topo: &harness::BindFirstTopology,
opener_extra: &[u32],
producer_extra: &[u32],
) {
let opener_ids = topo.opener.manager().channel_ids();
let producer_ids = topo.producer_manager.channel_ids();
let opener_ok = opener_ids
.iter()
.all(|id| *id == 0 || opener_extra.contains(id));
let producer_ok = producer_ids
.iter()
.all(|id| *id == 0 || *id % 2 == 1 || producer_extra.contains(id));
assert!(
opener_ok && producer_ok,
"channel sets: opener={opener_ids:?} producer={producer_ids:?}"
);
}
/// 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
@@ -268,10 +133,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
let bind_addr = session.reply1().expect("reply#1").bind_addr;
assert_eq!(
bind_addr,
alksocks::rfc1928::util::target_addr::TargetAddr::Ip(SocketAddr::from((
[127, 0, 0, 1],
BOUND_PORT
))),
TargetAddr::Ip(SocketAddr::from(([127, 0, 0, 1], COMPOSED_BOUND_PORT))),
"reply#1 advertises the real OS-chosen bound"
);
@@ -279,7 +141,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
// 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.
push_accept(&suite.queue, ("10.0.0.1", PEER_PORT)).await;
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
let mut plane = tokio::time::timeout(Duration::from_secs(10), session.await_inbound())
.await
.expect("reply#2 within timeout")
@@ -287,10 +149,7 @@ async fn composed_bind_full_story_bound_peer_and_round_trip() {
.expect("the plane exists");
assert_eq!(
plane.peer,
alksocks::rfc1928::util::target_addr::TargetAddr::Ip(SocketAddr::from((
[10, 0, 0, 1],
PEER_PORT
))),
TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], COMPOSED_PEER_PORT))),
"reply#2 carries the forwarded op's real peer"
);
@@ -324,10 +183,10 @@ 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_only_channels`
// 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_only_channels(&suite.topo, &[0], &[channel_id]).await;
assert_composed_channels(&suite.topo, &[0], &[channel_id]).await;
}
/// The client-gone teardown (the drop-witness): dropping the BIND
@@ -382,7 +241,7 @@ async fn client_gone_teardown_releases_the_listener() {
suite.queue.0.lock().await.is_empty(),
"the dropped listener consumed nothing"
);
assert_only_channels(&suite.topo, &[0], &[0]).await;
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
/// Two concurrent BIND sessions get distinct tokens (the mint is
@@ -401,8 +260,8 @@ async fn concurrent_bind_sessions_get_distinct_tokens() {
// distinct tokens — observably: both sessions resolve their own
// forwarded sessions, the correlation delivered each to the right
// waiter).
push_accept(&suite.queue, ("10.0.0.1", PEER_PORT)).await;
push_accept(&suite.queue, ("10.0.0.2", PEER_PORT + 1)).await;
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
composed_push_accept(&suite.queue, ("10.0.0.2", COMPOSED_PEER_PORT + 1)).await;
let mut p1 = tokio::time::timeout(Duration::from_secs(10), s1.await_inbound())
.await
@@ -442,7 +301,7 @@ async fn concurrent_bind_sessions_get_distinct_tokens() {
assert!(s1.close().await);
assert!(s2.close().await);
tokio::time::sleep(Duration::from_millis(300)).await;
assert_only_channels(&suite.topo, &[0], &[0]).await;
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
/// A forwarded-open failure is per-accept isolated: an opener identity
@@ -452,13 +311,13 @@ async fn concurrent_bind_sessions_get_distinct_tokens() {
/// usable, and the failure does not tear the listener down.
#[tokio::test]
async fn forwarded_open_failure_is_per_accept_isolated() {
let suite = wire_suite_with_identity(harness::composed_scopeless_opener_identity()).await;
let suite = wire_suite_with_identity(composed_scopeless_opener_identity()).await;
let mut session = bind_session(&suite).await;
let _ = session.reply1().expect("reply#1");
// The accept fires (the queue accept consumed — the producer's
// accept loop is alive), but the forwarded open fails FORBIDDEN.
push_accept(&suite.queue, ("10.0.0.1", PEER_PORT)).await;
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
@@ -468,7 +327,7 @@ async fn forwarded_open_failure_is_per_accept_isolated() {
// accepts and the listener survives (the alktunnels producer
// suite's `forwarded_open_failure_isolates_the_accept` pin, end to
// end).
push_accept(&suite.queue, ("10.0.0.1", PEER_PORT)).await;
composed_push_accept(&suite.queue, ("10.0.0.1", COMPOSED_PEER_PORT)).await;
tokio::time::sleep(Duration::from_millis(300)).await;
assert!(
suite.queue.0.lock().await.is_empty(),
@@ -493,10 +352,10 @@ async fn late_forwarded_open_after_client_gone_drops_the_channel() {
// arriving in the race window may reach the dispatcher before the
// EOF sentinel does; the forwarded open then finds a dead oneshot
// (the correlation miss) and drops the channel.
push_accept(&suite.queue, ("10.0.0.1", PEER_PORT)).await;
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
// way: no leaked channels on the opener side.
assert_only_channels(&suite.topo, &[0], &[0]).await;
assert_composed_channels(&suite.topo, &[0], &[0]).await;
}
+27
View File
@@ -161,6 +161,33 @@ 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
/// 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.
#[tokio::test]
async fn fresh_association_parks_before_any_tunnel_is_opened() {
let dial_count = Arc::new(AtomicUsize::new(0));
let topo = wire_direct(counting_udp_echo_dial(Arc::clone(&dial_count))).await;
let mut egress = egress_for(&topo, 8).await;
// The never-opened table: `next_reply` parks (Pending), never an
// instantly-ready `Err` (the stage would end the association
// before its first datagram).
{
let mut poll = egress.next_reply();
tokio::time::timeout(std::time::Duration::from_millis(100), &mut poll)
.await
.expect_err("the fresh association's next_reply parks");
}
// After the first datagram opens a tunnel, the normal contract
// holds (the round-trip works).
round_trip(egress.as_mut(), &dest(53), b"after").await;
assert_eq!(dial_count.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn lru_eviction_reopens_evicted_destinations() {
let dial_count = Arc::new(AtomicUsize::new(0));
+176
View File
@@ -1211,6 +1211,182 @@ 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
/// 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 {
Identity {
id: "composed-opener-full".to_string(),
scopes: vec![
alktunnels::TUNNEL_OPEN_SCOPE.to_string(),
alktunnels::TUNNEL_DIRECT_SCOPE.to_string(),
alktunnels::TUNNEL_FORWARDED_SCOPE.to_string(),
SOCKS5_OPEN_SCOPE.to_string(),
SOCKS5_BIND_SCOPE.to_string(),
],
resources: HashMap::new(),
}
}
// --- 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-accept + assembly-integration) drive the same shapes.
/// The resource the bind-first producer serves (the composed suites'
/// 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 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
/// forwarded op's `peer` param carries it to reply#2).
pub const COMPOSED_PEER_PORT: u16 = 55231;
/// The test bind (the assembly's bind seam): reports the test's
/// bound address + an opaque listener witness.
pub fn composed_test_bind() -> alktunnels::producer::BindFn {
Arc::new(|_substrate, _backing| {
Box::pin(async {
Ok(alktunnels::producer::BoundListener {
listener: Arc::new(()) as Arc<dyn std::any::Any + Send + Sync>,
bound: alktunnels::params::SubstrateAddr::HostPort {
host: "127.0.0.1".to_string(),
port: COMPOSED_BOUND_PORT,
},
})
})
})
}
/// The accept-queue pair the test accept loop pops from (the alktunnels
/// listener test's shape: a VecDeque + Notify — one accepted pair per
/// dispatcher call).
pub type ComposedAcceptQueue = (
Arc<
tokio::sync::Mutex<
std::collections::VecDeque<(
alktunnels::params::SubstrateAddr,
alktunnels::producer::TargetHandle,
)>,
>,
>,
Arc<tokio::sync::Notify>,
);
pub fn composed_fresh_queue() -> ComposedAcceptQueue {
(
Arc::new(tokio::sync::Mutex::new(std::collections::VecDeque::new())),
Arc::new(tokio::sync::Notify::new()),
)
}
/// 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).
fn composed_accepted_halves(
peer_addr: (&str, u16),
) -> (
alktunnels::params::SubstrateAddr,
alktunnels::producer::TargetHandle,
) {
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
let (mut t_read, mut t_write) = tokio::io::split(target_side);
tokio::spawn(async move {
let mut buf = vec![0u8; 8192];
loop {
match t_read.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if t_write.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (c_read, c_write) = tokio::io::split(consumer_side);
(
alktunnels::params::SubstrateAddr::HostPort {
host: peer_addr.0.to_string(),
port: peer_addr.1,
},
alktunnels::producer::TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
},
)
}
/// The test accept loop (the dispatcher invokes the closure per
/// accept): pop one accepted pair, or park until notified.
pub fn composed_queue_loop(
queue: ComposedAcceptQueue,
) -> alktunnels::producer::BindFirstAcceptLoop {
let (queue, notify) = queue;
Arc::new(move |_listener| {
let queue = Arc::clone(&queue);
let notify = Arc::clone(&notify);
Box::pin(async move {
let popped = {
let notified = std::pin::pin!(notify.notified());
let mut q = queue.lock().await;
if let Some(pair) = q.pop_front() {
Some(pair)
} else {
drop(q);
notified.await;
let mut q = queue.lock().await;
q.pop_front()
}
};
popped.ok_or(alktunnels::TunnelEstablishError::DialFailed(
"listener closed".to_string(),
))
})
})
}
/// Push an accepted pair into the test's accept queue.
pub async fn composed_push_accept(queue: &ComposedAcceptQueue, peer: (&str, u16)) {
let (peer_addr, handle) = composed_accepted_halves(peer);
queue.0.lock().await.push_back((peer_addr, handle));
queue.1.notify_waiters();
}
/// 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
/// (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
/// listener's teardown; upstream's own bind-first suites make the
/// same concession in `no_stray_channels`).
pub async fn assert_composed_channels(
topo: &BindFirstTopology,
opener_extra: &[u32],
producer_extra: &[u32],
) {
let opener_ids = topo.opener.manager().channel_ids();
let producer_ids = topo.producer_manager.channel_ids();
let opener_ok = opener_ids
.iter()
.all(|id| *id == 0 || opener_extra.contains(id));
let producer_ok = producer_ids
.iter()
.all(|id| *id == 0 || *id % 2 == 1 || producer_extra.contains(id));
assert!(
opener_ok && producer_ok,
"channel sets: opener={opener_ids:?} producer={producer_ids:?}"
);
}
#[cfg(test)]
mod tests {
use super::*;