Files
alksocks/tests/local-backend-producer.rs
T
glm-5.3-flash e196ed43e3 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
2026-11-03 19:31:53 +00:00

510 lines
18 KiB
Rust

//! 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
//! stack on real sockets (`ProducerDoor` → `serve_socks5_over` →
//! `local::tcp_dial()` / `local::bind_accept()`), BIND through the
//! 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).
#![cfg(feature = "local")]
#![allow(dead_code)]
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::time::timeout;
use alksocks::local::ProducerDoor;
use alksocks::producer::Socks5Producer;
use alksocks::rfc1928::util::target_addr::{TargetAddr, ToTargetAddr};
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).
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");
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 producer under test: local socket-backed seams all three
/// (the whole stack on real sockets), BIND policy as given.
fn local_producer(bind_policy: Option<bool>) -> Socks5Producer {
let builder = Socks5Producer::builder()
.dial(alksocks::local::tcp_dial())
.egress(alksocks::local::udp_egress())
.accept(alksocks::local::bind_accept(std::net::IpAddr::from([
127, 0, 0, 1,
])));
let builder = match bind_policy {
Some(can) => builder.bind_policy(can),
None => builder,
};
builder.build()
}
/// Serve the producer behind a real listener (ephemeral loopback
/// port); returns the vanilla client's door address and the guard.
async fn serve(
producer: Socks5Producer,
idle: Duration,
cap: usize,
) -> (std::net::SocketAddr, alksocks::local::ProducerDoorGuard) {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local_addr");
let door = ProducerDoor::builder()
.producer(producer)
.idle_first_byte(idle)
.max_sessions(cap)
.build()
.expect("the door builds");
let guard = door.serve_on(listener).await;
(addr, guard)
}
/// A vanilla client: connect to the door, run the full NoAuth
/// greeting + CONNECT request, read the reply, and echo round-trip
/// through the dialed target. Returns the reply code.
async fn vanilla_connect(
door: std::net::SocketAddr,
target: std::net::SocketAddr,
) -> (TcpStream, u8, [u8; 6]) {
let mut stream = TcpStream::connect(door).await.expect("door connect");
stream
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
stream
.read_exact(&mut chosen)
.await
.expect("method selection");
assert_eq!(chosen, [0x05, 0x00], "no-auth selected");
let mut req = vec![0x05, 0x01, 0x00, 0x01];
req.extend_from_slice(&[127, 0, 0, 1]);
req.extend_from_slice(&target.port().to_be_bytes());
stream.write_all(&req).await.expect("request");
stream.flush().await.expect("flush");
let mut head = [0u8; 4];
stream.read_exact(&mut head).await.expect("reply head");
assert_eq!(head[0], 0x05, "reply is v5");
assert_eq!(head[3], 0x01, "v4 BND.ADDR");
let mut addr = [0u8; 6];
stream.read_exact(&mut addr).await.expect("BND.ADDR");
(stream, head[1], addr)
}
#[tokio::test]
async fn vanilla_client_connect_round_trips_through_local_seams() {
let target = echo_tcp_server().await;
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let (mut stream, code, _addr) = timeout(BOUND, vanilla_connect(door, target))
.await
.expect("round-trip");
assert_eq!(
code, 0x00,
"the CONNECT resolved through the real dial seam"
);
// The data plane is raw pass-through to the real echo server.
stream.write_all(b"ping").await.expect("probe");
let mut buf = [0u8; 4];
timeout(BOUND, stream.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"ping");
drop(stream);
_guard.close().await;
}
/// 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]
async fn vanilla_client_bind_round_trips_with_real_addresses() {
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let mut client = TcpStream::connect(door).await.expect("door connect");
client
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
client
.read_exact(&mut chosen)
.await
.expect("method selection");
assert_eq!(chosen, [0x05, 0x00]);
// BIND for the ephemeral port (ATYP v4, port 0).
client
.write_all(&[0x05, 0x02, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await
.expect("bind request");
// Reply#1: success + the real bound listener address.
let mut head = [0u8; 4];
client.read_exact(&mut head).await.expect("reply#1 head");
assert_eq!(head[1], 0x00, "BIND reply#1 succeeded");
let mut bnd = [0u8; 6];
client.read_exact(&mut bnd).await.expect("reply#1 BND.ADDR");
assert_eq!(bnd[0], 127, "the reply carries the real loopback bind");
let bound_port = u16::from_be_bytes([bnd[4], bnd[5]]);
assert!(bound_port != 0, "a real listener was bound");
// The inbound peer connects to the advertised address.
let advertised = std::net::SocketAddr::from(([127, 0, 0, 1], bound_port));
let mut peer = TcpStream::connect(advertised).await.expect("peer connects");
peer.write_all(b"hello-bind").await.expect("peer writes");
// Reply#2: success + the peer's real address.
let mut head2 = [0u8; 4];
client.read_exact(&mut head2).await.expect("reply#2 head");
assert_eq!(head2[1], 0x00, "BIND reply#2 succeeded");
let mut peer_addr = [0u8; 6];
client
.read_exact(&mut peer_addr)
.await
.expect("reply#2 DST.ADDR");
assert_eq!(peer_addr[0], 127, "reply#2 carries the real peer address");
let peer_port = u16::from_be_bytes([peer_addr[4], peer_addr[5]]);
assert!(peer_port != 0, "the peer's ephemeral port is real");
// The data plane is raw pass-through to the accepted peer.
let mut buf = [0u8; 10];
timeout(BOUND, client.read_exact(&mut buf))
.await
.expect("data plane")
.expect("read");
assert_eq!(&buf, b"hello-bind");
client.write_all(b"pong").await.expect("client writes");
let mut echo = [0u8; 4];
timeout(BOUND, peer.read_exact(&mut echo))
.await
.expect("reverse data plane")
.expect("read");
assert_eq!(&echo, b"pong");
drop(peer);
drop(client);
_guard.close().await;
}
/// 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.
#[tokio::test]
async fn idle_first_byte_timeout_closes_the_accepted_connection() {
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let mut stream = TcpStream::connect(door).await.expect("door connect");
let mut buf = [0u8; 2];
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"),
}
_guard.close().await;
}
/// The session cap (ADR-014 knob 2): an accept beyond the bound is
/// refused door-side (immediate EOF), while the sessions under the
/// bound keep serving.
#[tokio::test]
async fn session_cap_refuses_beyond_the_bound_while_live_sessions_serve() {
let target = echo_tcp_server().await;
let (door, _guard) = serve(local_producer(None), IDLE, 1).await;
// One live session fills the cap (holding the reply open).
let (mut held, code, _) = timeout(BOUND, vanilla_connect(door, target))
.await
.expect("held session");
assert_eq!(code, 0x00, "the cap's first session serves");
// The second accept is beyond the bound: closed immediately.
let mut refused = TcpStream::connect(door).await.expect("door connect");
let mut buf = [0u8; 2];
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"),
}
// The held session still serves (isolation: the refusal did not
// touch it).
held.write_all(b"still-alive").await.expect("held writes");
let mut echo = [0u8; 11];
timeout(BOUND, held.read_exact(&mut echo))
.await
.expect("held echoes")
.expect("read");
assert_eq!(&echo, b"still-alive");
drop(held);
_guard.close().await;
}
/// Per-connection failure isolation: a connection speaking protocol
/// 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() {
let target = echo_tcp_server().await;
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
// Garbage: a valid greeting, then a malformed command head.
let mut bad = TcpStream::connect(door).await.expect("door connect");
bad.write_all(&[0x05, 0x01, 0x00]).await.expect("greeting");
let mut chosen = [0u8; 2];
bad.read_exact(&mut chosen).await.expect("method selection");
bad.write_all(&[0x05, 0x01, 0x00, 0xFF])
.await
.expect("garbage head");
drop(bad);
tokio::time::sleep(Duration::from_millis(50)).await;
// The loop continues: a well-formed client serves normally.
let (mut stream, code, _) = timeout(BOUND, vanilla_connect(door, target))
.await
.expect("next session");
assert_eq!(code, 0x00, "the loop survived the failed connection");
stream.write_all(b"ping").await.expect("probe");
let mut buf = [0u8; 4];
timeout(BOUND, stream.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"ping");
drop(stream);
_guard.close().await;
}
/// 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.
#[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
// bind_policy is the whole gate on this path).
let mut client = TcpStream::connect(door).await.expect("door connect");
client
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
client
.read_exact(&mut chosen)
.await
.expect("method selection");
client
.write_all(&[0x05, 0x02, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await
.expect("bind request");
let mut head = [0u8; 4];
client.read_exact(&mut head).await.expect("reply head");
assert_eq!(head[1], 0x02, "bind_policy(false) → ConnectionNotAllowed");
drop(client);
// CONNECT still serves through the same closed-gate producer.
let (stream, code, _) = timeout(BOUND, vanilla_connect(door, target))
.await
.expect("connect session");
assert_eq!(code, 0x00, "the gate closes BIND alone");
drop(stream);
_guard.close().await;
}
/// The default BIND posture holds too: with the accept seam wired and
/// no explicit policy, BIND is permitted (the backward-compat
/// reading) and serves through the real listener seam.
#[tokio::test]
async fn default_bind_posture_serves_through_the_accept_seam() {
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let mut client = TcpStream::connect(door).await.expect("door connect");
client
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
client
.read_exact(&mut chosen)
.await
.expect("method selection");
client
.write_all(&[0x05, 0x02, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await
.expect("bind request");
let mut head = [0u8; 4];
client.read_exact(&mut head).await.expect("reply#1 head");
assert_eq!(head[1], 0x00, "the default posture permits BIND");
drop(client);
_guard.close().await;
}
/// UDP ASSOCIATE through the local backend: the sentinel reply rides
/// the door, the datagram stage relays through the real UDP egress
/// (a real loopback UDP echo target).
#[tokio::test]
async fn vanilla_client_udp_associate_relays_through_the_local_egress() {
let sock = tokio::net::UdpSocket::bind("127.0.0.1:0")
.await
.expect("bind");
let udp_target = sock.local_addr().expect("local_addr");
tokio::spawn(async move {
let mut buf = [0u8; 65535];
loop {
let Ok((n, from)) = sock.recv_from(&mut buf).await else {
return;
};
if sock.send_to(&buf[..n], from).await.is_err() {
return;
}
}
});
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let mut client = TcpStream::connect(door).await.expect("door connect");
client
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
client
.read_exact(&mut chosen)
.await
.expect("method selection");
client
.write_all(&[0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
.await
.expect("associate request");
let mut head = [0u8; 4];
client.read_exact(&mut head).await.expect("reply head");
assert_eq!(
head[1], 0x00,
"the associate resolved through the real egress seam"
);
let mut bnd = [0u8; 6];
client.read_exact(&mut bnd).await.expect("BND.ADDR");
assert_eq!(&bnd, &[127, 0, 0, 1, 0, 0], "the sentinel BND.ADDR");
// One datagram frame (the in-band wire shape) echoes through the
// real UDP egress.
let dst = TargetAddr::Ip(udp_target);
let frame = alksocks::wire::frame(&dst, b"udp-probe").expect("frame");
client.write_all(&frame).await.expect("datagram frame");
let mut reply = vec![0u8; frame.len()];
timeout(BOUND, client.read_exact(&mut reply))
.await
.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), 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];
let atyp = inner[0];
let addr_len = match atyp {
0x01 => 1 + 4 + 2,
0x03 => 1 + 1 + inner[1] as usize + 2,
0x04 => 1 + 16 + 2,
other => panic!("unexpected ATYP {other}"),
};
assert_eq!(&inner[addr_len..], b"udp-probe", "the datagram echoed");
drop(client);
_guard.close().await;
}
/// The door's address target form: a domain-form CONNECT target
/// 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() {
let target = echo_tcp_server().await;
let (door, _guard) = serve(local_producer(None), IDLE, 16).await;
let mut client = TcpStream::connect(door).await.expect("door connect");
client
.write_all(&[0x05, 0x01, 0x00])
.await
.expect("greeting");
let mut chosen = [0u8; 2];
client
.read_exact(&mut chosen)
.await
.expect("method selection");
let domain = "localhost".to_owned();
let mut req = vec![0x05, 0x01, 0x00, 0x03, domain.len() as u8];
req.extend_from_slice(domain.as_bytes());
req.extend_from_slice(&target.port().to_be_bytes());
client.write_all(&req).await.expect("request");
client.flush().await.expect("flush");
let mut head = [0u8; 4];
client.read_exact(&mut head).await.expect("reply head");
assert_eq!(head[1], 0x00, "the domain resolved at the seam");
let mut addr = [0u8; 6];
client.read_exact(&mut addr).await.expect("BND.ADDR");
client.write_all(b"dom").await.expect("probe");
let mut buf = [0u8; 3];
timeout(BOUND, client.read_exact(&mut buf))
.await
.expect("echo")
.expect("read");
assert_eq!(&buf, b"dom");
drop(client);
_guard.close().await;
}
/// The `ToTargetAddr` import stays exercised (the compile-time shape
/// the domain test rides).
#[allow(dead_code)]
fn _target_addr_shape() {
let _: TargetAddr = std::net::SocketAddr::from(([127, 0, 0, 1], 1))
.to_target_addr()
.expect("encode");
}