feat(tunnel): adopt the forwarded-op establisher gate (alktunnels 0.5.0, review-003 W2)
- install_forwarded_surface registers register_tunnel_forwarded_openable_with_establisher (ADR-008 Amendment 2's recommended shape) with a ForwardedGate capturing the ListenRouter: a stale/missing listen token fails the open typed at the call plane (channel:open_failed, unknown_resource) BEFORE any channel is born — the phantom-channel shape is closed at the open; malformed arrivals are likewise typed (handler_error) instead of success-then-silent-birth-teardown. - the arrival pump's handler-side correlation re-lookup stays as the residual-race + cap-admission teardown-at-birth fallback (the base shape's posture, retained; the narrow gate-pass->handler-run race documented on the pump). - the unscoped-arrival ACL row double-pins the pinned gate order (ACL before establisher — a genuinely minted token refused by ACL alone). - rows: a_correlation_miss_fails_the_open_typed_before_any_channel (miss: typed refusal, no phantom channel, no acceptor row), a_gate_pass_arrival_serves_the_route_unchanged (the serving flow unchanged), and the lib unit pin the_pump_residual_miss_teardown_at_births_the_fabric_half. - the harness opener now maps the wire refusal's details.reason back into the TunnelEstablishError taxonomy (the real opener's map) — the flat HandlerError swallow would erase exactly what the gate rows pin. Verification: cargo test --all-features --test tunnel_mapping (22, incl. both real-tool smokes), cargo test --features tunnel --lib tunnel_map, full cargo test (default + --all-features), clippy x2 -D warnings, fmt.
This commit is contained in:
1 parent
36d08868c9
commit
4668061b19
2 files changed
+259
-43
No files matched your search
+111
-8
@@ -1,7 +1,9 @@
|
||||
//! The `direct-tcpip` / `direct-streamlocal` mapping — the tunnel
|
||||
//! branch's route-1 bridge (`w3-tunnel-direct`, ADR-006 decision 1):
|
||||
//! the adapter-side bridge POC-3 stage C proved, re-landed as contract
|
||||
//! against published alktunnels 0.4.1.
|
||||
//! against published alktunnels 0.4.1, with the forwarded serving
|
||||
//! surface adopted to alktunnels 0.5.0's establisher shape (the
|
||||
//! review-003 W2 adoption — the `ForwardedGate` correlation gate).
|
||||
//!
|
||||
//! ## The route-1 bridge (ADR-006 decision 1)
|
||||
//!
|
||||
@@ -39,7 +41,8 @@
|
||||
//! — **the fabric listens**; per arrival the data flows back toward
|
||||
//! the vanilla SSH client, which dials its own target. Three moving
|
||||
//! parts, all decided against alktunnels 0.4.1's actual surfaces with
|
||||
//! POC-3 §C.4 as the template:
|
||||
//! POC-3 §C.4 as the template (the gate adoption rides 0.5.0's
|
||||
//! establisher variant, above):
|
||||
//!
|
||||
//! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`):
|
||||
//! the door maps the vanilla listen request family-ward as
|
||||
@@ -82,12 +85,21 @@
|
||||
//! client's manager + alktunnels' `tunnel_forwarded_spec()` +
|
||||
//! `make_forwarded_op_handler` over the crate's arrival pump, the
|
||||
//! wire-level ACL (`tunnel:forwarded` scope) and the per-connection
|
||||
//! channel budget enforced inside alkcall's dispatch.
|
||||
//! channel budget enforced inside alkcall's dispatch — registered
|
||||
//! in the **establisher variant** (alktunnels 0.5.0's
|
||||
//! `register_tunnel_forwarded_openable_with_establisher`, the
|
||||
//! review-003 W2 adoption): the router's correlation lookup rides
|
||||
//! the establisher's `ForwardedGate`, so a stale/missing token
|
||||
//! fails the open typed at the call plane (`channel:open_failed`,
|
||||
//! `unknown_resource`) before any channel is born; the handler's
|
||||
//! re-lookup keeps the residual-race +
|
||||
//! cap-admission teardown-at-birth fallback.
|
||||
//! 3. **The arrival pump** — the `forwarded-tcpip` data leg: the
|
||||
//! registered [`ForwardedHandler`]-closure correlates the
|
||||
//! arrival's `listen` token through [`ListenRouter`] (the route was
|
||||
//! registered at listen acceptance with the owning session's
|
||||
//! handle + cap); a miss is **teardown-at-birth** (drop the fabric
|
||||
//! handle + cap); a miss here is the gate-pass residual race —
|
||||
//! still **teardown-at-birth** (drop the fabric
|
||||
//! half, never a half-open SSH channel). On a hit, per vanilla `-R`
|
||||
//! convention: the door opens a `forwarded-tcpip` channel *toward
|
||||
//! the client* echoing **the listen request's own args** — the
|
||||
@@ -216,7 +228,10 @@ use alkcall::core::auth::AuthContext;
|
||||
use alkcall::core::types::BiStream;
|
||||
use alktunnels::consumer::{TunnelListener, TunnelSession};
|
||||
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams, TunnelParams};
|
||||
use alktunnels::producer::{ForwardedHandler, register_tunnel_forwarded_openable};
|
||||
use alktunnels::producer::{
|
||||
ForwardedGate, ForwardedHandler, TunnelEstablishError,
|
||||
register_tunnel_forwarded_openable_with_establisher,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use russh::Channel;
|
||||
use russh::server::{ChannelOpenHandle, Handle};
|
||||
@@ -431,7 +446,25 @@ impl Default for ListenRouter {
|
||||
/// deployment policy; an assembly wanting a different per-identity
|
||||
/// cap shapes it through its own `ServingConfig`/`registry`
|
||||
/// composition upstream) — and spawns alktunnels' forward-op handler
|
||||
/// over the crate's arrival pump. The registration is
|
||||
/// over the crate's arrival pump.
|
||||
///
|
||||
/// The registration shape is the **establisher variant**
|
||||
/// (`register_tunnel_forwarded_openable_with_establisher` — alktunnels
|
||||
/// 0.5.0, review 003 W2's adoption, ADR-008 Amendment 2's recommended
|
||||
/// form): the router's correlation lookup rides a [`ForwardedGate`]
|
||||
/// capturing the router, so a stale/missing `listen` token fails the
|
||||
/// open at alkcall's call plane as a typed `channel:open_failed`
|
||||
/// (`details.reason == "unknown_resource"`) **before any channel is
|
||||
/// born** — the phantom-channel class (call-plane success, then
|
||||
/// handler-side decline) is closed at the open; door-side arrival
|
||||
/// correlations that pass the gate but lose the narrow gate-pass→
|
||||
/// handler-run race, plus every cap-admission decline, still take the
|
||||
/// handler's teardown-at-birth path (the residual-race posture the
|
||||
/// base registration also carries). A malformed arrival is likewise
|
||||
/// typed (`handler_error`) instead of the base shape's
|
||||
/// success-then-silent-birth-teardown.
|
||||
///
|
||||
/// The registration is
|
||||
/// one-per-op-id-per-registry: alkcall's `register` silently
|
||||
/// replaces an existing entry, so the door **pre-checks the op** —
|
||||
/// a `tunnel/forwarded` registration that already exists (e.g. a
|
||||
@@ -450,6 +483,21 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
||||
return Err("tunnel listen seam configured without a fabric leg".to_string());
|
||||
};
|
||||
let router = Arc::new(ListenRouter::default());
|
||||
let gate: ForwardedGate = {
|
||||
let router = Arc::clone(&router);
|
||||
Arc::new(move |params: &TunnelForwardedParams| {
|
||||
if router.route_for(params.listen).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TunnelEstablishError::UnknownResource(format!(
|
||||
"no accepted listen routes the arrival's token {} \
|
||||
— the forwarding session is gone or was never minted \
|
||||
(alkssh: stale/missing `-R` token)",
|
||||
params.listen
|
||||
)))
|
||||
}
|
||||
})
|
||||
};
|
||||
let handler: ForwardedHandler = {
|
||||
let router = Arc::clone(&router);
|
||||
Arc::new(move |params: TunnelForwardedParams, bidi: BiStream| {
|
||||
@@ -472,10 +520,11 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
||||
alktunnels::params::OP_TUNNEL_FORWARDED
|
||||
));
|
||||
}
|
||||
register_tunnel_forwarded_openable(
|
||||
register_tunnel_forwarded_openable_with_establisher(
|
||||
&core,
|
||||
&seam.registry,
|
||||
AuthContext::anonymous(b"alk/tunnel"),
|
||||
gate,
|
||||
handler,
|
||||
)
|
||||
.map_err(|failure| format!("tunnel/forwarded openable registration: {failure}"))?;
|
||||
@@ -491,7 +540,12 @@ pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Resu
|
||||
///
|
||||
/// 1. **Correlation** — the route for the arrival's minted token;
|
||||
/// a miss is teardown-at-birth (drop the fabric half; the alkcall
|
||||
/// wrapper's awaited join ends, the channel reaps).
|
||||
/// wrapper's awaited join ends, the channel reaps). The gate
|
||||
/// (the establisher's call-plane correlation) misses a stale
|
||||
/// token before any channel is born; this handler-side re-lookup
|
||||
/// is the narrow gate-pass→handler-run **residual race** — the
|
||||
/// same miss class, past the open — and the never-established cap
|
||||
/// declines below take the same path.
|
||||
/// 2. **The in-crate cap admission** (bounded resources at the accept
|
||||
/// surface — the wave-2 gate's booked acceptor vocabulary): no
|
||||
/// room → the fabric half drops, the arrival never opens an SSH
|
||||
@@ -686,3 +740,52 @@ pub(crate) struct PendingListen {
|
||||
/// the session's count dies with the handler, per the channels
|
||||
/// cap's same posture).
|
||||
pub(crate) type ListenerCap = ChannelCap;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams};
|
||||
|
||||
use super::{ListenRouter, pump_forwarded_arrival};
|
||||
|
||||
/// The arrival pump's handler-side miss (the narrow gate-pass→
|
||||
/// handler-run residual race the establisher shape retains) still
|
||||
/// teardown-at-births: a token with no route at handler time drops
|
||||
/// the fabric half — the adoptee reads EOF, never a half-served
|
||||
/// arrival.
|
||||
#[tokio::test]
|
||||
async fn the_pump_residual_miss_teardown_at_births_the_fabric_half() {
|
||||
let router = ListenRouter::default();
|
||||
let (door, fabric_half) = tokio::io::duplex(64 * 1024);
|
||||
let (reader, writer) = tokio::io::split(door);
|
||||
|
||||
let bidi = alkcall::core::types::BiStream::from_joined(reader, writer);
|
||||
|
||||
let pump = pump_forwarded_arrival(
|
||||
std::sync::Arc::new(router),
|
||||
TunnelForwardedParams {
|
||||
listen: 424_242,
|
||||
substrate: Substrate::Tcp,
|
||||
peer: SubstrateAddr::HostPort {
|
||||
host: "127.0.0.1".to_owned(),
|
||||
port: 1234,
|
||||
},
|
||||
},
|
||||
bidi,
|
||||
);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), pump)
|
||||
.await
|
||||
.expect("the miss returns in time (the pump drops the half and ends)");
|
||||
|
||||
let mut adoptee = fabric_half;
|
||||
let read = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
tokio::io::AsyncReadExt::read_u8(&mut adoptee),
|
||||
)
|
||||
.await
|
||||
.expect("the adoptee's EOF is observable in time");
|
||||
assert!(
|
||||
read.is_err(),
|
||||
"the adopted fabric half EOFs (teardown-at-birth, not a half-serve)"
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
-35
@@ -1,9 +1,11 @@
|
||||
//! 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
|
||||
//! 0.5.0 seam (alkssh 0.2.0's W2 adoption: `TunnelSession::open_direct`
|
||||
//! → accept only after the fabric open succeeds → `take_halves()`
|
||||
//! pumped against the taken channel; the forwarded serving surface on
|
||||
//! the establisher shape — the `ForwardedGate` correlation gate),
|
||||
//! 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
|
||||
@@ -204,11 +206,11 @@ mod serving {
|
||||
|
||||
/// 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
|
||||
/// allocated the channel — the gate-pass/residual-race
|
||||
/// 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).
|
||||
/// unknown-token gate miss / 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 {
|
||||
@@ -495,8 +497,47 @@ mod serving {
|
||||
let response = call
|
||||
.call(alktunnels::params::OP_TUNNEL_FORWARDED, payload)
|
||||
.await;
|
||||
// The real opener maps the wire refusal's
|
||||
// reason through the `From<TunnelEstablish
|
||||
// Error> for EstablishmentError` table — the
|
||||
// typed `unknown_resource` (the gate's miss
|
||||
// class) must survive the client side here,
|
||||
// so the double maps the details.reason back
|
||||
// into the establisher's taxonomy (the flat
|
||||
// `HandlerError` swallow would erase exactly
|
||||
// what the gate rows pin).
|
||||
let out = response.result.map_err(|e| {
|
||||
TunnelEstablishError::HandlerError(format!("forwarded open: {e:?}"))
|
||||
if e.code == "channel:open_failed" {
|
||||
match e
|
||||
.details
|
||||
.as_ref()
|
||||
.and_then(|d| d.get("reason"))
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"unknown_resource" => {
|
||||
TunnelEstablishError::UnknownResource(e.message.clone())
|
||||
}
|
||||
"dial_failed" => {
|
||||
TunnelEstablishError::DialFailed(e.message.clone())
|
||||
}
|
||||
"resource_shortage" => {
|
||||
TunnelEstablishError::ResourceShortage(
|
||||
e.message.clone(),
|
||||
)
|
||||
}
|
||||
"handler_error" => {
|
||||
TunnelEstablishError::HandlerError(e.message.clone())
|
||||
}
|
||||
_ => TunnelEstablishError::HandlerError(format!(
|
||||
"forwarded open: {e:?}"
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
TunnelEstablishError::HandlerError(format!(
|
||||
"forwarded open: {e:?}"
|
||||
))
|
||||
}
|
||||
})?;
|
||||
let id =
|
||||
out.get("channel_id")
|
||||
@@ -1486,26 +1527,19 @@ mod serving {
|
||||
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.
|
||||
/// - *Contract stamp:* the forwarded gate's miss class (alktunnels
|
||||
/// 0.5.0, review 003 W2 adoption) at the alkssh level: an arrival
|
||||
/// whose `listen` token has no route (nothing was ever minted for
|
||||
/// this door) fails the open at the call plane as a typed
|
||||
/// `channel:open_failed` (`TunnelEstablishError::UnknownResource`
|
||||
/// — the wire's `unknown_resource` reason) **before any channel
|
||||
/// is born** — the phantom-channel shape (call-plane success,
|
||||
/// then handler-side decline) is closed at the open: no channel
|
||||
/// allocation, no adopted halves, no acceptor row.
|
||||
#[tokio::test]
|
||||
async fn a_correlation_miss_adopts_no_ssh_channel() {
|
||||
async fn a_correlation_miss_fails_the_open_typed_before_any_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 (session, mut opens) = listen_session(&double, Limits::default()).await;
|
||||
|
||||
let params = alktunnels::params::TunnelForwardedParams {
|
||||
listen: 424_242,
|
||||
@@ -1515,20 +1549,95 @@ mod serving {
|
||||
port: 1234,
|
||||
},
|
||||
};
|
||||
let (mut send, mut recv) = double
|
||||
let outcome = double.open_forwarded_directly(params).await;
|
||||
let err = match outcome {
|
||||
Ok(_) => {
|
||||
panic!("the gate miss fails the open typed — no phantom channel exists to adopt")
|
||||
}
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(
|
||||
matches!(err, TunnelEstablishError::UnknownResource(_)),
|
||||
"expected UnknownResource at the call plane, got {err:?}"
|
||||
);
|
||||
|
||||
// No arrival rode the acceptor — the open died at the call
|
||||
// plane; the handler never ran.
|
||||
tokio::time::timeout(Duration::from_millis(500), opens.recv())
|
||||
.await
|
||||
.expect_err("a gate-missed arrival never opens an SSH channel");
|
||||
assert!(opens.is_empty());
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* the forwarded gate's pass posture (alktunnels
|
||||
/// 0.5.0, review 003 W2 adoption): an arrival whose token is
|
||||
/// genuinely routed behaves exactly as the base shape did — the
|
||||
/// open succeeds at the call plane, the door allocates the SSH
|
||||
/// channel at the acceptor, and the fabric-side halves are the
|
||||
/// arrival's data plane (the gate adds an establishment phase;
|
||||
/// the serving flow is unchanged).
|
||||
#[tokio::test]
|
||||
async fn a_gate_pass_arrival_serves_the_route_unchanged() {
|
||||
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 = tcpip_forward(&session.client, "127.0.0.1", u32::from(port))
|
||||
.await
|
||||
.expect("the listen is minted");
|
||||
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: 1234,
|
||||
},
|
||||
};
|
||||
let (mut send, _recv) = double
|
||||
.open_forwarded_directly(params)
|
||||
.await
|
||||
.expect("the door-side openable allocated the arrival's channel");
|
||||
.expect("the gate-passed arrival adopts its fabric-side halves");
|
||||
|
||||
// The serving flow is the unchanged shape: the arrival becomes
|
||||
// an SSH `forwarded-tcpip` open at the acceptor, and data rides
|
||||
// the fabric half through to it.
|
||||
let open = tokio::time::timeout(Duration::from_secs(10), opens.recv())
|
||||
.await
|
||||
.expect("the gate-passed arrival observed in time")
|
||||
.expect("the acceptor's stream stays alive");
|
||||
let mut channel = match open {
|
||||
ForwardedOpen::Tcpip { channel, .. } => channel,
|
||||
other => panic!("the gate-passed arrival opened something else: {other:?}"),
|
||||
};
|
||||
|
||||
send.write_all(b"orphan arrival")
|
||||
.await
|
||||
.expect("the adopted halves still write (the door dropped its end — EOF, not refusal)");
|
||||
.expect("the adopted halves still write (the serving flow ran)");
|
||||
drop(send);
|
||||
|
||||
// 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)");
|
||||
let mut seen: Vec<u8> = Vec::new();
|
||||
tokio::time::timeout(Duration::from_secs(15), async {
|
||||
while seen.len() < 13 {
|
||||
match channel.wait().await {
|
||||
Some(ChannelMsg::Data { data }) => seen.extend_from_slice(&data),
|
||||
Some(_) => continue,
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("the fabric half rides in time");
|
||||
assert_eq!(
|
||||
seen,
|
||||
b"orphan arrival".to_vec(),
|
||||
"the gate-passed arrival's data rode the unchanged serving flow"
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* AGENTS.md convention 7 (the visible-surface
|
||||
@@ -1537,7 +1646,11 @@ mod serving {
|
||||
/// 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.
|
||||
/// genuinely minted token. The genuinely-minted-token shape
|
||||
/// double-pins the gate ordering now (the establisher variant's
|
||||
/// pinned order, alktunnels 0.5.0 / ADR-008 Amendment 2): the ACL
|
||||
/// refuses before the establisher's gate ever looks up the token —
|
||||
/// an unscoped arrival could not leak a gate pass.
|
||||
#[tokio::test]
|
||||
async fn the_door_side_acl_refuses_an_unscoped_arrival() {
|
||||
let double = wire_fabric_with_listen(true, false).await;
|
||||
|
||||
Reference in new issue
Block a user