Files
alktunnels/tests/local_bindfirst.rs
T
glm-5.3-flash 514e0c1f25 feat: the local bind-first halves (shape 3 substrate half, ADR-008 §1)
- `bind_tcp_first` + `TcpListenerHalves::accept_pair`: the real-socket
  BindFn source (port 0 resolved via local_addr — the `bound` reply
  field's data) and the per-accept (peer, halves) pair (the peer rides
  accept()'s return — the forwarded op's `peer` carrier)
- unix bind-first parity shipped (OQ-TN-14): `bind_unix` +
  `UnixListenerHalves` (local_addr / queue-feeding accept_loop /
  bind-first accept_pair; an unnamed peer fails loud — the forwarded
  `peer` param requires an address) + `bind_unix_first`
- assembly conveniences: `local_bind_first()` (tcp/unix dispatch; udp
  fails loud — no accept on a UDP socket, the shape is not defined
  there, never a fake listener) + `local_bindfirst_accept_loop()`
  (downcasts the opaque listener; a foreign type fails loud)
- direct-op DialFn reuse verified + pinned: `local_dial` shared
  unchanged (the UDP framed wrap composes inside the closure);
  `direct_udp_through_local_dial_round_trips_framed` drives a REAL
  udp echo through the direct op's establisher path
- tests/local_bindfirst.rs: tcp e2e over real sockets (bound fidelity
  — the test dials the reported address; two clients → two forwarded
  channels pumping independently, real distinct peers), unix parity
  (real path in `bound`, named-peer client via socket2, real peer path
  on the forwarded open), udp refusal, downcast-miss probe
- accept-first shapes (accept_loop + AcceptQueue) untouched

Verified: cargo test (default) + --all-features green (45 lib; 151
integration), clippy -D warnings clean (native all-features + wasm32),
fmt clean, wasm32 check passes (default crate stays wasm-clean),
cargo doc clean (no new warnings).

Tasks: tunnels/local-bind-halves (status completed; Notes + Summary
filled). CHANGELOG: Unreleased → Added.
2026-09-19 16:30:46 +00:00

403 lines
16 KiB
Rust

