Closes the wave-3 gate's booked finding item 4 (review-wave-3 §Booked findings): register on an already-held token now refuses fail-closed instead of silently replacing the live route — a collided mint (the ADR-008 discipline's upstream failure) can never steal a live adapter's arrival routes. - ListenRouter::register returns Result<(), String>; collision → Err, the held route untouched (src/tunnel_map.rs) - Error surface decided + recorded: the wire-time collision is the request-time decline disposition (RequestDenied, reason token-collision), not a new AdapterError variant — a global-request hook's Err propagates out of russh 0.65's packet loop and kills the session, while a refused listen's vanilla contract is RequestDenied; AdapterError::ListenRegistration keeps its construction-time class (src/error.rs doc records the sibling) - listen_request_dispatch: register moved before the bound-port out-param write; on refusal the listener cap releases, the uniform decline event emits, and the minted fabric listener tears down immediately (never a half state) (src/handler.rs) - Wire row a_collided_mint_is_refused_and_never_steals_the_live_route pins the full shape through a mint-strategy construct on the fabric double (MintStrategy::WithCollisions — the test forces what the fabric's discipline prevents): second register refuses, first route still serves end to end, refused endpoint never serves, count released (tests/tunnel_mapping.rs) Verification: cargo test (default + git/session/tunnel/sftp + --all-features) · clippy ×2 -D warnings · fmt --check · cargo doc --no-deps · taskgraph validate ✓ (35 tasks)
2278 lines
96 KiB
Rust
2278 lines
96 KiB
Rust
//! The `w3-tunnel-direct` contract rows — the tunnel branch served for
|
|
//! real: the route-1 bridge (ADR-006 decision 1) lands
|
|
//! `direct-tcpip`/`direct-streamlocal` through the published alktunnels
|
|
//! 0.4.1 seam (`TunnelSession::open_direct` → accept only after the
|
|
//! fabric open succeeds → `take_halves()` pumped against the taken
|
|
//! channel), observed through the loopback harness against the **fabric
|
|
//! double** (`w3-tunnel-direct`'s recorded test composition: the
|
|
//! POC-3 §C.4 in-process recipe — `ChannelsAdapter` +
|
|
//! `InstallChannelZero` + the alktunnels openables + a hand-rolled
|
|
//! dial/dial-fail seam, hand-rolled here, never a sibling-checkout
|
|
//! path).
|
|
//!
|
|
//! The decline-matrix classes *per feature build* stay in
|
|
//! `tests/dispatch_core.rs` (unfeatured rows in the featureless/git/
|
|
//! session builds; the absent-handle rows in the tunnel builds); this
|
|
//! binary adds the featured-build rows the fabric double makes
|
|
//! observable: the serve class (data round trip, both substrates), the
|
|
//! fabric-failure class (`ConnectFailed`), the cap classes (refusal +
|
|
//! the release mechanism: an admitted-but-rejected bridge releases its
|
|
//! count, a closed serving channel recovers), the composed identity
|
|
//! gate (the fabric's `AccessControl::check` refusing an unscoped
|
|
//! channels identity — the visible-surface invariant's alkcall half,
|
|
//! with the establisher never reached), the establisher's identity
|
|
//! witness (the POC-3 §C.3 shape), the OQ-11 off-Unix gate, and the
|
|
//! real-tool `ssh -L` smoke.
|
|
//!
|
|
//! The fabric double's dial seam is in-process by design: the dial
|
|
//! target is matched against a map of pre-created duplex halves keyed
|
|
//! by the rendered target string (`host:port` / socket path — exactly
|
|
//! the shape the establisher hands the `DialFn`), so the serve rows
|
|
//! are fully deterministic (no sockets on the target substrate), while
|
|
//! the real-tool smoke hand-rolls a real-TCP dial closure against a
|
|
//! real loopback endpoint (the smoke exercises the substrate for real;
|
|
//! tooling posture recorded like the git task — skipped-with-reason
|
|
//! when `ssh`/`curl` is unavailable, never silently green).
|
|
|
|
#![allow(dead_code)]
|
|
|
|
mod common;
|
|
|
|
#[cfg(feature = "tunnel")]
|
|
mod serving {
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
|
|
|
use alkcall::channels::adapter::{ChannelsAdapter, InstallChannelZero};
|
|
use alkcall::channels::client::{ChannelClient, ServingConfig};
|
|
use alkcall::channels::operations::{ChannelCore, ChannelOperations, Establishment};
|
|
use alkcall::core::auth::{AuthContext, Identity, NoopIdentityProvider};
|
|
use alkcall::core::types::{Connection as CoreConnection, ProtocolHandler};
|
|
use alkcall::protocol::connection::{CallConnection, split_single_stream};
|
|
use alkcall::protocol::dispatch::Dispatcher;
|
|
use alkcall::registry::registration::OperationRegistry;
|
|
use alktunnels::params::{
|
|
Substrate, SubstrateAddr, TUNNEL_DIRECT_SCOPE, TUNNEL_FORWARDED_SCOPE, TUNNEL_OPEN_SCOPE,
|
|
};
|
|
use alktunnels::producer::{
|
|
BindFirstAcceptLoop, BindFirstPlan, BindFn, BoundListener, DialFn, ForwardedOpener,
|
|
IdentityWitness, TargetHandle, TunnelEstablishError, make_bindfirst_accept_handler,
|
|
register_tunnel_direct_openable,
|
|
};
|
|
use russh::client::Msg;
|
|
use russh::{Channel, ChannelMsg, ChannelOpenFailure, Error};
|
|
use std::time::Duration;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::sync::mpsc;
|
|
|
|
use super::common::harness::{
|
|
ForwardedOpen, LoopbackSession, cancel_tcpip_forward, drive, open_direct_streamlocal,
|
|
open_direct_tcpip, serve_config as harness_serve_config, tcpip_forward,
|
|
};
|
|
use super::common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity};
|
|
use alkssh::{Limits, SshAdapter, TunnelListenSeam};
|
|
|
|
type TargetMap = Arc<std::sync::Mutex<HashMap<String, tokio::io::DuplexStream>>>;
|
|
|
|
/// The in-process fabric (`w3-tunnel-direct`'s recorded double, the
|
|
/// POC-3 §C.4 recipe): an `alk/channels` connection over a tokio
|
|
/// duplex whose accept side registers `ChannelOperations` + the
|
|
/// `channels/tunnel/direct` openable (the establisher's dial seam
|
|
/// injected), and whose connect side is the `Arc<ChannelClient>` a
|
|
/// tunnel-featured serve-config holds. The accept side's channel-0
|
|
/// identity (`caller`) is what the fabric's
|
|
/// `AccessControl::check(peer_identity)` gates the mapped opens
|
|
/// against — the assembly-composed identity of the real deployment;
|
|
/// `witness` records what the establisher's per-call auth (alkcall
|
|
/// CF-006) saw.
|
|
pub(super) async fn wire_fabric(
|
|
dial: DialFn,
|
|
caller: Identity,
|
|
witness: IdentityWitness,
|
|
) -> Arc<ChannelClient> {
|
|
let registry = Arc::new(OperationRegistry::new());
|
|
let registry_for_hook = Arc::clone(®istry);
|
|
let witness_for_hook = Arc::clone(&witness);
|
|
let caller_for_hook = caller.clone();
|
|
let dial_for_hook = Arc::clone(&dial);
|
|
|
|
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
|
|
let registry = Arc::clone(®istry_for_hook);
|
|
let witness = Arc::clone(&witness_for_hook);
|
|
let caller = caller_for_hook.clone();
|
|
let dial = Arc::clone(&dial_for_hook);
|
|
tokio::spawn(async move {
|
|
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
|
|
return;
|
|
};
|
|
let (writer, reader) = split_single_stream(channel0_bidi);
|
|
// The serving-side identity set once at install — the
|
|
// hub_leg.rs one-time-set precedent the door's
|
|
// `set_identity` mechanics decision reads (the identity
|
|
// the serving dispatch resolves for the mapped opens'
|
|
// ACL).
|
|
let _ = channel0_conn.set_identity(caller);
|
|
let call_connection = Arc::new(CallConnection::new_single_stream(
|
|
channel0_conn,
|
|
Arc::clone(&writer),
|
|
));
|
|
let ops = ChannelOperations::with_default_policy(manager.clone());
|
|
ops.register_on(®istry).expect("channel ops register");
|
|
register_tunnel_direct_openable(
|
|
&ChannelCore::new(manager.clone(), alkcall::channels::policy::default_policy()),
|
|
®istry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
dial,
|
|
Some(witness),
|
|
)
|
|
.expect("tunnel direct openable registers");
|
|
let dp = Dispatcher::new(registry, Arc::new(NoopIdentityProvider));
|
|
dp.serve_single_stream(call_connection, reader, writer)
|
|
.await;
|
|
})
|
|
});
|
|
|
|
let (consumer_end, producer_end) = tokio::io::duplex(64 * 1024);
|
|
let consumer_conn = CoreConnection::from_bidi(consumer_end, b"alk/channels".to_vec(), None);
|
|
let producer_conn = CoreConnection::from_bidi(producer_end, b"alk/channels".to_vec(), None);
|
|
|
|
let adapter =
|
|
ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
|
|
tokio::spawn(async move {
|
|
let _ = ProtocolHandler::handle(
|
|
&adapter,
|
|
producer_conn,
|
|
&AuthContext::anonymous(b"alk/channels"),
|
|
)
|
|
.await;
|
|
});
|
|
|
|
Arc::new(
|
|
ChannelClient::from_connection(consumer_conn)
|
|
.await
|
|
.expect("the fabric's channels client establishes"),
|
|
)
|
|
}
|
|
|
|
/// The leg-2 fabric double (`w3-tunnel-listen`'s recorded
|
|
/// composition): the leg-1 wire fabric extended with the
|
|
/// **bind-first listen half** — the assembly-composed shape the
|
|
/// vanilla `-R` listen flow rides, built exactly on the published
|
|
/// alktunnels pieces: a `bindfirst_establisher`-shaped
|
|
/// establisher, `make_bindfirst_accept_handler`, and the
|
|
/// `ForwardedOpener` dialing the forwarded op back at the door.
|
|
///
|
|
/// The **door-side serving surface**: the `Arc<ChannelClient>`
|
|
/// constructed with `from_connection_with_serving` against a
|
|
/// shared registry the row hands alkssh through the
|
|
/// `TunnelListenSeam` (one `Arc`, both sides).
|
|
///
|
|
/// The double's bind seam is the **dynamic-address producer** the
|
|
/// door's naming convention front: the establisher treats the
|
|
/// wire-mapped resource string as the bind endpoint directly
|
|
/// (`resource = "127.0.0.1:9200"` binds `127.0.0.1:9200`) and mints
|
|
/// the correlation token per the ADR-008 discipline. Who may bind
|
|
/// what is the fabric's policy — the double binds what the room
|
|
/// asks; a real fabric's ACL is the same seam. `localhost`
|
|
/// renders to `127.0.0.1` so the real-tool smoke's curl is
|
|
/// deterministic about the family.
|
|
pub(super) struct FabricDouble {
|
|
pub(super) client: Arc<ChannelClient>,
|
|
pub(super) registry: Arc<OperationRegistry>,
|
|
opener: Arc<tokio::sync::OnceCell<ForwardedOpener>>,
|
|
/// The establisher's mint stream (the rows' real-token vehicle
|
|
/// — the ACL row needs the genuinely minted token its listen's
|
|
/// fabric established; the SSH layer never carries the token).
|
|
minted: tokio::sync::Mutex<mpsc::UnboundedReceiver<u32>>,
|
|
}
|
|
|
|
impl FabricDouble {
|
|
/// The fabric's `ForwardedOpener` (the call plane), exported
|
|
/// for the rows' direct drives (an arrival the fabric would
|
|
/// make, dialed by the row itself — the unknown-token and
|
|
/// composed-gate rows' vehicle).
|
|
pub(super) async fn forwarded_opener(&self) -> ForwardedOpener {
|
|
for _ in 0..600 {
|
|
if let Some(opener) = self.opener.get() {
|
|
return Arc::clone(opener);
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
panic!("the fabric's forwarded opener never installed");
|
|
}
|
|
|
|
/// Drives the fabric's forwarded-open closure directly:
|
|
/// `Ok` carries the adopted fabric-side halves (the door
|
|
/// allocated the channel — the correlation-miss row's
|
|
/// teardown-at-birth is then observable as an EOF on the
|
|
/// `recv` half), `Err` is the door-side refusal (the
|
|
/// composed-gate row's ACL answer — the arrival never became
|
|
/// anything).
|
|
/// The next minted correlation token (one per accepted listen,
|
|
/// in mint order — the rows' real-token vehicle).
|
|
pub(super) async fn minted_token(&self) -> u32 {
|
|
self.minted
|
|
.lock()
|
|
.await
|
|
.recv()
|
|
.await
|
|
.expect("the fabric's establisher minted a token in time")
|
|
}
|
|
|
|
pub(super) async fn open_forwarded_directly(
|
|
&self,
|
|
params: alktunnels::params::TunnelForwardedParams,
|
|
) -> Result<
|
|
(
|
|
alkcall::channels::reassembly::MpscSendStream,
|
|
alkcall::channels::reassembly::MpscRecvStream,
|
|
),
|
|
TunnelEstablishError,
|
|
> {
|
|
let opener = self.forwarded_opener().await;
|
|
opener(params).await
|
|
}
|
|
}
|
|
|
|
/// The double's establisher mint discipline: `Counter` is the
|
|
/// ADR-008 shape the real fabric carries (a non-zero-seeded
|
|
/// counter, never 0, never re-minted); `WithCollisions` is the
|
|
/// row-pinned collision construct — the first `collisions` mints
|
|
/// return a fixed `token` (the test forces what the fabric's
|
|
/// discipline prevents), then the counter continues from where it
|
|
/// left.
|
|
#[derive(Clone, Copy)]
|
|
pub(super) enum MintStrategy {
|
|
Counter,
|
|
WithCollisions { token: u32, collisions: usize },
|
|
}
|
|
|
|
/// The dynamic-bind producer's establisher (the double's shape of
|
|
/// the `bindfirst_establisher` recipe): parse
|
|
/// `TunnelParams {resource, substrate}`, bind via the injected
|
|
/// [`BindFn`] directly on the resource string, mint the token per
|
|
/// the ADR-008 discipline (non-zero-seeded counter, never 0), and
|
|
/// contribute the `bound` + `listen` reply fields (ADR-049
|
|
/// amendment 3).
|
|
fn dynamic_bindfirst_establisher(
|
|
bind: BindFn,
|
|
minted: Arc<mpsc::UnboundedSender<u32>>,
|
|
mint: MintStrategy,
|
|
) -> alkcall::channels::operations::OpenEstablisher {
|
|
let counter = Arc::new(AtomicU32::new(1));
|
|
let (collided_token, remaining_collisions) = match mint {
|
|
MintStrategy::Counter => (0, 0usize),
|
|
MintStrategy::WithCollisions { token, collisions } => (token, collisions),
|
|
};
|
|
let collisions = Arc::new(AtomicUsize::new(remaining_collisions));
|
|
Arc::new(move |input: serde_json::Value, _auth| {
|
|
let bind = Arc::clone(&bind);
|
|
let minted = Arc::clone(&minted);
|
|
let counter = Arc::clone(&counter);
|
|
let collisions = Arc::clone(&collisions);
|
|
Box::pin(async move {
|
|
let params: alktunnels::params::TunnelParams =
|
|
serde_json::from_value(input.clone()).map_err(|e| {
|
|
TunnelEstablishError::HandlerError(format!("invalid params: {e}"))
|
|
})?;
|
|
let token = if collided_token != 0 && collisions.load(Ordering::Relaxed) > 0 {
|
|
collisions.fetch_sub(1, Ordering::Relaxed);
|
|
collided_token
|
|
} else {
|
|
let next = counter.fetch_add(1, Ordering::Relaxed);
|
|
if next == 0 { 1 } else { next }
|
|
};
|
|
let _ = minted.send(token);
|
|
let BoundListener { listener, bound } =
|
|
bind(params.substrate, ¶ms.resource).await?;
|
|
let plan = BindFirstPlan {
|
|
listener,
|
|
token,
|
|
substrate: params.substrate,
|
|
};
|
|
Ok(Establishment::new(Arc::new(plan))
|
|
.with_reply_field(
|
|
"bound",
|
|
serde_json::to_value(&bound)
|
|
.map_err(|e| TunnelEstablishError::HandlerError(format!("{e}")))?,
|
|
)
|
|
.with_reply_field("listen", serde_json::json!(token)))
|
|
})
|
|
})
|
|
}
|
|
|
|
/// The double's real-socket bind seam: real TCP/unix binds of the
|
|
/// wire-named endpoint (the platform boundary the
|
|
/// `tunnels/local` feature serves in a real deployment —
|
|
/// hand-rolled here, never a sibling-checkout path). Unix binds
|
|
/// unlink a stale socket file first (`bind_unix`'s caller
|
|
/// posture).
|
|
fn real_bind() -> BindFn {
|
|
Arc::new(move |substrate, endpoint: &str| {
|
|
let endpoint = endpoint.to_owned();
|
|
Box::pin(async move {
|
|
match substrate {
|
|
alktunnels::params::Substrate::Unix => {
|
|
let _ = std::fs::remove_file(&endpoint);
|
|
let listener = tokio::net::UnixListener::bind(&endpoint).map_err(|e| {
|
|
TunnelEstablishError::ResourceShortage(format!(
|
|
"bind `{endpoint}`: {e}"
|
|
))
|
|
})?;
|
|
Ok(BoundListener {
|
|
listener: Arc::new(listener),
|
|
bound: SubstrateAddr::Path {
|
|
path: endpoint.clone(),
|
|
},
|
|
})
|
|
}
|
|
_ => {
|
|
let hostport = endpoint.replace("localhost:", "127.0.0.1:");
|
|
let listener =
|
|
tokio::net::TcpListener::bind(&hostport)
|
|
.await
|
|
.map_err(|e| {
|
|
TunnelEstablishError::ResourceShortage(format!(
|
|
"bind `{hostport}`: {e}"
|
|
))
|
|
})?;
|
|
let addr = listener.local_addr().expect("bound tcp addr");
|
|
Ok(BoundListener {
|
|
listener: Arc::new(listener),
|
|
bound: SubstrateAddr::HostPort {
|
|
host: addr.ip().to_string(),
|
|
port: addr.port(),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// The double's accept loop: real TCP/unix accept of ONE pair per
|
|
/// invocation (the dispatcher's per-accept contract), downcasting
|
|
/// the plan's opaque listener to the bind seam's own wrappers. The
|
|
/// unix leg's `peer` is fabricated (an unnamed peer cannot ride
|
|
/// the forwarded op's required `peer` param — the shipped
|
|
/// accept-pair posture).
|
|
fn real_accept_loop() -> BindFirstAcceptLoop {
|
|
Arc::new(move |listener: Arc<dyn std::any::Any + Send + Sync>| {
|
|
Box::pin(async move {
|
|
if let Some(tcp) = listener.downcast_ref::<tokio::net::TcpListener>() {
|
|
let (stream, peer) = tcp
|
|
.accept()
|
|
.await
|
|
.map_err(|e| TunnelEstablishError::DialFailed(format!("accept: {e}")))?;
|
|
let (read, write) = stream.into_split();
|
|
return Ok((
|
|
SubstrateAddr::HostPort {
|
|
host: peer.ip().to_string(),
|
|
port: peer.port(),
|
|
},
|
|
TargetHandle {
|
|
read: Box::new(read),
|
|
write: Box::new(write),
|
|
},
|
|
));
|
|
}
|
|
if let Some(unix) = listener.downcast_ref::<tokio::net::UnixListener>() {
|
|
let (stream, _peer) = unix
|
|
.accept()
|
|
.await
|
|
.map_err(|e| TunnelEstablishError::DialFailed(format!("accept: {e}")))?;
|
|
let (read, write) = stream.into_split();
|
|
return Ok((
|
|
SubstrateAddr::Path {
|
|
path: "/fabric-arrival-peer.sock".to_owned(),
|
|
},
|
|
TargetHandle {
|
|
read: Box::new(read),
|
|
write: Box::new(write),
|
|
},
|
|
));
|
|
}
|
|
Err(TunnelEstablishError::HandlerError(
|
|
"the plan's listener is not one of the double's binds".into(),
|
|
))
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Wires the leg-2 fabric double with the ADR-008 mint discipline
|
|
/// (the rows' default mint).
|
|
pub(super) async fn wire_fabric_with_listen(
|
|
door_scoped_caller: bool,
|
|
arrival_scoped: bool,
|
|
) -> FabricDouble {
|
|
wire_fabric_with_listen_mint(door_scoped_caller, arrival_scoped, MintStrategy::Counter)
|
|
.await
|
|
}
|
|
|
|
/// Wires the leg-2 fabric double (leg 1 unchanged underneath — the
|
|
/// dial seam + direct openable stay exactly `wire_fabric`'s
|
|
/// composition, against a leg-1 fail dial the -R rows never
|
|
/// exercise). `door_scoped_caller` composes the fabric-side
|
|
/// serving identity (what gates the door's listen op's ACL there);
|
|
/// `arrival_scoped` composes the door-side `ServingConfig.identity`
|
|
/// with or without the `tunnel:forwarded` scope (the composed-gate
|
|
/// row's negative leg). `mint` is the establisher's mint
|
|
/// discipline ([`MintStrategy`]) — the collision rows' vehicle.
|
|
pub(super) async fn wire_fabric_with_listen_mint(
|
|
door_scoped_caller: bool,
|
|
arrival_scoped: bool,
|
|
mint: MintStrategy,
|
|
) -> FabricDouble {
|
|
// The two sides' registries are independent (the fabric's own
|
|
// dispatch registry; and the DOOR-side shared one the row
|
|
// hands alkssh's seam — the registration into the fabric's
|
|
// registry would be the silent-fork shape). The earlier draft
|
|
// wired one Arc to both — pinned wrong here for exactly one
|
|
// reason: the door's `tunnel/forwarded` openable must live in
|
|
// the DOOR's dispatch surface alone.
|
|
let door_registry = Arc::new(OperationRegistry::new());
|
|
let registry = Arc::new(OperationRegistry::new());
|
|
let registry_for_hook = Arc::clone(®istry);
|
|
let door_caller = if door_scoped_caller {
|
|
identity("door-caller", &[TUNNEL_OPEN_SCOPE])
|
|
} else {
|
|
identity("door-caller-no-scope", &["ssh"])
|
|
};
|
|
let arrival_identity = if arrival_scoped {
|
|
identity("fabric-arrival", &[TUNNEL_FORWARDED_SCOPE])
|
|
} else {
|
|
identity("fabric-arrival-plain", &["ssh"])
|
|
};
|
|
let dial: DialFn = Arc::new(
|
|
move |_substrate: alktunnels::params::Substrate, target: &str| {
|
|
let target = target.to_owned();
|
|
Box::pin(async move {
|
|
Err::<TargetHandle, TunnelEstablishError>(TunnelEstablishError::DialFailed(
|
|
format!("the leg-1 dial double is not part of the -R rows: {target}"),
|
|
))
|
|
})
|
|
},
|
|
);
|
|
let opener_cell = Arc::new(tokio::sync::OnceCell::new());
|
|
let opener_for_hook = Arc::clone(&opener_cell);
|
|
let (minted_sender, minted_receiver) = mpsc::unbounded_channel::<u32>();
|
|
let minted_tx: Arc<mpsc::UnboundedSender<u32>> = Arc::new(minted_sender);
|
|
let minted_for_hook = minted_tx.clone();
|
|
|
|
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
|
|
let registry = Arc::clone(®istry_for_hook);
|
|
let door_caller = door_caller.clone();
|
|
let minted_tx = minted_for_hook.clone();
|
|
let dial = Arc::clone(&dial);
|
|
let opener_cell = Arc::clone(&opener_for_hook);
|
|
tokio::spawn(async move {
|
|
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
|
|
return;
|
|
};
|
|
let (writer, reader) = split_single_stream(channel0_bidi);
|
|
let _ = channel0_conn.set_identity(door_caller);
|
|
let call_connection = Arc::new(CallConnection::new_single_stream(
|
|
channel0_conn,
|
|
Arc::clone(&writer),
|
|
));
|
|
|
|
// The fabric's ForwardedOpener (the call plane):
|
|
// dial the forwarded op at the door, adopt the
|
|
// door-allocated id on the fabric's manager, and
|
|
// return the adopted channel's halves
|
|
// (make_bindfirst_accept_handler composes the
|
|
// pump). Exported for the rows' direct drives.
|
|
let open_forwarded: ForwardedOpener = {
|
|
let call = Arc::clone(&call_connection);
|
|
let manager = manager.clone();
|
|
Arc::new(move |params: alktunnels::params::TunnelForwardedParams| {
|
|
let call = Arc::clone(&call);
|
|
let manager = manager.clone();
|
|
Box::pin(async move {
|
|
let payload = serde_json::to_value(¶ms)
|
|
.map_err(|e| TunnelEstablishError::HandlerError(format!("{e}")))?;
|
|
let response = call
|
|
.call(alktunnels::params::OP_TUNNEL_FORWARDED, payload)
|
|
.await;
|
|
let out = response.result.map_err(|e| {
|
|
TunnelEstablishError::HandlerError(format!("forwarded open: {e:?}"))
|
|
})?;
|
|
let id =
|
|
out.get("channel_id")
|
|
.and_then(|v| v.as_u64())
|
|
.ok_or_else(|| {
|
|
TunnelEstablishError::HandlerError("no channel_id".into())
|
|
})? as u32;
|
|
manager
|
|
.adopt_channel(id, alktunnels::params::TUNNEL_ALPN, None)
|
|
.await
|
|
.map_err(|e| {
|
|
TunnelEstablishError::HandlerError(format!("adopt: {e:?}"))
|
|
})
|
|
})
|
|
})
|
|
};
|
|
let _ = opener_cell.set(Arc::clone(&open_forwarded));
|
|
|
|
let ops = ChannelOperations::with_default_policy(manager.clone());
|
|
ops.register_on(®istry).expect("channel ops register");
|
|
register_tunnel_direct_openable(
|
|
&ChannelCore::new(manager.clone(), alkcall::channels::policy::default_policy()),
|
|
®istry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
dial,
|
|
None,
|
|
)
|
|
.expect("the direct openable registers");
|
|
let core =
|
|
ChannelCore::new(manager.clone(), alkcall::channels::policy::default_policy());
|
|
core.register_openable_with_establisher(
|
|
alktunnels::params::tunnel_open_spec(),
|
|
Some(dynamic_bindfirst_establisher(
|
|
real_bind(),
|
|
Arc::clone(&minted_tx),
|
|
mint,
|
|
)),
|
|
make_bindfirst_accept_handler(real_accept_loop(), open_forwarded),
|
|
®istry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
None,
|
|
)
|
|
.expect("the bind-first listen openable registers");
|
|
|
|
let dp = Dispatcher::new(registry, Arc::new(NoopIdentityProvider));
|
|
dp.serve_single_stream(call_connection, reader, writer)
|
|
.await;
|
|
})
|
|
});
|
|
|
|
let (consumer_end, producer_end) = tokio::io::duplex(64 * 1024);
|
|
let consumer_conn = CoreConnection::from_bidi(consumer_end, b"alk/channels".to_vec(), None);
|
|
let producer_conn = CoreConnection::from_bidi(producer_end, b"alk/channels".to_vec(), None);
|
|
|
|
let adapter =
|
|
ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
|
|
tokio::spawn(async move {
|
|
let _ = ProtocolHandler::handle(
|
|
&adapter,
|
|
producer_conn,
|
|
&AuthContext::anonymous(b"alk/channels"),
|
|
)
|
|
.await;
|
|
});
|
|
|
|
let client = ChannelClient::from_connection_with_serving(
|
|
consumer_conn,
|
|
Some(ServingConfig {
|
|
registry: Arc::clone(&door_registry),
|
|
identity_provider: Arc::new(NoopIdentityProvider),
|
|
identity: Some(arrival_identity),
|
|
}),
|
|
)
|
|
.await
|
|
.expect("the fabric's channels client establishes");
|
|
|
|
FabricDouble {
|
|
client: Arc::new(client),
|
|
registry: door_registry,
|
|
opener: opener_cell,
|
|
minted: tokio::sync::Mutex::new(minted_receiver),
|
|
}
|
|
}
|
|
|
|
/// A pre-created duplex target — the "in-process dial" half: the
|
|
/// test holds the returned side, the map hands the dial seam the
|
|
/// other half when the establisher renders a matching target.
|
|
fn inproc_target(targets: &TargetMap, key: &str, capacity: usize) -> tokio::io::DuplexStream {
|
|
let (target_side, test_side) = tokio::io::duplex(capacity);
|
|
targets
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.insert(key.to_owned(), target_side);
|
|
test_side
|
|
}
|
|
|
|
/// The hand-rolled dial seam (the assembly's injected `DialFn`,
|
|
/// doubled): an exact `host:port` / socket-path key pops the
|
|
/// pre-created half; an absent key is the dial-fail seam
|
|
/// (`TunnelEstablishError::DialFailed`) — both the fabric-failure
|
|
/// rows' shape and the release rows' failure leg. The call counter
|
|
/// witnesses the "no fabric call" assertions.
|
|
fn inproc_dial(targets: TargetMap, calls: Arc<AtomicUsize>) -> DialFn {
|
|
Arc::new(move |_substrate: Substrate, target: &str| {
|
|
let targets = Arc::clone(&targets);
|
|
let calls = Arc::clone(&calls);
|
|
let target = target.to_owned();
|
|
Box::pin(async move {
|
|
calls.fetch_add(1, Ordering::SeqCst);
|
|
let half = targets
|
|
.lock()
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
.remove(&target)
|
|
.ok_or_else(|| {
|
|
TunnelEstablishError::DialFailed(format!(
|
|
"in-process double target `{target}` absent"
|
|
))
|
|
})?;
|
|
let (read, write) = tokio::io::split(half);
|
|
Ok(TargetHandle {
|
|
read: Box::new(read),
|
|
write: Box::new(write),
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
/// The fabric caller identity with the direct op's scope — the
|
|
/// composed-gate identity the serving side resolves, so the ACL
|
|
/// admits the mapped opens.
|
|
fn scoped_caller() -> Identity {
|
|
identity("tunnel-caller", &[TUNNEL_DIRECT_SCOPE])
|
|
}
|
|
|
|
/// The tunnel build's authenticated loopback session over the
|
|
/// doubles, with the fabric client wired in: the fixture identity
|
|
/// resolves through the provider and the serve-config carries the
|
|
/// `channel_client` (the route-1 bridge's family-fabric entry);
|
|
/// `limits` rides the serve-config (the cap rows tune them).
|
|
async fn tunnel_session(client: Arc<ChannelClient>, limits: Limits) -> LoopbackSession {
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve = harness_serve_config(provider, &[fixture("bravo")], limits);
|
|
serve.channel_client = Some(client);
|
|
let adapter = SshAdapter::new(serve)
|
|
.await
|
|
.expect("the tunnel build's adapter constructs");
|
|
|
|
let policy = Arc::new(MemHostKeyPolicy::with_default(true));
|
|
let mut session = drive(adapter, Arc::clone(&policy), Limits::default())
|
|
.await
|
|
.expect("the loopback mounts");
|
|
|
|
let result =
|
|
authenticate_publickey(&mut session.client, "tunnel-rider", fixture("alpha")).await;
|
|
assert!(
|
|
result.success(),
|
|
"the fixture identity authenticates before every tunnel row"
|
|
);
|
|
session
|
|
}
|
|
|
|
/// The harness import that needs the loopback session's client
|
|
/// (kept near the top of the module for the rows' readability).
|
|
use super::common::harness::authenticate_publickey;
|
|
|
|
/// Matches an open failure's SSH reason — the wire decline reason
|
|
/// the rows assert.
|
|
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
|
assert!(
|
|
matches!(&err, Error::ChannelOpenFailure(reason) if *reason == expected),
|
|
"{context}: expected open decline with {expected:?}, got {err:?}"
|
|
);
|
|
}
|
|
|
|
/// The serve row's data leg: the SSH channel against the in-process
|
|
/// target — one request down `direct-tcpip`, one reply back (the
|
|
/// bidirectional pump of the route-1 bridge, the POC-3 §C.1 shape:
|
|
/// zero framing, both directions live).
|
|
async fn round_trip(
|
|
channel: &mut Channel<Msg>,
|
|
mut target_side: tokio::io::DuplexStream,
|
|
request: &[u8],
|
|
reply: &[u8],
|
|
context: &str,
|
|
) {
|
|
channel
|
|
.data_bytes(request.to_vec())
|
|
.await
|
|
.expect("the SSH channel carries the request down");
|
|
|
|
let mut seen_request = vec![0u8; request.len()];
|
|
tokio::time::timeout(
|
|
std::time::Duration::from_secs(15),
|
|
target_side.read_exact(&mut seen_request),
|
|
)
|
|
.await
|
|
.expect("the request arrives through the bridge in time")
|
|
.expect("the pump delivers the request to the substrate");
|
|
assert_eq!(
|
|
seen_request, request,
|
|
"{context}: the bridge pumps the SSH channel bytes unframed"
|
|
);
|
|
|
|
target_side
|
|
.write_all(reply)
|
|
.await
|
|
.expect("the target half writes the reply");
|
|
target_side.flush().await.expect("the flush rides");
|
|
|
|
let mut seen_reply = Vec::new();
|
|
tokio::time::timeout(std::time::Duration::from_secs(15), async {
|
|
while seen_reply.len() < reply.len() {
|
|
match channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen_reply.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("{context}: the channel ended before the reply arrived"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the reply arrives through the bridge in time");
|
|
seen_reply.truncate(reply.len());
|
|
assert_eq!(
|
|
seen_reply, reply,
|
|
"{context}: the bridge pumps the substrate bytes back unframed"
|
|
);
|
|
}
|
|
|
|
/// - *Contract stamp:* ADR-006 decision 1 (the route-1 bridge: the
|
|
/// `direct-tcpip` open → `TunnelSession::open_direct` over a held
|
|
/// `ChannelClient` → accept only after the fabric open succeeds →
|
|
/// `take_halves()` pumped bidirectionally — the POC-3 §C.1 shape,
|
|
/// zero framing both directions; the open hook returns fast).
|
|
#[tokio::test]
|
|
async fn direct_tcpip_serves_through_the_route_1_bridge() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let witness = Arc::new(tokio::sync::Mutex::new(None));
|
|
let target_side = inproc_target(&targets, "127.0.0.1:36001", 4096);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), Arc::clone(&calls)),
|
|
scoped_caller(),
|
|
Arc::clone(&witness),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36001)
|
|
.await
|
|
.expect("the featured direct-tcpip row serves through the bridge");
|
|
round_trip(
|
|
&mut channel,
|
|
target_side,
|
|
b"GET /hello HTTP/1.1\r\nHost: target.example\r\n\r\n",
|
|
b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
|
|
"the direct-tcpip serve row",
|
|
)
|
|
.await;
|
|
|
|
let witnessed = witness.lock().await.clone();
|
|
assert_eq!(
|
|
witnessed.as_deref(),
|
|
Some("tunnel-caller"),
|
|
"the direct establisher's per-call auth saw the channels connection's identity (CF-006, POC-3 §C.3's witness)"
|
|
);
|
|
assert_eq!(
|
|
calls.load(Ordering::SeqCst),
|
|
1,
|
|
"one fabric dial rode the open"
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* mapping.md §Channel-type rows
|
|
/// (`direct-streamlocal` → `SubstrateAddr::Path` +
|
|
/// `Substrate::Unix`, the route-1 bridge's second substrate;
|
|
/// ADR-006 decision 1).
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn direct_streamlocal_serves_through_the_route_1_bridge_on_unix() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let target_side = inproc_target(&targets, "/inproc/tunnel-target.sock", 4096);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), calls),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let mut channel = open_direct_streamlocal(&session.client, "/inproc/tunnel-target.sock")
|
|
.await
|
|
.expect("the on-Unix direct-streamlocal row serves through the bridge");
|
|
round_trip(
|
|
&mut channel,
|
|
target_side,
|
|
b"unix-request-bytes",
|
|
b"unix",
|
|
"the direct-streamlocal serve row",
|
|
)
|
|
.await;
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* ADR-006 decision 1 (fabric-open failure →
|
|
/// the explicit `ConnectFailed` reject — accept only after the
|
|
/// fabric open succeeds; the wave-2 stub's reason surviving as
|
|
/// the real decline; POC-3 §C.2's failure shape).
|
|
#[tokio::test]
|
|
async fn a_failed_fabric_open_rejects_the_open_connect_failed() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let client = wire_fabric(
|
|
inproc_dial(targets, Arc::clone(&calls)),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let err = open_direct_tcpip(&session.client, "unreachable.example", 9)
|
|
.await
|
|
.expect_err("the fabric open fails on the absent dial target");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the fabric-failure direct-tcpip row",
|
|
);
|
|
assert_eq!(
|
|
calls.load(Ordering::SeqCst),
|
|
1,
|
|
"the bridge dialed the fabric exactly once — the SSH open carried the fabric's answer"
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded cap release
|
|
/// mechanism (the admission is taken at the hook and held across
|
|
/// the fabric open; a fabric failure releases the count **inside
|
|
/// the spawned task** — the `channel_close` release point only
|
|
/// fires for established channels, so without this release a
|
|
/// failed bridge would leak budget): a failed bridge under a
|
|
/// full cap does not strand the budget — the next open of the
|
|
/// same session is admitted (it would answer
|
|
/// `AdministrativelyProhibited` if the count had leaked).
|
|
#[tokio::test]
|
|
async fn a_failed_bridge_releases_the_cap_count_before_the_next_open() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let target_side = inproc_target(&targets, "127.0.0.1:36002", 512);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), calls),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let limits = Limits {
|
|
max_concurrent_channels: 1,
|
|
..Limits::default()
|
|
};
|
|
let session = tunnel_session(client, limits).await;
|
|
|
|
let err = open_direct_tcpip(&session.client, "unreachable.example", 9)
|
|
.await
|
|
.expect_err("the failing bridge declines");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the cap-release row's failing leg",
|
|
);
|
|
|
|
// The failure answer landed before the client saw it
|
|
// (release-then-reject inside the task), so the very next open
|
|
// of this same session observes the released count.
|
|
let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36002)
|
|
.await
|
|
.expect("the next open is admitted — the failed bridge released its count");
|
|
round_trip(
|
|
&mut channel,
|
|
target_side,
|
|
b"req",
|
|
b"rep",
|
|
"the cap-release row's serving leg",
|
|
)
|
|
.await;
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 9 / `overview.md`
|
|
/// §Bounded-resources policy + `ChannelCap`'s recorded
|
|
/// disjoint-release shape: a *closed serving* channel releases
|
|
/// through the `channel_close` hook — a full cap refuses with the
|
|
/// cap reason, and a recovered cap admits again.
|
|
#[tokio::test]
|
|
async fn the_cap_refuses_beyond_the_ceiling_and_recovers_on_close() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let target_side = inproc_target(&targets, "127.0.0.1:36003", 512);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), calls),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let limits = Limits {
|
|
max_concurrent_channels: 1,
|
|
..Limits::default()
|
|
};
|
|
let session = tunnel_session(client, limits).await;
|
|
|
|
let mut channel = open_direct_tcpip(&session.client, "127.0.0.1", 36003)
|
|
.await
|
|
.expect("the first open admits under the cap");
|
|
round_trip(
|
|
&mut channel,
|
|
target_side,
|
|
b"q",
|
|
b"a",
|
|
"the cap-refusal row's serving channel",
|
|
)
|
|
.await;
|
|
|
|
let err = open_direct_tcpip(&session.client, "127.0.0.1", 36003)
|
|
.await
|
|
.expect_err("the second open rides beyond the cap");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::AdministrativelyProhibited,
|
|
"the cap-exceeded direct-tcpip row",
|
|
);
|
|
|
|
channel.close().await.expect("the serving channel closes");
|
|
// The close cycles back through the packet loop, the
|
|
// established-channel release fires, and the cap recovers. The
|
|
// recovery is observable because the dial target is consumed
|
|
// once: the moment the cap admits again, the open proceeds
|
|
// onto the bridge (and fails at the fabric dial with the
|
|
// connect-failed class) — an `AdministrativelyProhibited`
|
|
// answer here would mean a leaked count.
|
|
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
|
let mut attempts = 0u32;
|
|
loop {
|
|
match open_direct_tcpip(&session.client, "127.0.0.1", 36003).await {
|
|
Ok(opened) => {
|
|
drop(opened);
|
|
return;
|
|
}
|
|
Err(Error::ChannelOpenFailure(
|
|
ChannelOpenFailure::AdministrativelyProhibited,
|
|
)) if attempts < 40 => {
|
|
attempts += 1;
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
}
|
|
Err(Error::ChannelOpenFailure(
|
|
ChannelOpenFailure::AdministrativelyProhibited,
|
|
)) => {
|
|
panic!("the cap never recovered after the close — the count leaked")
|
|
}
|
|
Err(Error::ChannelOpenFailure(_)) => {
|
|
// The open went past the cap answer and onto the
|
|
// bridge (the fabric dial's connect-failed) — the
|
|
// released count is what this row observes.
|
|
return;
|
|
}
|
|
Err(other) => panic!("unexpected open error during recovery: {other:?}"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the cap recovery row resolves in time");
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
|
|
/// invariant's alkcall half): the fabric's
|
|
/// `AccessControl::check(peer_identity)` gates the mapped open —
|
|
/// a channels connection whose identity lacks the direct op's
|
|
/// scope is refused by the fabric's dispatch (`FORBIDDEN`), the
|
|
/// establisher never runs, and the door answers the SSH open
|
|
/// with the explicit `ConnectFailed`. The auth bridge's
|
|
/// SSH-resolved identity and the fabric leg's dial identity
|
|
/// compose at the assembly (the deployment policy) — this row is
|
|
/// the composed-gate witness through the double.
|
|
#[tokio::test]
|
|
async fn the_fabric_acl_refuses_an_unscoped_channels_identity() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let witness = Arc::new(tokio::sync::Mutex::new(None));
|
|
let _ = inproc_target(&targets, "127.0.0.1:36004", 512);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), Arc::clone(&calls)),
|
|
identity("tunnel-caller-no-scope", &["ssh"]),
|
|
Arc::clone(&witness),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let err = open_direct_tcpip(&session.client, "127.0.0.1", 36004)
|
|
.await
|
|
.expect_err("the unscoped channels identity is gated at the fabric");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the fabric-ACL direct-tcpip row",
|
|
);
|
|
assert_eq!(
|
|
calls.load(Ordering::SeqCst),
|
|
0,
|
|
"the ACL check precedes the establisher — a refused open never dialed"
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* mapping.md §Channel-type rows (`direct-tcpip`'s
|
|
/// target is the SSH open arguments verbatim; an impossible wire
|
|
/// target — a port outside the u16 space — is a declined
|
|
/// pre-admission, `invalid-target`, connect-failed on the wire).
|
|
#[tokio::test]
|
|
async fn an_out_of_range_wire_port_declines_pre_admission() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let client = wire_fabric(
|
|
inproc_dial(targets, Arc::clone(&calls)),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let err = open_direct_tcpip(&session.client, "target.example", u32::from(u16::MAX) + 1)
|
|
.await
|
|
.expect_err("an out-of-range wire port declines");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the invalid-target direct-tcpip row",
|
|
);
|
|
assert_eq!(
|
|
calls.load(Ordering::SeqCst),
|
|
0,
|
|
"decline-first, alloc never: an impossible target never reaches the fabric"
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* OQ-11 (the runtime-Unix gate: universal
|
|
/// compile, clean open-decline off-Unix **before any fabric
|
|
/// call** — never a partial bridge): this row compiles in this
|
|
/// binary only off-Unix and pins the gate with its dial-call
|
|
/// witness at zero. On-Unix builds ride the serve row above.
|
|
#[cfg(not(unix))]
|
|
#[tokio::test]
|
|
async fn direct_streamlocal_declines_off_unix_before_any_fabric_call() {
|
|
let targets: TargetMap = Arc::default();
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let _ = inproc_target(&targets, "/inproc/off-unix.sock", 512);
|
|
let client = wire_fabric(
|
|
inproc_dial(Arc::clone(&targets), Arc::clone(&calls)),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let session = tunnel_session(client, Limits::default()).await;
|
|
|
|
let err = open_direct_streamlocal(&session.client, "/inproc/off-unix.sock")
|
|
.await
|
|
.expect_err("the off-Unix streamlocal open declines");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the OQ-11 off-Unix direct-streamlocal row",
|
|
);
|
|
assert_eq!(
|
|
calls.load(Ordering::SeqCst),
|
|
0,
|
|
"the runtime gate precedes any fabric call — no partial bridge"
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
// --- the `-R` listen rows (w3-tunnel-listen) ---------------------------
|
|
|
|
/// The tunnel build's authenticated loopback session over the
|
|
/// leg-2 double, with the fabric leg + listen seam wired (the
|
|
/// `-R` bridge's serve-config halves) and the harness client's
|
|
/// acceptor installed (the matching-`-R`-forward stand-in). The
|
|
/// returned receiver is the forwarded-opens observation channel.
|
|
async fn listen_session(
|
|
double: &FabricDouble,
|
|
limits: Limits,
|
|
) -> (
|
|
LoopbackSession,
|
|
tokio::sync::mpsc::UnboundedReceiver<ForwardedOpen>,
|
|
) {
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve = harness_serve_config(provider, &[fixture("bravo")], limits);
|
|
serve.channel_client = Some(Arc::clone(&double.client));
|
|
serve.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry)));
|
|
let adapter = SshAdapter::new(serve)
|
|
.await
|
|
.expect("the -R wired adapter constructs");
|
|
|
|
let policy = Arc::new(MemHostKeyPolicy::with_default(true));
|
|
let (acceptor, receiver) = super::common::harness::ForwardedAcceptor::new();
|
|
let mut session = super::common::harness::drive_with_acceptor(
|
|
adapter,
|
|
Arc::clone(&policy),
|
|
Limits::default(),
|
|
Some(Arc::new(acceptor)),
|
|
)
|
|
.await
|
|
.expect("the loopback mounts");
|
|
|
|
let result =
|
|
authenticate_publickey(&mut session.client, "tunnel-rider", fixture("alpha")).await;
|
|
assert!(
|
|
result.success(),
|
|
"the fixture identity authenticates before every -R row"
|
|
);
|
|
(session, receiver)
|
|
}
|
|
|
|
/// A free loopback port (the rows' pick helper; the listen binds
|
|
/// exactly what it was asked).
|
|
async fn pick_port() -> u16 {
|
|
let probe = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("the probe binds");
|
|
probe.local_addr().expect("addr").port()
|
|
}
|
|
|
|
/// The arrival row's pump legs: the arrival's request reaches the
|
|
/// forwarded SSH channel, the channel's reply reaches the arrival
|
|
/// socket — the fabric `BiStream` ↔ SSH channel duplex, zero
|
|
/// framing both directions (the route-1 bridge's primitive,
|
|
/// reversed).
|
|
async fn arrival_pump(
|
|
arrival: &mut tokio::net::TcpStream,
|
|
channel: &mut Channel<Msg>,
|
|
request: &[u8],
|
|
reply: &[u8],
|
|
context: &str,
|
|
) {
|
|
let mut seen: Vec<u8> = Vec::new();
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while seen.len() < request.len() {
|
|
match channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("the forwarded channel ended before the request arrived"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the arrival's request reaches the forwarded channel in time");
|
|
assert_eq!(
|
|
seen, request,
|
|
"{context}: the fabric → SSH pump carries the arrival bytes unframed"
|
|
);
|
|
|
|
channel
|
|
.data_bytes(reply.to_vec())
|
|
.await
|
|
.expect("{context}: the SSH channel carries the reply toward the arrival");
|
|
let mut seen_reply = vec![0u8; reply.len()];
|
|
tokio::time::timeout(Duration::from_secs(15), arrival.read_exact(&mut seen_reply))
|
|
.await
|
|
.expect("the reply reaches the arrival socket in time")
|
|
.expect("{context}: the SSH → fabric pump carries the reply bytes unframed");
|
|
assert_eq!(seen_reply, reply, "{context}: the reply arrives verbatim");
|
|
}
|
|
|
|
/// - *Contract stamp:* mapping.md §Global-request rows
|
|
/// (`tcpip_forward` → the family-ward listen dial) + §Channel-type
|
|
/// rows, the `forwarded-tcpip` row (the data leg: per fabric
|
|
/// arrival the door opens a `forwarded-tcpip` channel **toward
|
|
/// the vanilla client**, echoing the listen request's own args —
|
|
/// the OpenSSH matcher parity — and pumping the fabric
|
|
/// `BiStream` ↔ the SSH channel). Observed end to end through the
|
|
/// harness's accept hook.
|
|
#[tokio::test]
|
|
async fn tcpip_forward_serves_through_the_family_listen() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let (session, mut opens) = listen_session(&double, Limits::default()).await;
|
|
|
|
let port = pick_port().await;
|
|
// A nonzero request's success reply carries no port (russh/sshd
|
|
// parity); the arrival's echo below is the actually-bound
|
|
// assertion.
|
|
let _reply = tcpip_forward(&session.client, "127.0.0.1", u32::from(port))
|
|
.await
|
|
.expect("the featured + configured listen serves");
|
|
|
|
let mut arrival = tokio::net::TcpStream::connect(("127.0.0.1", port))
|
|
.await
|
|
.expect("the fabric's listener accepts the arrival");
|
|
arrival
|
|
.write_all(b"arrival-request")
|
|
.await
|
|
.expect("the arrival socket carries the request");
|
|
|
|
let open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the forwarded open arrives in time")
|
|
.expect("the acceptor observed the server-initiated open");
|
|
let ForwardedOpen::Tcpip {
|
|
connected_address,
|
|
connected_port,
|
|
originator_address,
|
|
originator_port,
|
|
mut channel,
|
|
} = open
|
|
else {
|
|
panic!("the -R data leg opens forwarded-tcpip, got another open shape");
|
|
};
|
|
assert_eq!(
|
|
connected_address, "127.0.0.1",
|
|
"the connected address echoes the listen request's wire string verbatim (sshd parity)"
|
|
);
|
|
assert_eq!(
|
|
connected_port,
|
|
u32::from(port),
|
|
"the port echoes the actually-bound number (the port the fabric bound)"
|
|
);
|
|
assert_eq!(
|
|
originator_address, "127.0.0.1",
|
|
"the originator carries the arrival's peer (TunnelForwardedParams.peer)"
|
|
);
|
|
assert!(
|
|
originator_port > 0,
|
|
"the originator's port is the arrival's ephemeral port"
|
|
);
|
|
|
|
arrival_pump(
|
|
&mut arrival,
|
|
&mut channel,
|
|
b"arrival-request",
|
|
b"forwarded-reply",
|
|
"the tcpip_forward serve row",
|
|
)
|
|
.await;
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* `client.md` §Lifecycle primitives (the
|
|
/// bound-port spec: `port = 0` → the server picks and the
|
|
/// actually-bound number returns) — the out-param carries the
|
|
/// fabric's `bound` reply field's port, and the arrival's echo
|
|
/// port is that same allocated number (sshd's `get_local_port`
|
|
/// parity — a forwarded open for a `0`-minted listener never
|
|
/// claims port 0).
|
|
#[tokio::test]
|
|
async fn port_zero_mint_is_resolved_into_the_bound_port_out_param() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let (session, mut opens) = listen_session(&double, Limits::default()).await;
|
|
|
|
let bound = tcpip_forward(&session.client, "127.0.0.1", 0)
|
|
.await
|
|
.expect("the port-0 mint serves");
|
|
assert_ne!(bound, 0, "the fabric's actually-bound port rides out");
|
|
|
|
let mut arrival =
|
|
tokio::net::TcpStream::connect(("127.0.0.1", u16::try_from(bound).expect("u16")))
|
|
.await
|
|
.expect("the minted listener accepts");
|
|
arrival.write_all(b"zero-mint").await.expect("rides");
|
|
|
|
let open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the forwarded open arrives in time")
|
|
.expect("the acceptor observed the open");
|
|
let ForwardedOpen::Tcpip {
|
|
connected_address,
|
|
connected_port,
|
|
mut channel,
|
|
..
|
|
} = open
|
|
else {
|
|
panic!("the tcp forward opened forwarded-tcpip");
|
|
};
|
|
assert_eq!(connected_address, "127.0.0.1");
|
|
assert_eq!(connected_port, bound, "the echo port is the minted one");
|
|
let mut seen: Vec<u8> = Vec::new();
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while seen.len() < 9 {
|
|
match channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("the forwarded channel ended before the arrival rode"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the pump carries the arrival in time");
|
|
assert_eq!(seen, b"zero-mint");
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* the cancel forms (`cancel_tcpip_forward`):
|
|
/// 1:1 into the family listener's teardown — the matching listen
|
|
/// cancels `Ok`, the fabric's bound listener goes away (a
|
|
/// re-connect is refused), and a second cancel of the same
|
|
/// (address, port) pair answers the not-found denial
|
|
/// (`RequestDenied` — the sshd cancel-of-unknown parity).
|
|
#[tokio::test]
|
|
async fn cancel_tcpip_forward_tears_the_fabric_listener_down() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let (session, mut opens) = listen_session(&double, Limits::default()).await;
|
|
|
|
let port = pick_port().await;
|
|
let bound = u32::from(port);
|
|
tcpip_forward(&session.client, "127.0.0.1", bound)
|
|
.await
|
|
.expect("the listen serves before its cancel");
|
|
|
|
cancel_tcpip_forward(&session.client, "127.0.0.1", bound)
|
|
.await
|
|
.expect("the pending listen's cancel answers success");
|
|
let denied = cancel_tcpip_forward(&session.client, "127.0.0.1", bound)
|
|
.await
|
|
.expect_err("a second cancel of the same pair is the not-found denial");
|
|
assert!(
|
|
matches!(denied, Error::RequestDenied),
|
|
"expected RequestDenied, got {denied:?}"
|
|
);
|
|
|
|
// The fabric listener is gone (the teardown cascaded: the
|
|
// door's listener session dropped, the far accept loop's EOF
|
|
// sentinel fired, the plan's listener dropped — the bind
|
|
// releases). Bounded retry: the connect is refused.
|
|
tokio::time::timeout(Duration::from_secs(10), async {
|
|
loop {
|
|
match tokio::net::TcpStream::connect(("127.0.0.1", port)).await {
|
|
Err(_) => return,
|
|
Ok(stream) => drop(stream),
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("the fabric listener never came back after the cancel");
|
|
|
|
// The torn-down listen could not have served a late arrival as
|
|
// an SSH channel: nothing rode the acceptor.
|
|
tokio::time::timeout(Duration::from_millis(500), opens.recv())
|
|
.await
|
|
.expect_err("no forwarded open may follow a cancelled listen");
|
|
assert!(opens.is_empty());
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* the wave-3 gate's booked finding item 4
|
|
/// (`w4-router-collision`): the `ListenRouter` token-collision
|
|
/// pre-check. The fabric's ADR-008 mint discipline is the
|
|
/// upstream guarantee the door defends in depth against: the
|
|
/// test forces what the discipline prevents (the double
|
|
/// re-mints a live token), and the second listen **refuses
|
|
/// fail-closed** — `RequestDenied`, its minted fabric listener
|
|
/// torn down immediately (the refused registration never left a
|
|
/// half state), the listener cap released (the next mint is
|
|
/// admitted against the same 1-count ceiling) — and the **first
|
|
/// route still serves**: the collided mint can never steal a
|
|
/// live adapter's arrival routes.
|
|
#[tokio::test]
|
|
async fn a_collided_mint_is_refused_and_never_steals_the_live_route() {
|
|
let double = wire_fabric_with_listen_mint(
|
|
true,
|
|
true,
|
|
MintStrategy::WithCollisions {
|
|
token: 7,
|
|
collisions: 2,
|
|
},
|
|
)
|
|
.await;
|
|
// The ceiling of 2 both admits the collided mint past the
|
|
// listener cap (so the refusal is the route register, not the
|
|
// cap — the cap row's shape) and makes the refusal's cap
|
|
// release observable: without the release, the third mint
|
|
// would be cap-exceeded.
|
|
let limits = Limits {
|
|
max_concurrent_channels: 2,
|
|
..Limits::default()
|
|
};
|
|
let (session, mut opens) = listen_session(&double, limits).await;
|
|
|
|
let port_a = pick_port().await;
|
|
tcpip_forward(&session.client, "127.0.0.1", u32::from(port_a))
|
|
.await
|
|
.expect("the first listen registers its route normally");
|
|
|
|
// The second mint deliberately re-mints the live token: the
|
|
// route registration refuses, the listen declines fail-closed.
|
|
let port_b = pick_port().await;
|
|
let refused = tcpip_forward(&session.client, "127.0.0.1", u32::from(port_b))
|
|
.await
|
|
.expect_err("the collided mint's listen refuses");
|
|
assert!(
|
|
matches!(refused, Error::RequestDenied),
|
|
"expected RequestDenied, got {refused:?}"
|
|
);
|
|
|
|
// The collided listen's minted listener was torn down
|
|
// immediately: its bound endpoint never serves (bounded retry,
|
|
// the cancel row's teardown-cascade shape).
|
|
tokio::time::timeout(Duration::from_secs(10), async {
|
|
loop {
|
|
match tokio::net::TcpStream::connect(("127.0.0.1", port_b)).await {
|
|
Err(_) => return,
|
|
Ok(stream) => drop(stream),
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("the refused listen's fabric listener was torn down");
|
|
|
|
// The listener count was released at the refusal: against the
|
|
// same 2-count ceiling, the next mint (a fresh token — the
|
|
// collision was two mints on one listen, not a broken fabric)
|
|
// is admitted and registers normally (a `0` mint's success
|
|
// reply carries the actually-bound number — the sshd parity).
|
|
let third = tcpip_forward(&session.client, "127.0.0.1", 0)
|
|
.await
|
|
.expect("the collision released the listener count");
|
|
assert_ne!(third, 0);
|
|
|
|
// The first route still serves — the steal is impossible: an
|
|
// arrival onto the live route's endpoint opens the
|
|
// forwarded-tcpip channel and the bytes ride the pump.
|
|
let mut arrival = tokio::net::TcpStream::connect(("127.0.0.1", port_a))
|
|
.await
|
|
.expect("the live route's listener still accepts arrivals");
|
|
let request = b"still-served";
|
|
arrival
|
|
.write_all(request)
|
|
.await
|
|
.expect("the arrival's bytes ride toward the door");
|
|
let open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the live route's arrival open arrives in time")
|
|
.expect("observed");
|
|
let ForwardedOpen::Tcpip { mut channel, .. } = open else {
|
|
panic!("the live route's arrival opened something else");
|
|
};
|
|
let mut seen: Vec<u8> = Vec::new();
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while seen.len() < request.len() {
|
|
match channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("the live route's channel ended before the request rode"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the live route's pump rides in time");
|
|
assert_eq!(seen, request, "the collided mint stole nothing");
|
|
|
|
// Nothing rode the acceptor beyond the live route's served
|
|
// arrival (the refused mint's listener was torn down at birth
|
|
// — its arrivals could not exist).
|
|
tokio::time::timeout(Duration::from_millis(500), opens.recv())
|
|
.await
|
|
.expect_err("no arrival may follow the refused mint");
|
|
assert!(opens.is_empty());
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* the arrival pump's correlation-miss class
|
|
/// (`w3-tunnel-listen` — teardown-at-birth, never a half-open SSH
|
|
/// channel): an arrival whose `listen` token has no route
|
|
/// (nothing was ever minted for this door) is dropped by the
|
|
/// door-side handler — the open op's channel tears down at
|
|
/// birth, and the fabric side's adopted halves EOF immediately.
|
|
#[tokio::test]
|
|
async fn a_correlation_miss_adopts_no_ssh_channel() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve = harness_serve_config(provider, &[fixture("bravo")], Limits::default());
|
|
serve.channel_client = Some(Arc::clone(&double.client));
|
|
serve.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry)));
|
|
let _adapter = SshAdapter::new(serve)
|
|
.await
|
|
.expect("the -R wired adapter constructs (the registration is adapter-scope)");
|
|
|
|
let params = alktunnels::params::TunnelForwardedParams {
|
|
listen: 424_242,
|
|
substrate: alktunnels::params::Substrate::Tcp,
|
|
peer: alktunnels::params::SubstrateAddr::HostPort {
|
|
host: "127.0.0.1".to_owned(),
|
|
port: 1234,
|
|
},
|
|
};
|
|
let (mut send, mut recv) = double
|
|
.open_forwarded_directly(params)
|
|
.await
|
|
.expect("the door-side openable allocated the arrival's channel");
|
|
send.write_all(b"orphan arrival")
|
|
.await
|
|
.expect("the adopted halves still write (the door dropped its end — EOF, not refusal)");
|
|
|
|
// The door-side handler dropped the fabric half at birth: the
|
|
// EOF arrives bounded, the channel never half-serves.
|
|
tokio::time::timeout(Duration::from_secs(10), recv.read_u8())
|
|
.await
|
|
.expect("teardown-at-birth is observable in time")
|
|
.expect_err("the adoptee's read half EOFs (the channel reapped)");
|
|
}
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
|
|
/// invariant's alkcall half on the `-R` leg): the door-side
|
|
/// registry's `AccessControl::check` gates arrivals by the fabric
|
|
/// caller's identity — an identity lacking the
|
|
/// `tunnel:forwarded` scope is refused by alkcall's dispatch (the
|
|
/// openable's own spec) and no SSH channel opens, even for a
|
|
/// genuinely minted token.
|
|
#[tokio::test]
|
|
async fn the_door_side_acl_refuses_an_unscoped_arrival() {
|
|
let double = wire_fabric_with_listen(true, false).await;
|
|
let (session, mut opens) = listen_session(&double, Limits::default()).await;
|
|
|
|
let port = pick_port().await;
|
|
let _bound = tcpip_forward(&session.client, "127.0.0.1", u32::from(port))
|
|
.await
|
|
.expect(
|
|
"the listen itself is gated by the fabric-side identity, which carries the scope",
|
|
);
|
|
|
|
// A real token (minted for this listener), unscoped arrival
|
|
// identity: the door-side ACL refuses the open op — the fabric
|
|
// side sees the refusal, never a channel allocation.
|
|
let token = double.minted_token().await;
|
|
let params = alktunnels::params::TunnelForwardedParams {
|
|
listen: token,
|
|
substrate: alktunnels::params::Substrate::Tcp,
|
|
peer: alktunnels::params::SubstrateAddr::HostPort {
|
|
host: "127.0.0.1".to_owned(),
|
|
port: 4321,
|
|
},
|
|
};
|
|
let outcome = double.open_forwarded_directly(params).await;
|
|
assert!(
|
|
outcome.is_err(),
|
|
"the unscoped arrival identity is refused at the door's dispatch"
|
|
);
|
|
|
|
// Nothing rode the acceptor.
|
|
tokio::time::timeout(Duration::from_millis(500), opens.recv())
|
|
.await
|
|
.expect_err("an ACL-refused arrival never opens an SSH channel");
|
|
assert!(opens.is_empty());
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 9 (the listener count
|
|
/// is a bounded resource): the per-session listener map is
|
|
/// bounded through its dedicated counter against the same
|
|
/// `max_concurrent_channels` ceiling — the listen beyond the
|
|
/// count answers the vanilla `RequestDenied` while channels stay
|
|
/// free, and the cancel releases the count again.
|
|
#[tokio::test]
|
|
async fn the_listener_cap_bounds_the_session_listen_map() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let limits = Limits {
|
|
max_concurrent_channels: 1,
|
|
..Limits::default()
|
|
};
|
|
let (session, mut opens) = listen_session(&double, limits).await;
|
|
|
|
let first = tcpip_forward(&session.client, "127.0.0.1", 0)
|
|
.await
|
|
.expect("the first listen is admitted");
|
|
assert_ne!(first, 0);
|
|
let err = tcpip_forward(&session.client, "127.0.0.1", 0)
|
|
.await
|
|
.expect_err("the second listen rides beyond the listener cap");
|
|
assert!(
|
|
matches!(err, Error::RequestDenied),
|
|
"expected RequestDenied, got {err:?}"
|
|
);
|
|
|
|
// The cap recovery: the cancel of the first listen frees the
|
|
// count; the next mint is admitted.
|
|
cancel_tcpip_forward(&session.client, "127.0.0.1", first)
|
|
.await
|
|
.expect("the cancel releases the count");
|
|
let second = tcpip_forward(&session.client, "127.0.0.1", 0)
|
|
.await
|
|
.expect("the listener count recovered at the cancel");
|
|
assert_ne!(second, 0);
|
|
|
|
// Nothing in this row ever manufactured an arrival.
|
|
tokio::time::timeout(Duration::from_millis(300), opens.recv())
|
|
.await
|
|
.expect_err("no arrivals were manufactured in this row");
|
|
assert!(opens.is_empty());
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 9 / the acceptor
|
|
/// vocabulary's cap rows (the wave-2 gate's booked shape): the
|
|
/// per-arrival admission is the owning session's in-crate
|
|
/// channel cap — an arrival onto an exhausted count is refused
|
|
/// at the accept surface (no SSH channel opens; the fabric half
|
|
/// drops, teardown-at-birth), and an established channel's close
|
|
/// recovers the count (the next arrival is admitted — the
|
|
/// release proof).
|
|
#[tokio::test]
|
|
async fn the_arrival_admission_is_cap_gated_at_the_accept_surface() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let limits = Limits {
|
|
max_concurrent_channels: 1,
|
|
..Limits::default()
|
|
};
|
|
let (session, mut opens) = listen_session(&double, limits).await;
|
|
|
|
let port = pick_port().await;
|
|
let _reply = tcpip_forward(&session.client, "127.0.0.1", u32::from(port))
|
|
.await
|
|
.expect("a minted listen costs no channel count");
|
|
|
|
// The first arrival admits itself: its SSH channel opens and
|
|
// the count is full (held by the arrival socket + channel,
|
|
// both deliberately kept alive un-served).
|
|
let first_arrival = tokio::net::TcpStream::connect(("127.0.0.1", port))
|
|
.await
|
|
.expect("the first arrival is admitted");
|
|
let first_open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the first arrival's open arrives")
|
|
.expect("observed");
|
|
let ForwardedOpen::Tcpip {
|
|
channel: first_channel,
|
|
..
|
|
} = first_open
|
|
else {
|
|
panic!("the tcp arrival opened forwarded-tcpip");
|
|
};
|
|
|
|
// The second arrival meets the full count: no SSH channel
|
|
// opens (its fabric half teardown-at-births), and its socket
|
|
// EOFs bounded.
|
|
let mut second_arrival = tokio::net::TcpStream::connect(("127.0.0.1", port))
|
|
.await
|
|
.expect("the fabric listener still accepts arrivals");
|
|
tokio::time::timeout(Duration::from_millis(700), opens.recv())
|
|
.await
|
|
.expect_err("an arrival beyond the cap never opens an SSH channel");
|
|
let eof = tokio::time::timeout(Duration::from_secs(10), second_arrival.read_u8())
|
|
.await
|
|
.expect("the refused arrival's teardown is observable in time")
|
|
.expect_err("the refused arrival's socket EOFs");
|
|
let _ = eof;
|
|
|
|
// The release proof: the first arrival's socket goes — the
|
|
// pump's fabric→SSH direction EOFs (the pump's
|
|
// shutdown-on-completion half-closes the SSH channel — the
|
|
// OpenSSH client, whose target socket died with the arrival,
|
|
// closes its half; the harness's stand-in does exactly that).
|
|
// That close — not the pump — is what fires the packet loop's
|
|
// established-channel release.
|
|
drop(first_arrival);
|
|
let _ = first_channel.close().await;
|
|
|
|
let mut third_arrival = tokio::net::TcpStream::connect(("127.0.0.1", port))
|
|
.await
|
|
.expect("the fabric listener still serves");
|
|
third_arrival.write_all(b"again").await.expect("rides");
|
|
let third_open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the next arrival's open arrives — the released count recovered")
|
|
.expect("observed");
|
|
let ForwardedOpen::Tcpip {
|
|
channel: mut third_channel,
|
|
..
|
|
} = third_open
|
|
else {
|
|
panic!("the tcp arrival opened forwarded-tcpip");
|
|
};
|
|
let mut seen: Vec<u8> = Vec::new();
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while seen.len() < 5 {
|
|
match third_channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("the forwarded channel ended before the bytes rode"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the pump rides in time");
|
|
assert_eq!(seen, b"again");
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* OQ-11 on the listen flow (the runtime-Unix
|
|
/// gate riding the `streamlocal_forward` serving flow) + the unix
|
|
/// row of the `-R` leg: the unix listen serves through the fabric
|
|
/// (the path echoes verbatim — no port semantics, no port
|
|
/// out-param), the arrival opens
|
|
/// `forwarded-streamlocal@openssh.com` toward the client, and the
|
|
/// cancel form tears it down 1:1 (path-matched).
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn streamlocal_forward_serves_arrivals_and_cancels_through_the_family() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let (session, mut opens) = listen_session(&double, Limits::default()).await;
|
|
|
|
let path = format!("/tmp/alkssh-w3-listen-{}-listen.sock", std::process::id());
|
|
super::common::harness::streamlocal_forward(&session.client, &path)
|
|
.await
|
|
.expect("the unix listen serves");
|
|
|
|
let mut arrival = tokio::net::UnixStream::connect(&path)
|
|
.await
|
|
.expect("the fabric's unix listener accepts the arrival");
|
|
arrival.write_all(b"unix-arrival").await.expect("rides");
|
|
|
|
let open = tokio::time::timeout(Duration::from_secs(15), opens.recv())
|
|
.await
|
|
.expect("the forwarded open arrives in time")
|
|
.expect("observed");
|
|
let ForwardedOpen::Streamlocal {
|
|
socket_path,
|
|
mut channel,
|
|
} = open
|
|
else {
|
|
panic!("the unix arrival opens forwarded-streamlocal");
|
|
};
|
|
assert_eq!(
|
|
socket_path, path,
|
|
"the server_socket_path echoes the listen request's path verbatim"
|
|
);
|
|
let mut seen: Vec<u8> = Vec::new();
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while seen.len() < 12 {
|
|
match channel.wait().await {
|
|
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
|
Some(_) => continue,
|
|
None => panic!("the forwarded channel ended before the arrival rode"),
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.expect("the unix pump carries the arrival");
|
|
assert_eq!(seen, b"unix-arrival");
|
|
channel
|
|
.data_bytes(b"unix-reply".to_vec())
|
|
.await
|
|
.expect("the SSH channel carries the reply");
|
|
let mut reply = vec![0u8; 10];
|
|
tokio::time::timeout(Duration::from_secs(15), arrival.read_exact(&mut reply))
|
|
.await
|
|
.expect("the reply reaches the arrival stream in time")
|
|
.expect("rides");
|
|
assert_eq!(reply, b"unix-reply");
|
|
|
|
super::common::harness::cancel_streamlocal_forward(&session.client, &path)
|
|
.await
|
|
.expect("the pending unix listen's cancel answers success");
|
|
let denied = super::common::harness::cancel_streamlocal_forward(&session.client, &path)
|
|
.await
|
|
.expect_err("a second cancel of the same path is the not-found denial");
|
|
assert!(matches!(denied, Error::RequestDenied), "got {denied:?}");
|
|
|
|
session.unmount().await;
|
|
let _ = std::fs::remove_file(&path);
|
|
}
|
|
|
|
/// - *Contract stamp:* the unset-declines contract on the listen
|
|
/// seam (`w3-tunnel-listen`'s whole-`Option` shape — an
|
|
/// egress-only door is legitimate config): a `tunnel` build with
|
|
/// a fabric leg but no `tunnel_listen_seam` declines every listen
|
|
/// form with the vanilla `RequestDenied`, while the leg-1 rows
|
|
/// still route (the egress bridge is not dead).
|
|
#[tokio::test]
|
|
async fn listen_forms_decline_when_the_listen_seam_is_unset() {
|
|
let targets: TargetMap = Arc::default();
|
|
let client = wire_fabric(
|
|
inproc_dial(targets, Arc::new(AtomicUsize::new(0))),
|
|
scoped_caller(),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve = harness_serve_config(provider, &[fixture("bravo")], Limits::default());
|
|
serve.channel_client = Some(client);
|
|
let adapter = SshAdapter::new(serve)
|
|
.await
|
|
.expect("the egress-only door constructs without the seam");
|
|
|
|
let policy = Arc::new(MemHostKeyPolicy::with_default(true));
|
|
let mut session =
|
|
super::common::harness::drive(adapter, Arc::clone(&policy), Limits::default())
|
|
.await
|
|
.expect("the loopback mounts");
|
|
let result =
|
|
authenticate_publickey(&mut session.client, "tunnel-rider", fixture("alpha")).await;
|
|
assert!(result.success());
|
|
|
|
let err = tcpip_forward(&session.client, "127.0.0.1", 8080)
|
|
.await
|
|
.expect_err("the unset seam declines the listen");
|
|
assert!(matches!(err, Error::RequestDenied), "got {err:?}");
|
|
let err = cancel_tcpip_forward(&session.client, "127.0.0.1", 8080)
|
|
.await
|
|
.expect_err("the unset seam declines the cancel too");
|
|
assert!(matches!(err, Error::RequestDenied), "got {err:?}");
|
|
|
|
// The egress door still routes: the direct row reaches the
|
|
// fabric (the fail-dial class — `ConnectFailed`, not
|
|
// `AdministrativelyProhibited`).
|
|
let err = open_direct_tcpip(&session.client, "127.0.0.1", 36005)
|
|
.await
|
|
.expect_err("the direct row still routes onto the bridge");
|
|
open_failure(
|
|
err,
|
|
ChannelOpenFailure::ConnectFailed,
|
|
"the egress-only door's direct row (the seam-unconfigured row's control leg)",
|
|
);
|
|
|
|
session.unmount().await;
|
|
}
|
|
|
|
/// - *Contract stamp:* the door-side registration is
|
|
/// one-per-op-per-registry (mapping.md §Global-request rows / the
|
|
/// `install_forwarded_surface` claim): alkcall's `register`
|
|
/// silently replaces an existing op entry, so the door
|
|
/// pre-checks — a second `SshAdapter` composed over the same
|
|
/// shared seam registry is a **failed construction**
|
|
/// (`AdapterError::ListenRegistration`), never a silent steal of
|
|
/// the first adapter's arrival routes.
|
|
#[tokio::test]
|
|
async fn a_second_adapter_over_a_shared_listen_registry_fails_construction() {
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
|
|
let mut first = harness_serve_config(
|
|
Arc::clone(&provider) as Arc<dyn alkcall::core::auth::IdentityProvider>,
|
|
&[fixture("bravo")],
|
|
Limits::default(),
|
|
);
|
|
first.channel_client = Some(Arc::clone(&double.client));
|
|
first.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry)));
|
|
let _first = SshAdapter::new(first)
|
|
.await
|
|
.expect("the first adapter over the shared registry constructs");
|
|
|
|
let mut second = harness_serve_config(provider, &[fixture("charlie")], Limits::default());
|
|
second.channel_client = Some(Arc::clone(&double.client));
|
|
second.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry)));
|
|
let refused = SshAdapter::new(second)
|
|
.await
|
|
.expect_err("a conflicting registration is a failed construction");
|
|
assert!(
|
|
matches!(refused, alkssh::AdapterError::ListenRegistration(_)),
|
|
"the refusal is the ListenRegistration class, got {refused:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "tunnel", unix))]
|
|
mod real_tool {
|
|
//! The real-tool smoke (`w3-tunnel-direct`): one `ssh -L` (+ curl)
|
|
//! through a TCP-mounted adapter against the fabric double — the
|
|
//! POC-3 §C.1 gate re-landed. Tooling posture (recorded, like the
|
|
//! git task's): the smoke requires `ssh`/`curl` in the environment;
|
|
//! when absent it is **skipped with a printed reason**
|
|
//! (skipped-with-reason at most, never silently green).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use alktunnels::params::{Substrate, SubstrateAddr, TUNNEL_DIRECT_SCOPE};
|
|
use alktunnels::producer::{BindFn, BoundListener, DialFn, TargetHandle, TunnelEstablishError};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
use super::common::harness::serve_config as harness_serve_config;
|
|
use super::common::{MemIdentityProvider, fixture, identity};
|
|
use super::serving::{wire_fabric, wire_fabric_with_listen};
|
|
use alkssh::TunnelListenSeam;
|
|
|
|
/// The smoke's dial seam: a real-TCP dial closure (hand-rolled —
|
|
/// the assembly's `DialFn` injected over the substrate for real;
|
|
/// alktunnels' `local` feature stays off).
|
|
fn tcp_dial() -> DialFn {
|
|
Arc::new(move |_substrate: Substrate, target: &str| {
|
|
let target = target.to_owned();
|
|
Box::pin(async move {
|
|
let stream = tokio::net::TcpStream::connect(&target).await.map_err(|e| {
|
|
TunnelEstablishError::DialFailed(format!("dial `{target}`: {e}"))
|
|
})?;
|
|
let (read, write) = stream.into_split();
|
|
Ok(TargetHandle {
|
|
read: Box::new(read),
|
|
write: Box::new(write),
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
/// A tiny HTTP/1.1 endpoint (the POC's shape) — the substrate the
|
|
/// forwarded traffic dials through the fabric.
|
|
async fn spawn_http_endpoint() -> (std::net::SocketAddr, String) {
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("the smoke listener binds");
|
|
let addr = listener.local_addr().expect("local addr");
|
|
let body = "alkssh w3-tunnel-direct through the fabric\n".to_owned();
|
|
let body2 = body.clone();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
let Ok((mut conn, _)) = listener.accept().await else {
|
|
return;
|
|
};
|
|
let body = body2.clone();
|
|
tokio::spawn(async move {
|
|
let mut buf = vec![0u8; 8192];
|
|
let _ = conn.read(&mut buf).await;
|
|
let head = format!(
|
|
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
body.len(),
|
|
body
|
|
);
|
|
let _ = conn.write_all(head.as_bytes()).await;
|
|
let _ = conn.shutdown().await;
|
|
});
|
|
}
|
|
});
|
|
(addr, body)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn real_ssh_dash_l_smoke() {
|
|
if std::process::Command::new("ssh")
|
|
.arg("-V")
|
|
.output()
|
|
.is_err()
|
|
{
|
|
eprintln!("SKIPPED (with reason): real-tool tunnel smoke — ssh is unavailable");
|
|
return;
|
|
}
|
|
if std::process::Command::new("curl")
|
|
.arg("--version")
|
|
.output()
|
|
.is_err()
|
|
{
|
|
eprintln!("SKIPPED (with reason): real-tool tunnel smoke — curl is unavailable");
|
|
return;
|
|
}
|
|
|
|
let (http_addr, body) = spawn_http_endpoint().await;
|
|
let client = wire_fabric(
|
|
tcp_dial(),
|
|
identity("tunnel-caller", &[TUNNEL_DIRECT_SCOPE]),
|
|
Arc::new(tokio::sync::Mutex::new(None)),
|
|
)
|
|
.await;
|
|
|
|
let work = std::env::temp_dir().join(format!("alkssh-tunnel-smoke-{}", std::process::id()));
|
|
std::fs::create_dir_all(&work).expect("the smoke workdir creates");
|
|
let key_path = work.join("id_ed25519");
|
|
std::fs::write(&key_path, fixture("alpha").pem).expect("the fixture key writes");
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
|
|
.expect("the key file perms set");
|
|
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve =
|
|
harness_serve_config(provider, &[fixture("bravo")], alkssh::Limits::default());
|
|
serve.channel_client = Some(client);
|
|
let adapter = Arc::new(
|
|
alkssh::SshAdapter::new(serve)
|
|
.await
|
|
.expect("the smoke adapter constructs"),
|
|
);
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("the smoke listener binds");
|
|
let ssh_port = listener.local_addr().expect("local addr").port();
|
|
let accept_task = tokio::spawn(async move {
|
|
loop {
|
|
let Ok((stream, _)) = listener.accept().await else {
|
|
return;
|
|
};
|
|
let adapter = Arc::clone(&adapter);
|
|
tokio::spawn(async move {
|
|
let _ = adapter.handle(stream).await;
|
|
});
|
|
}
|
|
});
|
|
|
|
let fwd_port: u16 = pick_local_port().await;
|
|
let mut ssh_child = tokio::process::Command::new("ssh")
|
|
.args([
|
|
"-N",
|
|
"-i",
|
|
key_path.to_str().expect("tmp path"),
|
|
"-p",
|
|
&ssh_port.to_string(),
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"IdentitiesOnly=yes",
|
|
"-o",
|
|
"StrictHostKeyChecking=no",
|
|
"-o",
|
|
"UserKnownHostsFile=/dev/null",
|
|
"-o",
|
|
"ConnectTimeout=10",
|
|
"-o",
|
|
"ExitOnForwardFailure=yes",
|
|
"-L",
|
|
&format!(
|
|
"127.0.0.1:{fwd_port}:{}:{}",
|
|
http_addr.ip(),
|
|
http_addr.port()
|
|
),
|
|
])
|
|
.arg("tunnel-rider@127.0.0.1")
|
|
.stdout(std::process::Stdio::null())
|
|
.stderr(std::process::Stdio::null())
|
|
.spawn()
|
|
.expect("vanilla ssh spawns");
|
|
|
|
// The -L listener binds client-side; give it a bounded settle
|
|
// window then curl through it.
|
|
let through = tokio::time::timeout(std::time::Duration::from_secs(30), async {
|
|
let mut attempts = 0u32;
|
|
loop {
|
|
let out = tokio::process::Command::new("curl")
|
|
.args(["-s", "--max-time", "10"])
|
|
.arg(format!("http://127.0.0.1:{fwd_port}/"))
|
|
.output()
|
|
.await
|
|
.expect("curl spawns");
|
|
if out.status.success() {
|
|
return out;
|
|
}
|
|
attempts += 1;
|
|
assert!(
|
|
attempts < 20,
|
|
"curl never got through: exit {:?}",
|
|
out.status.code()
|
|
);
|
|
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("the smoke resolves in time");
|
|
|
|
let _ = ssh_child.kill().await;
|
|
accept_task.abort();
|
|
|
|
let stdout = String::from_utf8_lossy(&through.stdout).to_string();
|
|
assert!(
|
|
through.status.success() && stdout.trim_end() == body.trim_end(),
|
|
"vanilla ssh -L + curl through the door → fabric → endpoint returns the body — got {stdout:?}"
|
|
);
|
|
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
}
|
|
|
|
/// The smoke's bind seam: the leg-2 dynamic-address producer's
|
|
/// real-TCP bind (the wire-named endpoint bound for real; shared
|
|
/// shape with the harness rows' `real_bind`).
|
|
fn tcp_bind() -> BindFn {
|
|
Arc::new(move |_substrate, endpoint: &str| {
|
|
let endpoint = endpoint.to_owned();
|
|
let endpoint = endpoint.replace("localhost:", "127.0.0.1:");
|
|
Box::pin(async move {
|
|
let listener = tokio::net::TcpListener::bind(&endpoint)
|
|
.await
|
|
.map_err(|e| {
|
|
TunnelEstablishError::ResourceShortage(format!("bind `{endpoint}`: {e}"))
|
|
})?;
|
|
let addr = listener.local_addr().expect("bound addr");
|
|
Ok(BoundListener {
|
|
listener: Arc::new(listener),
|
|
bound: SubstrateAddr::HostPort {
|
|
host: addr.ip().to_string(),
|
|
port: addr.port(),
|
|
},
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn real_ssh_dash_r_smoke() {
|
|
if std::process::Command::new("ssh")
|
|
.arg("-V")
|
|
.output()
|
|
.is_err()
|
|
{
|
|
eprintln!("SKIPPED (with reason): real-tool -R smoke — ssh is unavailable");
|
|
return;
|
|
}
|
|
if std::process::Command::new("curl")
|
|
.arg("--version")
|
|
.output()
|
|
.is_err()
|
|
{
|
|
eprintln!("SKIPPED (with reason): real-tool -R smoke — curl is unavailable");
|
|
return;
|
|
}
|
|
|
|
// The vanilla client's own dial target (the `-R` convention:
|
|
// the client dials its own endpoint per arrival, from the
|
|
// machine the SSH client runs on).
|
|
let (http_addr, body) = spawn_http_endpoint().await;
|
|
let double = wire_fabric_with_listen(true, true).await;
|
|
|
|
let provider = Arc::new(MemIdentityProvider::new());
|
|
provider.register(
|
|
fixture("alpha").fingerprint,
|
|
identity("tunnel-rider", &["ssh"]),
|
|
);
|
|
let mut serve =
|
|
harness_serve_config(provider, &[fixture("bravo")], alkssh::Limits::default());
|
|
serve.channel_client = Some(Arc::clone(&double.client));
|
|
serve.tunnel_listen_seam = Some(TunnelListenSeam::new(Arc::clone(&double.registry)));
|
|
let adapter = Arc::new(
|
|
alkssh::SshAdapter::new(serve)
|
|
.await
|
|
.expect("the smoke adapter constructs"),
|
|
);
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("the smoke listener binds");
|
|
let ssh_port = listener.local_addr().expect("local addr").port();
|
|
let accept_task = tokio::spawn(async move {
|
|
loop {
|
|
let Ok((stream, _)) = listener.accept().await else {
|
|
return;
|
|
};
|
|
let adapter = Arc::clone(&adapter);
|
|
tokio::spawn(async move {
|
|
let _ = adapter.handle(stream).await;
|
|
});
|
|
}
|
|
});
|
|
|
|
let fwd_port: u16 = pick_local_port().await;
|
|
let mut ssh_child = tokio::process::Command::new("ssh")
|
|
.args([
|
|
"-N",
|
|
"-i",
|
|
work_key_path().to_str().expect("tmp path"),
|
|
"-p",
|
|
&ssh_port.to_string(),
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"IdentitiesOnly=yes",
|
|
"-o",
|
|
"StrictHostKeyChecking=no",
|
|
"-o",
|
|
"UserKnownHostsFile=/dev/null",
|
|
"-o",
|
|
"ConnectTimeout=10",
|
|
"-o",
|
|
"ExitOnForwardFailure=yes",
|
|
"-R",
|
|
&format!(
|
|
"127.0.0.1:{fwd_port}:{}:{}",
|
|
http_addr.ip(),
|
|
http_addr.port()
|
|
),
|
|
])
|
|
.arg("tunnel-rider@127.0.0.1")
|
|
.stdout(std::process::Stdio::null())
|
|
.stderr(std::process::Stdio::null())
|
|
.spawn()
|
|
.expect("vanilla ssh spawns");
|
|
|
|
// The fabric bound the -R listener; curl arrives on it, the
|
|
// door maps the arrival to a forwarded-tcpip channel the
|
|
// vanilla client accepts and dials its own endpoint through.
|
|
let through = tokio::time::timeout(std::time::Duration::from_secs(30), async {
|
|
let mut attempts = 0u32;
|
|
loop {
|
|
let out = tokio::process::Command::new("curl")
|
|
.args(["-s", "--max-time", "10"])
|
|
.arg(format!("http://127.0.0.1:{fwd_port}/"))
|
|
.output()
|
|
.await
|
|
.expect("curl spawns");
|
|
if out.status.success() {
|
|
return out;
|
|
}
|
|
attempts += 1;
|
|
assert!(
|
|
attempts < 20,
|
|
"curl never got through: exit {:?}",
|
|
out.status.code()
|
|
);
|
|
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("the -R smoke resolves in time");
|
|
|
|
let _ = ssh_child.kill().await;
|
|
accept_task.abort();
|
|
|
|
let stdout = String::from_utf8_lossy(&through.stdout).to_string();
|
|
assert!(
|
|
through.status.success() && stdout.trim_end() == body.trim_end(),
|
|
"vanilla ssh -R + curl through the fabric → arrival → forwarded-tcpip → client-dialed endpoint returns the body — got {stdout:?}"
|
|
);
|
|
}
|
|
|
|
/// The smoke's per-run workdir key path (the -L smoke's inline
|
|
/// shape extracted).
|
|
fn work_key_path() -> std::path::PathBuf {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let work =
|
|
std::env::temp_dir().join(format!("alkssh-tunnel-r-smoke-{}", std::process::id()));
|
|
std::fs::create_dir_all(&work).expect("the smoke workdir creates");
|
|
let key_path = work.join("id_ed25519");
|
|
std::fs::write(&key_path, fixture("alpha").pem).expect("the fixture key writes");
|
|
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
|
|
.expect("the key file perms set");
|
|
key_path
|
|
}
|
|
|
|
async fn pick_local_port() -> u16 {
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("the probe binds");
|
|
let port = listener.local_addr().expect("addr").port();
|
|
drop(listener);
|
|
port
|
|
}
|
|
}
|