feat: consumer-session — TunnelSession (open/adopt, data planes, teardown)
- TunnelSession (src/consumer.rs): open (ChannelClient::open_channel) + adopt (ChannelManager::adopt_channel) construction, substrate- shaped data planes (raw halves for stream; ADR-003 codec + pending frame queue for UDP), pump_against (session-owned pump_bidi handle), and teardown ownership per ADR-005: close (abort + ungraceful reap), join (await + reap + copy counts, pump-less (0,0,reaped)), Drop (abort + sync reap, never leaks). No Clone (compile_fail doc-test). - open_reverse_channel on the CallConnection (the reverse path's two-step: call then adopt — the POC's honest shape). - Error surfaces (src/error.rs): TunnelOpenError (typed ADR-049 §4 surface via open_ref/establishment_reason), TunnelIoError (wrong- substrate, TruncatedDatagram fail-loud, ChannelTaken), ReverseOpenError. - Tests: tests/consumer_session.rs (12) over two harness topologies — forward (wire_forward/ForwardTopology: consumer connect-side pure client, producer serving via adapter) and reverse (the POC shape); teardown matrix, W4 half-close, out-of-band close + self-reap (W3), F-2 empty datagram, AdoptFailed-on-collision (bogus-id adopt parks by design — documented), concurrent sessions. Harness: all_substrate_dial. - Adopt takes substrate (task-note deviation: the data plane shape rode the open call; cannot be inferred from the ID). Verified: cargo test green (38), clippy -D warnings (native + wasm32), fmt clean, cargo check --target wasm32-unknown-unknown passes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
//! Test harness — the producer↔consumer wiring over a
|
||||
//! `tokio::io::duplex` carrying the channels 8-byte chunk format.
|
||||
#![allow(dead_code)]
|
||||
//!
|
||||
//! Topology (the reverse POC's shape, mirrored for both directions):
|
||||
//! - **producer** (connect side of the channels connection — it runs
|
||||
@@ -222,6 +223,133 @@ pub async fn wire(registry: ResourceRegistry, dial: alktunnels::producer::DialFn
|
||||
.await
|
||||
}
|
||||
|
||||
/// The forward topology (the `-L` direction): the consumer dials the
|
||||
/// transport (connect side — `ChannelClient::from_connection`, a pure
|
||||
/// consumer serving nothing) and the producer accepts (the adapter +
|
||||
/// an install hook that registers the channel ops + the tunnel
|
||||
/// openable on the producer's serving registry). This is the topology
|
||||
/// `TunnelSession::open(&channel_client, params)` is normative for:
|
||||
/// the consumer's own client calls the open op on the producer's
|
||||
/// channel 0.
|
||||
///
|
||||
/// Identity on the accept-side serving path (the mTLS posture): the
|
||||
/// transport authenticated the dialer; the hook attaches the resolved
|
||||
/// identity to channel 0 so the serving dispatch sees the caller
|
||||
/// (the accept-side analogue of CF-005 (b)'s propagation).
|
||||
pub struct ForwardTopology {
|
||||
/// The consumer's own client (connect side, pure consumer). Its
|
||||
/// manager adopted the producer-allocated channel IDs; the
|
||||
/// session reaps via this manager.
|
||||
pub consumer: Arc<ChannelClient>,
|
||||
/// The producer's manager (accept side, serving) — for asserting
|
||||
/// wrapper-managed reaping.
|
||||
pub producer_manager: ChannelManager,
|
||||
/// What the establisher's per-call auth carried (the CF-006 probe).
|
||||
pub identity_witness: Arc<tokio::sync::Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
pub async fn wire_forward(
|
||||
registry: ResourceRegistry,
|
||||
dial: alktunnels::producer::DialFn,
|
||||
) -> ForwardTopology {
|
||||
let producer_op_registry = Arc::new(OperationRegistry::new());
|
||||
let (producer_manager_tx, mut producer_manager_rx) =
|
||||
tokio::sync::mpsc::channel::<ChannelManager>(1);
|
||||
let identity_witness = Arc::new(tokio::sync::Mutex::new(None::<String>));
|
||||
let dial_witness = Arc::clone(&identity_witness);
|
||||
|
||||
// --- producer (accept side, serving): adapter + install hook ------
|
||||
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
|
||||
let registry_arc = Arc::clone(&producer_op_registry);
|
||||
let resource_registry = registry.clone();
|
||||
let dial = Arc::clone(&dial);
|
||||
let witness = Arc::clone(&dial_witness);
|
||||
let manager_tx = producer_manager_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let channel0_bidi = match channel0_conn.accept_bi().await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
let (writer, reader) = split_single_stream(channel0_bidi);
|
||||
// The accept-side transport-identity posture: the transport
|
||||
// authenticated the dialer; attach the resolved identity to
|
||||
// channel 0 (the dispatch reads it as the caller identity).
|
||||
let _ = channel0_conn.set_identity(consumer_identity());
|
||||
let call_connection = Arc::new(CallConnection::new_single_stream(
|
||||
channel0_conn,
|
||||
Arc::clone(&writer),
|
||||
));
|
||||
|
||||
// Register the generic channel ops + the tunnel openable
|
||||
// (the producer.md recipe) before the dispatch loop runs.
|
||||
let core = alkcall::channels::operations::ChannelCore::new(
|
||||
manager.clone(),
|
||||
alkcall::channels::policy::default_policy(),
|
||||
);
|
||||
let ops = ChannelOperations::with_default_policy(manager.clone());
|
||||
ops.register_on(®istry_arc)
|
||||
.expect("register channel ops on producer");
|
||||
register_tunnel_openable(
|
||||
&core,
|
||||
&resource_registry,
|
||||
®istry_arc,
|
||||
AuthContext::anonymous(b"alk/tunnel"),
|
||||
dial,
|
||||
Some(witness),
|
||||
)
|
||||
.expect("register tunnel openable");
|
||||
let _ = manager_tx.send(manager).await;
|
||||
|
||||
let dp = Dispatcher::new(
|
||||
registry_arc,
|
||||
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
||||
);
|
||||
dp.serve_single_stream(call_connection, reader, writer)
|
||||
.await;
|
||||
})
|
||||
});
|
||||
|
||||
// --- transport ------------------------------------------------------
|
||||
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);
|
||||
|
||||
// The dialer's transport identity (the mTLS/QUIC posture) — set
|
||||
// before from_connection. The accept side resolves it out-of-band
|
||||
// (modeled in the hook above).
|
||||
consumer_conn
|
||||
.set_identity(consumer_identity())
|
||||
.expect("transport identity set once");
|
||||
|
||||
let adapter = ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
|
||||
let producer_transport_auth = AuthContext::anonymous(b"alk/channels");
|
||||
let _adapter_task = tokio::spawn(async move {
|
||||
let _ = alkcall::core::types::ProtocolHandler::handle(
|
||||
&adapter,
|
||||
producer_conn,
|
||||
&producer_transport_auth,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
// --- consumer (connect side): pure client ---------------------------
|
||||
let consumer_client = ChannelClient::from_connection(consumer_conn)
|
||||
.await
|
||||
.expect("consumer channel client init");
|
||||
let consumer_client = Arc::new(consumer_client);
|
||||
|
||||
let producer_manager = producer_manager_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("producer manager captured");
|
||||
|
||||
ForwardTopology {
|
||||
consumer: consumer_client,
|
||||
producer_manager,
|
||||
identity_witness,
|
||||
}
|
||||
}
|
||||
|
||||
/// An echo dial closure (the ADR-004 injection point under test): an
|
||||
/// in-process pipe per dial — the "target" echoes bytes until EOF.
|
||||
/// The consumer side gets split `DuplexStream` halves; a spawned task
|
||||
@@ -263,6 +391,17 @@ pub fn echo_dial() -> alktunnels::producer::DialFn {
|
||||
})
|
||||
}
|
||||
|
||||
/// A both-substrates dial: stream substrates echo via pipes, UDP
|
||||
/// echoes via the framed codec (the datagram tests' single dial).
|
||||
pub fn all_substrate_dial() -> alktunnels::producer::DialFn {
|
||||
let echo = echo_dial();
|
||||
let udp = framed_udp_echo_dial();
|
||||
Arc::new(move |substrate: Substrate, backing: &str| match substrate {
|
||||
Substrate::Udp => udp(substrate, backing),
|
||||
Substrate::Tcp | Substrate::Unix => echo(substrate, backing),
|
||||
})
|
||||
}
|
||||
|
||||
/// A failing dial closure (the `dial_failed` stand-in).
|
||||
pub fn failing_dial(message: &'static str) -> alktunnels::producer::DialFn {
|
||||
Arc::new(move |_substrate: Substrate, _backing: &str| {
|
||||
@@ -273,3 +412,60 @@ pub fn failing_dial(message: &'static str) -> alktunnels::producer::DialFn {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// A framed UDP echo dial (the ADR-003 codec at the target boundary —
|
||||
/// the in-process stand-in for the real `local` UDP adapter): the
|
||||
/// dial's halves carry the `[len: u16 BE]` wire framing; a spawned
|
||||
/// echo task decodes frames from the channel side and re-frames the
|
||||
/// echoed payloads back. The producer pump copies raw bytes; the
|
||||
/// codec lives at this boundary (the pump never sees UDP specifics).
|
||||
pub fn framed_udp_echo_dial() -> alktunnels::producer::DialFn {
|
||||
Arc::new(move |substrate: Substrate, backing: &str| {
|
||||
let backing = backing.to_string();
|
||||
Box::pin(async move {
|
||||
match substrate {
|
||||
Substrate::Udp => {
|
||||
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
|
||||
tokio::spawn(async move {
|
||||
use alktunnels::wire::DatagramReader;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
let (mut t_read, mut t_write) = tokio::io::split(target_side);
|
||||
let mut reader = DatagramReader::new();
|
||||
let mut buf = vec![0u8; 16 * 1024];
|
||||
loop {
|
||||
let n = match t_read.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => return,
|
||||
Ok(n) => n,
|
||||
};
|
||||
let dgs = match reader.feed(&buf[..n]) {
|
||||
Ok(dgs) => dgs,
|
||||
Err(_) => return,
|
||||
};
|
||||
for dg in dgs {
|
||||
let framed = match alktunnels::wire::frame_datagram(&dg) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return,
|
||||
};
|
||||
if t_write.write_all(&framed).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = t_write.flush().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = &backing;
|
||||
let (c_read, c_write) = tokio::io::split(consumer_side);
|
||||
Ok(alktunnels::producer::TargetHandle {
|
||||
read: Box::new(c_read),
|
||||
write: Box::new(c_write),
|
||||
})
|
||||
}
|
||||
Substrate::Tcp | Substrate::Unix => {
|
||||
Err(alktunnels::producer::TunnelEstablishError::DialFailed(
|
||||
format!("stream dial not wired in the udp harness: {backing}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user