//! Tests for the `local` feature's BIND-FIRST half (shape 3, ADR-008):
//! the real-socket `BindFn` sources (`bind_tcp_first` /
//! `bind_unix_first` — the `bound` reply field's data, port 0
//! resolved), the accept-pair loops (peer + halves per accept), and
//! the assembly convenience closures (`local_bind_first` +
//! `local_bindfirst_accept_loop`), through the real bind-first
//! establisher path over REAL sockets.
//!
//! Covered (the task's test plan): the bound address is the REAL
//! OS-chosen one (a fake `bound` would strand the test's dials), two
//! clients dial → two accepts → two forwarded channels pumping
//! independently (peer fidelity: each forwarded open carries the REAL
//! accepted peer), unix bind-first parity (OQ-TN-14 — the bind path is
//! the same shape as the dial path), and the udp bind-first refusal
//! (a UDP socket has no accept).
#![cfg(feature = "local")]
mod harness;
use std::sync::Arc;
use std::time::Duration;
use alkcall::channels::operations::{ChannelCore, OpenHandler};
use alkcall::core::auth::AuthContext;
use alkcall::protocol::wire::CallError;
use alktunnels::params::{parse_listen_reply, Substrate, SubstrateAddr, TunnelForwardedParams};
use alktunnels::producer::ResourceRegistry;
use serde_json::Value;
use tracing_subscriber::EnvFilter;
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.try_init();
}
/// The forwarded-op recording stub's peer log: the `peer` param of
/// every forwarded open the producer's dispatcher made (the
/// reply#2-fidelity probe surface — ADR-008 §3).
type PeerLog = Arc<tokio::sync::Mutex<Vec<SubstrateAddr>>>;
/// The forwarded-op opener-side serving stub (the queue-fed shape):
/// each forwarded open pops the NEXT app-side pair (the "local BIND
/// client" halves the test registered) and pumps the forwarded
/// channel against it, recording the open's `peer` param (the
/// reply#2-fidelity probe surface — ADR-008 §3). One listener session
/// = one token = MANY forwarded opens (per accept), so the
/// correlation is a queue, not a one-shot map.
fn recording_forwarded_handler(app_sides: AppSideQueue, peers: PeerLog) -> OpenHandler {
Arc::new(
move |input: Value, _plan, conn: alkcall::core::types::Connection, _auth| {
let app_sides = Arc::clone(&app_sides);
let peers = Arc::clone(&peers);
tokio::spawn(async move {
let forwarded: TunnelForwardedParams = match serde_json::from_value(input) {
Ok(p) => p,
Err(e) => {
tracing::error!(error = %e, "recording stub: params parse failed");
return;
}
};
peers.lock().await.push(forwarded.peer.clone());
let target = app_sides.lock().await.pop_front();
let Some(target) = target else {
tracing::error!(
listen = forwarded.listen,
"recording stub: no app-side pair registered"
);
return;
};
let Ok(bidi) = conn.accept_bi().await else {
tracing::error!("recording stub: accept_bi failed");
return;
};
alkcall::channels::pump::pump_bidi(bidi, target.read, target.write).await;
})
},
)
}
/// The app-side pairs the stub pops per forwarded open (the "local
/// BIND client" halves — the stub's pump target; the far end is the
/// test's echo task).
type AppSideQueue =
Arc<tokio::sync::Mutex<std::collections::VecDeque<alktunnels::producer::TargetHandle>>>;
/// Install the recording stub on the opener (consumer) side (post-hoc
/// on the captured serving registry — W2).
async fn install_recording_stub(topo: &harness::Topology) -> (AppSideQueue, PeerLog) {
let app_sides: AppSideQueue = Arc::new(tokio::sync::Mutex::new(Default::default()));
let peers: PeerLog = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let core = ChannelCore::new(
topo.consumer_manager.clone(),
alkcall::channels::policy::default_policy(),
);
core.register_openable(
alktunnels::params::tunnel_forwarded_spec(),
recording_forwarded_handler(Arc::clone(&app_sides), Arc::clone(&peers)),
&topo.consumer_registry,
AuthContext::anonymous(b"alk/tunnel"),
)
.expect("register recording forwarded stub on the opener");
(app_sides, peers)
}
/// Call the producer's open op on channel 0 with the FULL reply (the
/// bind-first reply projection probe — `bound` + `listen`).
async fn open_listen_with_reply(
topo: &harness::Topology,
resource: &str,
substrate: Substrate,
) -> Result<(u32, Value), CallError> {
let substrate_str = match substrate {
Substrate::Tcp => "tcp",
Substrate::Udp => "udp",
Substrate::Unix => "unix",
};
let payload = serde_json::json!({
"operationId": "channels/tunnel/sub",
"input": {
"resource": resource,
"substrate": substrate_str,
},
});
let response = topo.consumer_call.call_with_payload(payload).await;
let out = response.result?;
let channel_id = out["channel_id"].as_u64().expect("channel_id") as u32;
Ok((channel_id, out))
}
/// One app-side session: a raw duplex pair whose FAR end echoes (the
/// app-side session shape) — the stub pumps the forwarded channel
/// against the near end (pushed into the stub's queue); the test
/// drives the REAL accepted socket at the tunnel's other side, whose
/// bytes echo through the app-side session.
fn echo_app_side() -> alktunnels::producer::TargetHandle {
let (a, b) = tokio::io::duplex(64 * 1024);
let (a_read, a_write) = tokio::io::split(a);
let (b_read, b_write) = tokio::io::split(b);
tokio::spawn(async move {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let (mut r, mut w) = (b_read, b_write);
let mut buf = vec![0u8; 8192];
loop {
match r.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if w.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
alktunnels::producer::TargetHandle {
read: Box::new(a_read),
write: Box::new(a_write),
}
}
/// THE bind-first flow over REAL TCP sockets (the `local` shape-3
/// gate): `bind_tcp_first` binds port 0 → the reply's `bound` is the
/// REAL OS-chosen address (the test dials it — a fake bound would
/// strand the dial) → two clients dial → two accepts → two forwarded
/// channels → both pump independently, and each forwarded open
/// carries the REAL accepted peer (peer fidelity, reply#2 class).
#[tokio::test]
async fn bindfirst_tcp_real_sockets_bound_fidelity_and_two_pumps() {
init_tracing();
let registry = ResourceRegistry::new();
registry
.register("bind", Substrate::Tcp, "127.0.0.1:0")
.await;
let topo = harness::wire_bindfirst(
registry,
alktunnels::local::local_bind_first(),
alktunnels::local::local_bindfirst_accept_loop(),
)
.await;
let (channel_id, reply) = open_listen_with_reply(&topo, "bind", Substrate::Tcp)
.await
.expect("listen open");
let projected = parse_listen_reply(&reply).expect("projected reply");
let bind_addr = match &projected.bound {
Some(SubstrateAddr::HostPort { host, port }) => {
assert_eq!(host, "127.0.0.1", "the bound host is the bound loopback");
assert_ne!(*port, 0, "port 0 was resolved to the OS-chosen port");
format!("{host}:{port}")
}
other => panic!("bind-first open carries the real bound address: {other:?}"),
};
let token = projected.listen.expect("the `listen` token");
assert_ne!(token, 0, "token 0 is never minted (the wire-illegal value)");
let (app_sides, peers) = install_recording_stub(&topo).await;
// The opener adopts the LISTEN channel (the idle side). The
// halves are HELD — dropping them EOFs the idle stream and tears
// the listener down (the ADR-008 sentinel is live even here).
let (_idle_send, _idle_recv) = topo
.consumer_manager
.adopt_channel(channel_id, alktunnels::TUNNEL_ALPN, None)
.await
.expect("adopt listen channel");
// Two app-side sessions (the stub pops one per forwarded open).
app_sides.lock().await.push_back(echo_app_side());
app_sides.lock().await.push_back(echo_app_side());
// Two clients dial the reported bound address (the reply#1
// fidelity pin: `bound` is the REAL address, not a placeholder).
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut client1 = tokio::net::TcpStream::connect(&bind_addr)
.await
.expect("client 1 dials the bound address");
let mut client2 = tokio::net::TcpStream::connect(&bind_addr)
.await
.expect("client 2 dials the bound address");
// Both pump independently: interleaved writes, per-socket echoes.
client1.write_all(b"client-one").await.expect("write 1");
client2.write_all(b"client-two").await.expect("write 2");
let mut buf1 = vec![0u8; 10];
let mut buf2 = vec![0u8; 10];
let (r1, r2) = tokio::join!(
tokio::time::timeout(Duration::from_secs(5), client1.read_exact(&mut buf1)),
tokio::time::timeout(Duration::from_secs(5), client2.read_exact(&mut buf2)),
);
r1.expect("round trip 1").expect("read 1");
r2.expect("round trip 2").expect("read 2");
assert_eq!(&buf1, b"client-one", "pump 1 serves ITS socket");
assert_eq!(&buf2, b"client-two", "pump 2 serves ITS socket");
// Peer fidelity: each forwarded open carried the REAL accepted
// peer (loopback + a real ephemeral port; distinct per socket).
let logged = peers.lock().await.clone();
assert_eq!(logged.len(), 2, "one forwarded open per accept");
let mut ports = std::collections::HashSet::new();
for peer in &logged {
match peer {
SubstrateAddr::HostPort { host, port } => {
assert_eq!(host, "127.0.0.1", "the REAL peer host");
assert_ne!(*port, 0, "the REAL peer port");
ports.insert(*port);
}
other => panic!("tcp peer is HostPort-shaped: {other:?}"),
}
}
assert_eq!(ports.len(), 2, "two accepts carried distinct real peers");
}
/// Unix bind-first parity (OQ-TN-14 — the bind path ships with the
/// dial path): `bind_unix_first` binds a temp-path listener → the
/// reply's `bound` is the REAL bound socket path → a unix client
/// (bound to its own path, so the peer is representable) connects →
/// one accept → one forwarded channel carrying the REAL peer path →
/// byte round-trip.
#[tokio::test]
async fn bindfirst_unix_real_sockets_bound_and_pumped() {
init_tracing();
let dir = std::env::temp_dir().join(format!("alktunnels-bindfirst-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let listen_path = dir.join("listen.sock");
let client_path = dir.join("client.sock");
let _ = std::fs::remove_file(&listen_path);
let _ = std::fs::remove_file(&client_path);
let listen_str = listen_path.to_string_lossy().into_owned();
let registry = ResourceRegistry::new();
registry
.register("ubind", Substrate::Unix, &listen_str)
.await;
let topo = harness::wire_bindfirst(
registry,
alktunnels::local::local_bind_first(),
alktunnels::local::local_bindfirst_accept_loop(),
)
.await;
let (channel_id, reply) = open_listen_with_reply(&topo, "ubind", Substrate::Unix)
.await
.expect("listen open");
let projected = parse_listen_reply(&reply).expect("projected reply");
assert_eq!(
projected.bound,
Some(SubstrateAddr::Path {
path: listen_str.clone()
}),
"the `bound` field is the REAL bound socket path"
);
let token = projected.listen.expect("the `listen` token");
assert_ne!(token, 0, "token 0 is never minted (the wire-illegal value)");
let (app_sides, peers) = install_recording_stub(&topo).await;
// The opener adopts the LISTEN channel (the idle side) — held for
// the test's lifetime (dropping the halves EOFs the idle stream
// and tears the listener down, the ADR-008 sentinel).
let (_idle_send, _idle_recv) = topo
.consumer_manager
.adopt_channel(channel_id, alktunnels::TUNNEL_ALPN, None)
.await
.expect("adopt listen channel");
// One app-side session (the stub pops it for the forwarded open).
app_sides.lock().await.push_back(echo_app_side());
// The unix client binds its own socket at a named path FIRST (an
// unnamed peer cannot be represented in the forwarded `peer`
// param — ADR-008 §3; the named-peer shape is the parity case).
// socket2: std has no `UnixStream::bind`; a socket2 socket binds
// the sockaddr at the path, then connects (the peer address is
// the client's own bound path).
let client_sock = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)
.expect("socket2 client");
client_sock
.bind(&socket2::SockAddr::unix(&client_path).expect("client sockaddr"))
.expect("bind client path");
client_sock
.connect(&socket2::SockAddr::unix(&listen_path).expect("listen sockaddr"))
.expect("client dials");
let client_std: std::os::unix::net::UnixStream = client_sock.into();
client_std.set_nonblocking(true).expect("nonblocking");
let mut client = tokio::net::UnixStream::from_std(client_std).expect("tokio client");
use tokio::io::{AsyncReadExt, AsyncWriteExt};
client.write_all(b"unix-bind").await.expect("write");
let mut buf = vec![0u8; 9];
tokio::time::timeout(Duration::from_secs(5), client.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(&buf, b"unix-bind");
// Peer fidelity: the forwarded open carried the client's REAL
// bound path.
let logged = peers.lock().await.clone();
assert_eq!(
logged,
vec![SubstrateAddr::Path {
path: client_path.to_string_lossy().into_owned()
}],
"the forwarded open carried the REAL accepted peer path"
);
let _ = std::fs::remove_file(&listen_path);
let _ = std::fs::remove_file(&client_path);
}
/// The udp bind-first refusal: a UDP socket has no accept, so the
/// bind closure fails loud (`handler_error`-class) — the shape is not
/// defined there, never a silent fake listener.
#[tokio::test]
async fn udp_bind_first_fails_loud() {
let registry = ResourceRegistry::new();
registry
.register("ubind", Substrate::Udp, "127.0.0.1:0")
.await;
let topo = harness::wire_bindfirst(
registry,
alktunnels::local::local_bind_first(),
alktunnels::local::local_bindfirst_accept_loop(),
)
.await;
let payload = serde_json::json!({
"operationId": "channels/tunnel/sub",
"input": {"resource": "ubind", "substrate": "udp"},
});
let response = topo.consumer_call.call_with_payload(payload).await;
let err = response.result.expect_err("udp bind-first must fail");
assert_eq!(err.code, "channel:open_failed");
let reason = err
.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.expect("reason in details");
assert_eq!(reason, "handler_error");
}
/// The accept-pair loop's mismatch probe: a listener NOT produced by
/// this module's bind closures fails loud inside the closure (the
/// downcast is the assembly side of the type-erased injection — a
/// foreign listener type is `handler_error`, never a phantom accept).
#[tokio::test]
async fn accept_pair_loop_downcast_miss_fails_loud() {
let loop_ = alktunnels::local::local_bindfirst_accept_loop();
let foreign: Arc<dyn std::any::Any + Send + Sync> = Arc::new(4242u32);
let err = loop_(foreign)
.await
.expect_err("a foreign listener fails loud");
assert!(
matches!(
err,
alktunnels::producer::TunnelEstablishError::HandlerError(_)
),
"the downcast miss is handler_error-class: {err:?}"
);
}