door: the TCP front door — NoAuth raw pipe + RFC 1929 cred-bearing + BIND

The consumer's TCP front door (feature `local`, ADR-005/ADR-014): the
ssh `-D` local exposure vanilla clients point at.

- `src/door.rs`: `FrontDoor::builder().credentials(...).bind(...)`
  → `serve`/`serve_on`. The NoAuth door is the literal raw pipe
  (accept → channel BiStream via copy_bidirectional, zero protocol
  translation). The cred-bearing door terminates RFC 1929 locally
  (the forked accept_password_auth shape) and re-originates in-band
  via the token-carrying consumer sessions — the vanilla replies are
  re-encoded at the door (the RFC codec boundary, ADR-003), the
  password never crosses the channel as a SOCKS password. The BIND
  door rides BindSession with reply#1 fidelity (the advertised
  address passes through unchanged). UDP ASSOCIATE at the TCP door
  refuses RFC-faithfully (0x07 — the UDP door's task). The ADR-014
  accept-path defense: the idle-first-byte window (peek-based,
  door-side close) + the max-sessions cap (semaphore) as constructor
  config; per-accept failure isolation (the loop continues).
- The identity mechanism (decided + recorded in the task Notes): the
  per-credential identity rides the open-op `auth_token` payload
  (CF-005 precedence (1)) — the token is the mapped identity's id;
  the producer resolves it through ServingConfig.identity_provider.
  No per-credential ChannelClient.
- `src/consumer.rs`: `TokenClient` (the token-carrying open surface
  over the taken CallConnection + shared manager) and the
  `*_with_token` twins of the three session constructors through the
  open_with_token_and_negotiate prologue (teardown-at-birth
  preserved); `Socks5ChannelSession::reply_addr` (the CONNECT
  reply's BND.ADDR for the door's re-encoding). Existing surfaces
  untouched.
- `tests/door-tcp.rs`: 10 integration tests — the NoAuth raw-pipe
  round-trip (the POC #1 curl shape), good/bad credentials (the
  establisher witness sees the mapped identity, never the password;
  a rejection opens no channel), the never-pipes-raw-post-auth pin,
  the BIND door's vanilla conversation, the idle timeout, the
  session cap, failure isolation, and the native-session/BIND-token
  regression guards. Harness: wire_consumer_side_with_provider (the
  producer's identity provider configurable).

Verification: cargo test (375) + --all-features (423) green; clippy
-D warnings clean (default + all-features); fmt --check clean; wasm
check + wasm clippy green; default graph stays net-free.
This commit is contained in:
glm-5.3-flash committed 2026-09-25 12:37:23 +00:00
1 parent f3c371310c
commit b8fd75b9f3
6 files changed
+1905 -18

No files matched your search

+858
View File
@@ -0,0 +1,858 @@
//! The TCP front door's integration tests (feature `local`): the
//! NoAuth raw pipe with a vanilla RFC 1928 client (the POC #1 curl
//! shape), the cred-bearing door's RFC 1929 (good creds → CONNECT
//! round-trip; bad creds → rejection with no channel opened; the
//! command bytes never reach the producer as greeting bytes), the
//! BIND door (reply#1/reply#2 fidelity), the ADR-014 accept-path
//! defense (the idle-first-byte timeout + the session cap), and the
//! per-connection failure isolation.
#![cfg(feature = "local")]
mod harness;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::timeout;
use alkcall::core::auth::{Identity, IdentityProvider};
use alksocks::consumer::{BindSession, Socks5ChannelSession};
use alksocks::rfc1928::consts;
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
use alksocks::{FrontDoor, SOCKS5_BIND_SCOPE};
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 —
/// 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`
/// (plus `socks5:bind` when the entry is bind-scoped); anything else
/// resolves to none (the dispatch falls through to the transport
/// identity per the CF-005 precedence order).
#[derive(Clone)]
struct DoorTokenProvider {
bind_scoped: Vec<String>,
}
impl IdentityProvider for DoorTokenProvider {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, token: &alkcall::core::auth::AuthToken) -> Option<Identity> {
let id = String::from_utf8_lossy(&token.raw).into_owned();
if id.starts_with("door-") {
let mut scopes = vec![alksocks::SOCKS5_OPEN_SCOPE.to_string()];
if self.bind_scoped.contains(&id) {
scopes.push(SOCKS5_BIND_SCOPE.to_string());
}
return Some(Identity {
id,
scopes,
resources: Default::default(),
});
}
None
}
}
/// An echo TCP server on an ephemeral loopback port (the vanilla
/// 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");
tokio::spawn(async move {
loop {
let Ok((mut stream, _)) = listener.accept().await else {
return;
};
tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match stream.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if stream.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
}
});
addr
}
/// The door's raw RFC 1928 client (the differential-suite pattern): a
/// `TcpStream` dialed to the door, greeting + method selection +
/// command + reply read.
struct VanillaClient {
stream: TcpStream,
}
impl VanillaClient {
async fn dial(addr: SocketAddr) -> Self {
Self {
stream: TcpStream::connect(addr).await.expect("dial the door"),
}
}
/// 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
.write_all(&[0x05, 0x01, 0x02])
.await
.expect("greeting written");
let mut chosen = [0u8; 2];
self.stream
.read_exact(&mut chosen)
.await
.expect("method selected");
assert_eq!(chosen[0], consts::SOCKS5_VERSION);
chosen[1]
}
/// Greeting `05 01 00` (no-auth offered) → the method-selection
/// read.
async fn greet_no_auth(&mut self) -> u8 {
self.stream
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting written");
let mut chosen = [0u8; 2];
self.stream
.read_exact(&mut chosen)
.await
.expect("method selected");
assert_eq!(chosen[0], consts::SOCKS5_VERSION);
chosen[1]
}
/// The RFC 1929 credential sub-negotiation → the status byte.
async fn send_credentials(&mut self, user: &str, pass: &str) -> u8 {
let mut packet = vec![0x01, user.len() as u8];
packet.extend_from_slice(user.as_bytes());
packet.push(pass.len() as u8);
packet.extend_from_slice(pass.as_bytes());
self.stream.write_all(&packet).await.expect("creds sent");
let mut status = [0u8; 2];
self.stream.read_exact(&mut status).await.expect("status");
assert_eq!(status[0], 0x01, "the sub-negotiation version is 1");
status[1]
}
/// A CONNECT request (v4 target) → the reply code.
async fn connect_v4(&mut self, target: SocketAddr) -> u8 {
let mut req = [0u8; 10];
req[..3].copy_from_slice(&[0x05, consts::SOCKS5_CMD_TCP_CONNECT, 0x00]);
req[3] = 0x01;
let SocketAddr::V4(v4) = target else {
panic!("v4 targets only");
};
req[4..8].copy_from_slice(&v4.ip().octets());
req[8..10].copy_from_slice(&v4.port().to_be_bytes());
self.stream.write_all(&req).await.expect("request sent");
let (code, _addr) = self.read_reply_addr().await;
code
}
/// A BIND request (v4 target) → reply#1's code + the advertised
/// BND.ADDR.
async fn bind_v4(&mut self, ip: [u8; 4], port: u16) -> (u8, TargetAddr) {
let mut req = [0u8; 10];
req[..3].copy_from_slice(&[0x05, consts::SOCKS5_CMD_TCP_BIND, 0x00]);
req[3] = 0x01;
req[4..8].copy_from_slice(&ip);
req[8..10].copy_from_slice(&port.to_be_bytes());
self.stream.write_all(&req).await.expect("bind sent");
self.read_reply_addr().await
}
/// Read one reply (any BND.ADDR form) → (code, addr).
async fn read_reply_addr(&mut self) -> (u8, TargetAddr) {
let mut head = [0u8; 4];
self.stream.read_exact(&mut head).await.expect("reply head");
assert_eq!(head[0], consts::SOCKS5_VERSION, "reply version 5");
let addr = match head[3] {
consts::SOCKS5_ADDR_TYPE_IPV4 => {
let mut rest = [0u8; 6];
self.stream.read_exact(&mut rest).await.expect("v4 addr");
TargetAddr::Ip(SocketAddr::from((
std::net::Ipv4Addr::from([rest[0], rest[1], rest[2], rest[3]]),
u16::from_be_bytes([rest[4], rest[5]]),
)))
}
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
let mut len = [0u8; 1];
self.stream.read_exact(&mut len).await.expect("domain len");
let mut rest = vec![0u8; len[0] as usize + 2];
self.stream.read_exact(&mut rest).await.expect("domain");
TargetAddr::Domain(
String::from_utf8(rest[..rest.len() - 2].to_vec()).expect("domain utf8"),
u16::from_be_bytes([rest[rest.len() - 2], rest[rest.len() - 1]]),
)
}
other => panic!("unknown reply ATYP {other:#x}"),
};
(head[1], addr)
}
}
/// 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
/// pipe (the vanilla client's greeting crosses it verbatim).
#[tokio::test]
async fn noauth_door_raw_pipe_vanilla_connect_round_trip() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
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
// 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");
let door = FrontDoor::builder().bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
let mut vanilla = VanillaClient::dial(addr).await;
let method = timeout(BOUND, vanilla.greet_no_auth())
.await
.expect("greeting round-trips through the raw pipe");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_NONE);
let code = timeout(BOUND, vanilla.connect_v4(echo))
.await
.expect("CONNECT round-trips through the raw pipe");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
// The data plane is the literal pipe: echo both directions.
vanilla.stream.write_all(b"vanilla-echo").await.unwrap();
let mut buf = [0u8; 12];
timeout(BOUND, vanilla.stream.read_exact(&mut buf))
.await
.expect("echo within bound")
.expect("read");
assert_eq!(&buf, b"vanilla-echo", "the raw pipe round-trips");
topo.consumer.manager();
}
/// The cred-bearing door: good creds → RFC 1929 success → CONNECT
/// round-trip. The mapped identity reaches the producer (the
/// establisher witness sees the mapped identity id, never the
/// password); the in-band conversation is re-originated (the vanilla
/// client's command bytes were consumed by the door's own RFC half).
#[tokio::test]
async fn cred_door_good_creds_connect_round_trip_with_mapped_identity() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with_provider(
dial,
None,
None,
Some(consumer_identity()),
Arc::new(DoorTokenProvider {
bind_scoped: vec![],
}),
)
.await;
let _ = echo;
let table = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
username: "alice".to_string(),
password: "secret".to_string(),
identity: Identity {
id: "door-alice".to_string(),
scopes: vec![alksocks::SOCKS5_OPEN_SCOPE.to_string()],
resources: Default::default(),
},
}]);
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().credentials(table).bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
let mut vanilla = VanillaClient::dial(addr).await;
let method = timeout(BOUND, vanilla.greet_password())
.await
.expect("greeting");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_PASSWORD);
let status = timeout(BOUND, vanilla.send_credentials("alice", "secret"))
.await
.expect("credential exchange");
assert_eq!(status, 0x00, "accepted");
assert_eq!(status, 0x00, "RFC 1929 success");
let code = timeout(BOUND, vanilla.connect_v4(echo))
.await
.expect("CONNECT after auth");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
vanilla.stream.write_all(b"cred-echo").await.unwrap();
let mut buf = [0u8; 9];
timeout(BOUND, vanilla.stream.read_exact(&mut buf))
.await
.expect("echo within bound")
.expect("read");
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.
timeout(BOUND, async {
loop {
let seen = topo.identity_witness.lock().await.clone();
if seen.as_deref() == Some("door-alice") {
return;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("the establisher witnessed the 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
/// held door-side).
#[tokio::test]
async fn cred_door_bad_creds_rejected_no_channel_opened() {
let dial = alksocks::tcp_dial();
let table = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
username: "alice".to_string(),
password: "secret".to_string(),
identity: Identity {
id: "door-alice".to_string(),
scopes: vec![alksocks::SOCKS5_OPEN_SCOPE.to_string()],
resources: Default::default(),
},
}]);
let topo = harness::wire_consumer_side_with_provider(
dial,
None,
None,
Some(consumer_identity()),
Arc::new(DoorTokenProvider {
bind_scoped: vec![],
}),
)
.await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().credentials(table).bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
let mut vanilla = VanillaClient::dial(addr).await;
let method = timeout(BOUND, vanilla.greet_password())
.await
.expect("greeting");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_PASSWORD);
let status = timeout(BOUND, vanilla.send_credentials("alice", "wrong"))
.await
.expect("credential exchange");
assert_eq!(status, 0xff, "RFC 1929 rejection");
// No channel opened: the witness never fired (the door-side
// rejection never reached the producer), and the consumer's
// manager holds only channel 0.
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
topo.identity_witness.lock().await.as_deref(),
None,
"the rejection never opened a channel"
);
assert!(
topo.consumer
.manager()
.channel_ids()
.iter()
.all(|&id| id == 0),
"no channel entry exists opener-side"
);
}
/// 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
/// 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
/// garbage), and the witness carries the mapped identity (the
/// password never crossed).
#[tokio::test]
async fn cred_door_never_pipes_raw_post_auth() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
let table = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
username: "bob".to_string(),
password: "pw".to_string(),
identity: Identity {
id: "door-bob".to_string(),
scopes: vec![alksocks::SOCKS5_OPEN_SCOPE.to_string()],
resources: Default::default(),
},
}]);
let topo = harness::wire_consumer_side_with_provider(
dial,
None,
None,
Some(consumer_identity()),
Arc::new(DoorTokenProvider {
bind_scoped: vec![],
}),
)
.await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().credentials(table).bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
// 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
// re-spoke the bytes in-band).
let mut vanilla = VanillaClient::dial(addr).await;
let method = timeout(BOUND, vanilla.greet_password())
.await
.expect("greeting");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_PASSWORD, "password auth");
let status = timeout(BOUND, vanilla.send_credentials("bob", "pw"))
.await
.expect("creds");
assert_eq!(status, 0x00, "accepted");
let code = timeout(BOUND, vanilla.connect_v4(echo))
.await
.expect("the in-band conversation is fresh (never the consumed bytes)");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
vanilla.stream.write_all(b"pin").await.unwrap();
let mut buf = [0u8; 3];
timeout(BOUND, vanilla.stream.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"pin");
}
/// The BIND door (cred-bearing variant): a vanilla BIND conversation
/// 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]
async fn bind_door_vanilla_conversation_reply_fidelity() {
let dial = alksocks::tcp_dial();
let egress = None;
let accept = Some(alksocks::bind_accept([127, 0, 0, 1]));
let table = alksocks::CredentialTable::new(vec![alksocks::DoorCredential {
username: "carol".to_string(),
password: "pw".to_string(),
identity: Identity {
id: "door-carol".to_string(),
scopes: vec![
alksocks::SOCKS5_OPEN_SCOPE.to_string(),
SOCKS5_BIND_SCOPE.to_string(),
],
resources: Default::default(),
},
}]);
let topo = harness::wire_consumer_side_with_provider(
dial,
egress,
accept,
Some(consumer_identity()),
Arc::new(DoorTokenProvider {
bind_scoped: vec!["door-carol".to_string()],
}),
)
.await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().credentials(table).bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
let mut vanilla = VanillaClient::dial(addr).await;
let method = timeout(BOUND, vanilla.greet_password())
.await
.expect("greeting");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_PASSWORD, "password auth");
let status = timeout(BOUND, vanilla.send_credentials("carol", "pw"))
.await
.expect("creds");
assert_eq!(status, 0x00, "accepted");
// 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
.expect("reply#1");
assert_eq!(
code1,
consts::SOCKS5_REPLY_SUCCEEDED,
"BIND served under the gate"
);
let TargetAddr::Ip(advertised_sa) = advertised else {
panic!("an IP advertised address, got {advertised:?}")
};
assert!(
advertised_sa.ip().is_loopback() && advertised_sa.port() != 0,
"reply#1 carries the real listener address: {advertised_sa}"
);
// The app server dials the advertised address.
let mut app = timeout(BOUND, TcpStream::connect(advertised_sa))
.await
.expect("dial within bound")
.expect("app dials the advertised address");
app.write_all(b"vanilla-bind").await.unwrap();
app.flush().await.unwrap();
// Reply#2: success + the real peer (the door adds nothing).
let (code2, peer) = timeout(BOUND, vanilla.read_reply_addr())
.await
.expect("reply#2");
assert_eq!(code2, consts::SOCKS5_REPLY_SUCCEEDED, "reply#2 success");
let TargetAddr::Ip(peer_sa) = peer else {
panic!("an IP peer, got {peer:?}")
};
assert_eq!(
peer_sa.port(),
app.local_addr().expect("app local").port(),
"reply#2 carries the real peer port (byte-exact)"
);
// The data plane through the door.
let mut buf = [0u8; 12];
timeout(BOUND, vanilla.stream.read_exact(&mut buf))
.await
.expect("data plane round trip")
.expect("read");
assert_eq!(&buf, b"vanilla-bind");
drop(app);
}
/// The idle-first-byte timeout (ADR-014 knob 1): an accepted
/// connection that sends no first byte within the window is closed
/// door-side.
#[tokio::test]
async fn idle_first_byte_timeout_closes_a_silent_connection() {
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder()
.bind(addr)
.idle_first_byte(Duration::from_millis(300))
.build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
// Connect and stay silent: the door must close the socket within
// (window + slack).
let mut silent = TcpStream::connect(addr).await.expect("dial the door");
let mut probe = [0u8; 1];
let start = std::time::Instant::now();
let outcome = timeout(Duration::from_secs(3), silent.read(&mut probe)).await;
match outcome {
Err(_) => panic!("the silent connection must be closed door-side"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("EOF expected, got {n} bytes"),
}
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(250),
"the door waited the window before closing (not instant): {elapsed:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"the close arrived near the window, not arbitrarily late: {elapsed:?}"
);
// No channel was opened for the silent connection.
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(
topo.consumer
.manager()
.channel_ids()
.iter()
.all(|&id| id == 0),
"the idle timeout fired before any channel opened"
);
}
/// The session cap (ADR-014 knob 2): an accept beyond the bound is
/// 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() {
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder()
.bind(addr)
.idle_first_byte(Duration::from_secs(30))
.max_sessions(1)
.build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
// Session 1: connects and stays silent (holds the one slot).
let mut holder = TcpStream::connect(addr).await.expect("first dial");
let _ = &mut holder;
// Session 2: beyond the cap — 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];
let outcome = timeout(Duration::from_secs(3), {
let mut refused = refused;
async move { refused.read(&mut probe).await }
})
.await;
match outcome {
Err(_) => panic!("the beyond-cap connection must be refused"),
Ok(Ok(0)) | Ok(Err(_)) => {}
Ok(Ok(n)) => panic!("no bytes for a beyond-cap accept, got {n}"),
}
// 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-less. Assert: no channels at all for either connection
// (both silent), and the door is still serving (a fresh dial
// succeeds).
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(
topo.consumer
.manager()
.channel_ids()
.iter()
.all(|&id| id == 0),
"neither silent connection opened a channel"
);
// The door keeps serving (the loop continued).
let fresh = TcpStream::connect(addr).await;
assert!(fresh.is_ok(), "the door keeps accepting after refusals");
drop(fresh);
}
/// Per-connection failure isolation (the alktunnels per-accept
/// 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]
async fn per_connection_failure_isolation() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
// Client A: protocol garbage (a bad version byte through the raw
// pipe — the producer's typestate refuses in-band; the door's
// pipe ends with the session).
let mut garbage = VanillaClient::dial(addr).await;
garbage.stream.write_all(&[0x04, 0x01, 0x00]).await.unwrap();
// Give the producer's refusal a beat; the door's pipe tears down.
tokio::time::sleep(Duration::from_millis(200)).await;
drop(garbage);
// Client B: the full vanilla CONNECT round-trip still works (the
// door survived the dead client).
let mut good = VanillaClient::dial(addr).await;
let method = timeout(BOUND, good.greet_no_auth())
.await
.expect("the door is alive");
assert_eq!(method, consts::SOCKS5_AUTH_METHOD_NONE);
let code = timeout(BOUND, good.connect_v4(echo))
.await
.expect("the door survived the dead client");
assert_eq!(code, consts::SOCKS5_REPLY_SUCCEEDED);
good.stream.write_all(b"alive").await.unwrap();
let mut buf = [0u8; 5];
timeout(BOUND, good.stream.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"alive");
}
/// 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
/// sessions keep serving; a cred-bearing door's client is door-only
/// 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() {
let echo = echo_tcp_server().await;
let dial = alksocks::tcp_dial();
let topo = harness::wire_consumer_side_with(dial, None, None, None).await;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("door addr");
let door = FrontDoor::builder().bind(addr).build();
let _serve = tokio::spawn({
let client = Arc::clone(&topo.consumer);
async move {
door.serve_on(listener, client)
.await
.expect("the door serves")
}
});
// The channels-native CONNECT session rides the same client.
let mut session = Socks5ChannelSession::connect(topo.consumer.as_ref(), TargetAddr::Ip(echo))
.await
.expect("the native session connects");
let (mut read, mut write) = session.split().expect("post-reply pipe");
write.write_all(b"native").await.unwrap();
write.flush().await.unwrap();
let mut buf = [0u8; 6];
timeout(BOUND, read.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"native");
session.close().await;
}
/// The BIND door via the channels-native session (the regression
/// guard on the door's BIND path machinery: the same
/// `BindSession::bind_with_token` surface the door rides, driven
/// directly with the token provider wired).
#[tokio::test]
async fn bind_session_with_token_round_trip() {
let dial = alksocks::tcp_dial();
let accept = Some(alksocks::bind_accept([127, 0, 0, 1]));
let topo = harness::wire_consumer_side_with_provider(
dial,
None,
accept,
Some(consumer_identity()),
Arc::new(DoorTokenProvider {
bind_scoped: vec!["door-native-bind".to_string()],
}),
)
.await;
let token_client =
Arc::new(alksocks::consumer::TokenClient::wrap(topo.consumer.as_ref()).await);
let mut session = BindSession::bind_with_token(
token_client.as_ref(),
("bind.token", 0u16).to_target_addr().unwrap(),
b"door-native-bind".to_vec(),
)
.await
.expect("the token-carrying BIND session binds");
let TargetAddr::Ip(sa) = session.reply1().expect("reply#1").bind_addr.clone() else {
panic!("an IP BND.ADDR")
};
assert!(sa.ip().is_loopback() && sa.port() != 0, "a real listener");
let mut app = timeout(BOUND, TcpStream::connect(sa))
.await
.expect("dial")
.expect("app dials");
let mut plane = timeout(BOUND, session.await_inbound())
.await
.expect("reply#2")
.expect("plane")
.expect("accept resolved");
plane.write_all(b"token-bind").await.unwrap();
plane.flush().await.unwrap();
let mut buf = [0u8; 10];
timeout(BOUND, app.read_exact(&mut buf))
.await
.expect("read")
.expect("read");
assert_eq!(&buf, b"token-bind");
session.close().await;
}
+25 -4
View File
@@ -672,6 +672,28 @@ pub async fn wire_consumer_side_with(
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
identity: Option<Identity>,
) -> ConsumerSideTopology {
wire_consumer_side_with_provider(
dial,
egress,
accept,
identity,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await
}
/// The consumer-side topology with the producer-side dispatch's
/// 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`].
pub async fn wire_consumer_side_with_provider(
dial: DialFn,
egress: Option<EgressFactory>,
accept: Option<BindAcceptFactory>,
identity: Option<Identity>,
provider: Arc<dyn IdentityProvider>,
) -> ConsumerSideTopology {
let producer_op_registry = Arc::new(OperationRegistry::new());
let (producer_manager_tx, mut producer_manager_rx) =
@@ -681,12 +703,14 @@ pub async fn wire_consumer_side_with(
let resolved = Arc::new(identity.unwrap_or_else(consumer_identity));
let resolved_for_hook = Arc::clone(&resolved);
let provider_for_hook = Arc::clone(&provider);
let dial_for_hook = Arc::clone(&dial);
let install_hook: InstallChannelZero = Arc::new(
move |manager: ChannelManager, channel0_conn: CoreConnection, _auth: AuthContext| {
let registry = Arc::clone(&producer_op_registry);
let manager_tx = producer_manager_tx.clone();
let witness = Arc::clone(&witness_for_hook);
let provider = Arc::clone(&provider_for_hook);
let resolved = Arc::clone(&resolved_for_hook);
let dial = Arc::clone(&dial_for_hook);
let egress = egress.clone();
@@ -718,10 +742,7 @@ pub async fn wire_consumer_side_with(
)
.expect("register socks5 openable (the consumer-side topology)");
let _ = manager_tx.send(manager).await;
let dp = Dispatcher::new(
registry,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
);
let dp = Dispatcher::new(registry, provider);
dp.serve_single_stream(call_connection, reader, writer)
.await;
})