- params.rs: OP_TUNNEL_DIRECT / TUNNEL_DIRECT_SCOPE constants;
SubstrateAddr (untagged {host,port} | {path}; also the ADR-008 §3
peer shape); TunnelDirectParams with render_target();
tunnel_direct_spec() — Sub-typed, External, alk/tunnel ALPN
marker, scope ["tunnel:direct"] (never implied by tunnel:open),
per-substrate if/then target sub-schema (malformed targets fail
the registry's schema gate as INVALID_INPUT before the
establisher). 7 unit tests.
- producer.rs: direct_establisher (+ witness variant) — parse,
render the target to the registry backing-string form, dial via
the SAME injected DialFn; no registry lookup (unknown_resource
can never fire); typed errors per ADR-007 §4 minus the registry
row; CF-006 witness seam. register_tunnel_direct_openable reuses
the base pump handler (plan-flow unchanged).
- consumer.rs: TunnelSession::open_direct — separate constructor
(two scopes are two capabilities), plain open_channel (dial
establishers never bind), identical session/data-planes/teardown.
- tests/producer_direct_op.rs: 15 integration tests — tcp + udp e2e
round-trips, malformed-target rejection (no phantom session),
dial-failure pass-through, NOT_FOUND posture, scope separation
both ways (FORBIDDEN, raw-call + session-level), CF-006 witness
(raw + session paths), parse backstop, target-rendering pin.
Harness: Direct registration mode, wire_direct* topologies,
direct_identity/both_scopes_identity.
- CHANGELOG: Unreleased → Added.
Verified: cargo test green (93 native, 94 --all-features); clippy
-D warnings clean (native + wasm32); fmt clean; wasm32 check passes
(default crate stays wasm-clean); cargo doc clean.
Task: tasks/tunnels/direct-op.md (status: completed)
827 lines
35 KiB
Rust
827 lines
35 KiB
Rust
//! 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
|
|
//! `ChannelClient::from_connection_with_serving`): dials the
|
|
//! duplex, serves the tunnel open op + generic channel ops on its
|
|
//! serving registry. Its manager allocates the tunnel channel IDs
|
|
//! (`ChannelSide::Connect` — ADR-047 §5: the serving side
|
|
//! allocates; the initiator adopts).
|
|
//! - **consumer** (accept side): the `ChannelsAdapter` + capturing
|
|
//! install hook. The hook captures the consumer's channel-0
|
|
//! `CallConnection` (for calling the producer's open op) and the
|
|
//! consumer's `ChannelManager` (for adopting the producer-
|
|
//! allocated IDs and tearing channels down). The consumer serves
|
|
//! no ops.
|
|
//!
|
|
//! Identity on the serving path follows alkcall 0.7.0's CF-005
|
|
//! precedence: payload `auth_token` → `ServingConfig.identity` → the
|
|
//! transport connection's identity (`Connection::set_identity` before
|
|
//! dialing). CF-006 (the per-call opener identity on the establisher)
|
|
//! is probed via the `identity_witness` the establisher records.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use alkcall::channels::adapter::{ChannelsAdapter, InstallChannelZero};
|
|
use alkcall::channels::client::{ChannelClient, ServingConfig};
|
|
use alkcall::channels::manager::ChannelManager;
|
|
use alkcall::channels::operations::ChannelOperations;
|
|
use alkcall::core::auth::{AuthContext, AuthToken, Identity, IdentityProvider};
|
|
use alkcall::core::types::Connection as CoreConnection;
|
|
use alkcall::protocol::connection::{split_single_stream, CallConnection};
|
|
use alkcall::protocol::dispatch::Dispatcher;
|
|
use alkcall::registry::registration::OperationRegistry;
|
|
|
|
use alktunnels::params::{Substrate, TUNNEL_DIRECT_SCOPE, TUNNEL_OPEN_SCOPE};
|
|
use alktunnels::producer::{
|
|
register_tunnel_direct_openable, register_tunnel_openable, ResourceRegistry,
|
|
};
|
|
|
|
/// The consumer identity the transport carries (the mTLS/QUIC
|
|
/// analogue — key-based identity resolved out-of-band, attached to
|
|
/// the dialing connection before `from_connection_with_serving`).
|
|
pub fn consumer_identity() -> Identity {
|
|
Identity {
|
|
id: "consumer".to_string(),
|
|
scopes: vec![TUNNEL_OPEN_SCOPE.to_string()],
|
|
resources: std::collections::HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// The direct-egress identity: `tunnel:direct` ONLY (no
|
|
/// `tunnel:open`) — the scope-separation probe's identity.
|
|
pub fn direct_identity() -> Identity {
|
|
Identity {
|
|
id: "consumer-direct".to_string(),
|
|
scopes: vec![TUNNEL_DIRECT_SCOPE.to_string()],
|
|
resources: std::collections::HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Both scopes: `tunnel:open` AND `tunnel:direct` (a deployment
|
|
/// granting both grants both explicitly — ADR-006 Amendment 1).
|
|
pub fn both_scopes_identity() -> Identity {
|
|
Identity {
|
|
id: "consumer-both".to_string(),
|
|
scopes: vec![
|
|
TUNNEL_OPEN_SCOPE.to_string(),
|
|
TUNNEL_DIRECT_SCOPE.to_string(),
|
|
],
|
|
resources: std::collections::HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// The scoped token (the hub-forwarding path) — resolves to
|
|
/// [`consumer_identity`] via [`TestIdProvider`].
|
|
pub const TEST_AUTH_TOKEN: &str = "test-token-with-tunnel-open-scope";
|
|
|
|
/// Resolve the scoped token to the consumer identity; anything else
|
|
/// resolves to none (falling through to the transport identity per
|
|
/// the 0.7.0 precedence order).
|
|
pub struct TestIdProvider;
|
|
impl IdentityProvider for TestIdProvider {
|
|
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
|
|
None
|
|
}
|
|
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
|
|
if token.raw == TEST_AUTH_TOKEN.as_bytes() {
|
|
Some(consumer_identity())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The full producer↔consumer topology over one duplex transport.
|
|
pub struct Topology {
|
|
/// The producer's client (connect side, serving). Its manager
|
|
/// allocated the tunnel channels; its pumps are wrapper-managed.
|
|
pub producer: Arc<ChannelClient>,
|
|
/// The consumer's manager (accept side). `adopt` installs the
|
|
/// producer-allocated IDs here; reaping calls `teardown_channel`.
|
|
pub consumer_manager: ChannelManager,
|
|
/// The consumer's channel-0 call connection — its surface for
|
|
/// calling the producer's open op (and channel/close).
|
|
pub consumer_call: Arc<CallConnection>,
|
|
/// The producer's serving registry (probes).
|
|
pub producer_registry: Arc<OperationRegistry>,
|
|
/// What the establisher's per-call auth carried as the opener
|
|
/// identity id (the CF-006 probe — `None` until an open runs).
|
|
pub identity_witness: Arc<tokio::sync::Mutex<Option<String>>>,
|
|
}
|
|
|
|
/// Wire the topology. `transport_identity` is the key-based identity
|
|
/// attached to the dialing connection (CF-005 remediation (b));
|
|
/// `serving_identity` is the explicit `ServingConfig.identity`
|
|
/// override (CF-005 remediation (a)) — `None` in the primary path.
|
|
/// `provider` resolves payload tokens (the hub-forwarding path).
|
|
/// The open-op registration mode: a dial establisher (shape 1) or a
|
|
/// listen establisher over an assembly-fed [`AcceptQueue`] (shape 2).
|
|
pub enum RegistrationMode {
|
|
Dial(alktunnels::producer::DialFn),
|
|
Listen(alktunnels::producer::AcceptFn),
|
|
/// A hanging establisher registered with an explicit
|
|
/// per-registration establishment timeout (the deadline-expiry
|
|
/// probe — ADR-049 §2's bound override; the open fails with reason
|
|
/// `timeout` and no channel survives).
|
|
Timeout(std::time::Duration),
|
|
/// The no-witness dial establisher (`tunnel_establisher` — the
|
|
/// public default shape, no CF-006 probe seam).
|
|
DialNoWitness(alktunnels::producer::DialFn),
|
|
/// The direct op (`channels/tunnel/direct`, ADR-007): the direct
|
|
/// spec + the direct establisher (no registry lookup) over the
|
|
/// SAME injected dial, with the CF-006 witness seam. The witness
|
|
/// rides the topology's `identity_witness` (the direct op's
|
|
/// witness and the base op's never mix in one test).
|
|
Direct(alktunnels::producer::DialFn),
|
|
}
|
|
|
|
pub async fn wire_with(
|
|
registry: ResourceRegistry,
|
|
mode: RegistrationMode,
|
|
transport_identity: Option<Identity>,
|
|
serving_identity: Option<Identity>,
|
|
provider: Arc<dyn IdentityProvider>,
|
|
) -> Topology {
|
|
let producer_op_registry = Arc::new(OperationRegistry::new());
|
|
let (consumer_call_tx, mut consumer_call_rx) =
|
|
tokio::sync::mpsc::channel::<Arc<CallConnection>>(1);
|
|
let (consumer_manager_tx, mut consumer_manager_rx) =
|
|
tokio::sync::mpsc::channel::<ChannelManager>(1);
|
|
|
|
// --- consumer (accept side): ChannelsAdapter + capturing hook ------
|
|
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
|
|
let call_tx = consumer_call_tx.clone();
|
|
let manager_tx = consumer_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);
|
|
let call_connection = Arc::new(CallConnection::new_single_stream(
|
|
channel0_conn,
|
|
Arc::clone(&writer),
|
|
));
|
|
let _ = call_tx.send(Arc::clone(&call_connection)).await;
|
|
let _ = manager_tx.send(manager).await;
|
|
// The consumer serves no ops — an empty registry. The
|
|
// dispatcher keeps the call loop alive (its read loop
|
|
// resolves the producer's responses to the consumer's
|
|
// pending entries).
|
|
let dp = Dispatcher::new(
|
|
Arc::new(OperationRegistry::new()),
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
);
|
|
dp.serve_single_stream(call_connection, reader, writer)
|
|
.await;
|
|
})
|
|
});
|
|
|
|
// --- transport ------------------------------------------------------
|
|
let (producer_end, consumer_end) = tokio::io::duplex(64 * 1024);
|
|
let producer_conn = CoreConnection::from_bidi(producer_end, b"alk/channels".to_vec(), None);
|
|
let consumer_conn = CoreConnection::from_bidi(consumer_end, b"alk/channels".to_vec(), None);
|
|
|
|
let adapter = ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
|
|
let consumer_transport_auth = AuthContext::anonymous(b"alk/channels");
|
|
let _adapter_task = tokio::spawn(async move {
|
|
let _ = alkcall::core::types::ProtocolHandler::handle(
|
|
&adapter,
|
|
consumer_conn,
|
|
&consumer_transport_auth,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
// The transport identity attaches to the dialing connection
|
|
// BEFORE from_connection_with_serving (the mTLS/QUIC posture).
|
|
if let Some(id) = transport_identity {
|
|
producer_conn
|
|
.set_identity(id)
|
|
.expect("transport identity set once");
|
|
}
|
|
|
|
// --- producer (connect side, serving) --------------------------------
|
|
let producer_client = ChannelClient::from_connection_with_serving(
|
|
producer_conn,
|
|
Some(ServingConfig {
|
|
registry: Arc::clone(&producer_op_registry),
|
|
identity_provider: provider,
|
|
identity: serving_identity,
|
|
}),
|
|
)
|
|
.await
|
|
.expect("producer channel client init");
|
|
|
|
// The generic channel ops ride the producer's serving registry
|
|
// (channel/close tears a channel down out-of-band — the review
|
|
// 007 recipe).
|
|
let ops = ChannelOperations::with_default_policy(producer_client.manager().clone());
|
|
ops.register_on(&producer_op_registry)
|
|
.expect("register channel ops on producer");
|
|
|
|
// Post-hoc openable registration on the shared registry (W2). The
|
|
// identity witness captures what the establisher's per-call auth
|
|
// carried (the CF-006 probe).
|
|
let identity_witness = Arc::new(tokio::sync::Mutex::new(None::<String>));
|
|
let core = alkcall::channels::operations::ChannelCore::new(
|
|
producer_client.manager().clone(),
|
|
alkcall::channels::policy::default_policy(),
|
|
);
|
|
match mode {
|
|
RegistrationMode::Dial(dial) => {
|
|
register_tunnel_openable(
|
|
&core,
|
|
®istry,
|
|
&producer_op_registry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
dial,
|
|
Some(Arc::clone(&identity_witness)),
|
|
)
|
|
.expect("register tunnel openable");
|
|
}
|
|
RegistrationMode::Direct(dial) => {
|
|
register_tunnel_direct_openable(
|
|
&core,
|
|
&producer_op_registry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
dial,
|
|
Some(Arc::clone(&identity_witness)),
|
|
)
|
|
.expect("register tunnel direct openable");
|
|
}
|
|
RegistrationMode::Listen(accept) => {
|
|
alktunnels::producer::register_tunnel_listen_openable(
|
|
&core,
|
|
®istry,
|
|
&producer_op_registry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
accept,
|
|
)
|
|
.expect("register tunnel listen openable");
|
|
}
|
|
RegistrationMode::DialNoWitness(dial) => {
|
|
// The public no-witness wrapper (tunnel_establisher) —
|
|
// registration + establisher body identical to the
|
|
// witnessed variant minus the CF-006 seam.
|
|
let core = alkcall::channels::operations::ChannelCore::new(
|
|
producer_client.manager().clone(),
|
|
alkcall::channels::policy::default_policy(),
|
|
);
|
|
core.register_openable_with_establisher(
|
|
alktunnels::params::tunnel_open_spec(),
|
|
Some(alktunnels::producer::tunnel_establisher(
|
|
registry.clone(),
|
|
dial,
|
|
)),
|
|
alktunnels::producer::make_tunnel_pump_handler(),
|
|
&producer_op_registry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
None,
|
|
)
|
|
.expect("register the no-witness wrapper establisher");
|
|
}
|
|
RegistrationMode::Timeout(timeout) => {
|
|
// The deadline-expiry probe's registration shape: a hanging
|
|
// establisher (never resolves) with an explicit per-
|
|
// registration timeout. The generic channel ops registered
|
|
// above on the SAME registry are unaffected (per-op
|
|
// timeout, ADR-049 §2).
|
|
// A fresh ChannelCore over the SAME manager + policy (both
|
|
// cores are cheap facades over the same pair — the second
|
|
// registration rides the same ledger/cap state; this arm
|
|
// is local to the probe so the other arms' `core` binding
|
|
// stays untouched).
|
|
let core = alkcall::channels::operations::ChannelCore::new(
|
|
producer_client.manager().clone(),
|
|
alkcall::channels::policy::default_policy(),
|
|
);
|
|
core.register_openable_with_establisher(
|
|
alktunnels::params::tunnel_open_spec(),
|
|
Some(Arc::new(|_input: serde_json::Value, _auth| {
|
|
Box::pin(async {
|
|
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
|
|
Err(
|
|
alkcall::channels::operations::EstablishmentError::HandlerError {
|
|
message: "hanging establisher resolved; must be timed out"
|
|
.to_string(),
|
|
},
|
|
)
|
|
})
|
|
})),
|
|
alktunnels::producer::make_tunnel_pump_handler(),
|
|
&producer_op_registry,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
Some(timeout),
|
|
)
|
|
.expect("register hanging establisher");
|
|
}
|
|
}
|
|
let producer_client = Arc::new(producer_client);
|
|
|
|
// --- consumer pieces (captured from the hook) -------------------------
|
|
let consumer_call = consumer_call_rx.recv().await.expect("consumer call conn");
|
|
let consumer_manager = consumer_manager_rx.recv().await.expect("consumer manager");
|
|
|
|
Topology {
|
|
producer: producer_client,
|
|
consumer_manager,
|
|
consumer_call,
|
|
producer_registry: producer_op_registry,
|
|
identity_witness,
|
|
}
|
|
}
|
|
|
|
/// The default topology: the consumer's transport identity is the
|
|
/// primary caller-identity path (no token). The provider is
|
|
/// noop-shaped — only the identity chain authorizes.
|
|
pub async fn wire(registry: ResourceRegistry, dial: alktunnels::producer::DialFn) -> Topology {
|
|
wire_with(
|
|
registry,
|
|
RegistrationMode::Dial(dial),
|
|
Some(consumer_identity()),
|
|
None,
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// The direct-op topology (ADR-007): the direct op registered with
|
|
/// the CF-006 witness; the consumer's transport identity is
|
|
/// [`both_scopes_identity`] (both grants — the opens authorize).
|
|
pub async fn wire_direct(dial: alktunnels::producer::DialFn) -> Topology {
|
|
wire_with(
|
|
ResourceRegistry::new(),
|
|
RegistrationMode::Direct(dial),
|
|
Some(both_scopes_identity()),
|
|
None,
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// [`wire_direct`] with an explicit transport identity (the
|
|
/// scope-separation probes construct their own identities).
|
|
pub async fn wire_direct_with_identity(identity: Identity) -> Topology {
|
|
wire_with(
|
|
ResourceRegistry::new(),
|
|
RegistrationMode::Direct(failing_dial("unused")),
|
|
Some(identity),
|
|
None,
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// A topology with the consumer's transport identity REPLACED by the
|
|
/// serving-side override (`ServingConfig.identity` — CF-005 (a) probe
|
|
/// shape) or stripped entirely (the fail-closed probe). The transport
|
|
/// identity still rides the dialing connection; the override is what
|
|
/// the serving dispatch resolves (the witness proves which one won).
|
|
pub async fn wire_serving_identity(
|
|
registry: ResourceRegistry,
|
|
dial: alktunnels::producer::DialFn,
|
|
override_identity: Option<Identity>,
|
|
) -> Topology {
|
|
wire_with(
|
|
registry,
|
|
RegistrationMode::Dial(dial),
|
|
Some(consumer_identity()),
|
|
override_identity,
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// The listen topology (shape 2): the producer's establisher pops
|
|
/// accepted handles from the assembly-fed [`AcceptQueue`] instead of
|
|
/// dialing. Same identity posture as [`wire`].
|
|
pub async fn wire_listen(
|
|
registry: ResourceRegistry,
|
|
accept: alktunnels::producer::AcceptFn,
|
|
) -> Topology {
|
|
wire_with(
|
|
registry,
|
|
RegistrationMode::Listen(accept),
|
|
Some(consumer_identity()),
|
|
None,
|
|
Arc::new(alkcall::core::auth::NoopIdentityProvider),
|
|
)
|
|
.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,
|
|
}
|
|
}
|
|
|
|
/// The DIRECT-op forward topology (ADR-007): the producer (accept
|
|
/// side, serving) registers the direct op — [`register_tunnel_direct_openable`]
|
|
/// over the injected dial, CF-006 witness attached — and the consumer
|
|
/// (connect side, pure client) holds the identity the session tests
|
|
/// authorize with. This is the topology `TunnelSession::open_direct`
|
|
/// is normative for.
|
|
pub async fn wire_direct_forward(dial: alktunnels::producer::DialFn) -> ForwardTopology {
|
|
wire_direct_forward_with_identity(dial, both_scopes_identity()).await
|
|
}
|
|
|
|
/// [`wire_direct_forward`] with an explicit transport identity (the
|
|
/// session-level scope-separation probes construct their own).
|
|
pub async fn wire_direct_forward_with_identity(
|
|
dial: alktunnels::producer::DialFn,
|
|
identity: Identity,
|
|
) -> 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);
|
|
let expected_caller = identity.clone();
|
|
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
|
|
let registry_arc = Arc::clone(&producer_op_registry);
|
|
let dial = Arc::clone(&dial);
|
|
let witness = Arc::clone(&dial_witness);
|
|
let manager_tx = producer_manager_tx.clone();
|
|
let caller = expected_caller.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
|
|
// (scopes included — resolved out-of-band) to channel 0.
|
|
let _ = channel0_conn.set_identity(caller);
|
|
let call_connection = Arc::new(CallConnection::new_single_stream(
|
|
channel0_conn,
|
|
Arc::clone(&writer),
|
|
));
|
|
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_direct_openable(
|
|
&core,
|
|
®istry_arc,
|
|
AuthContext::anonymous(b"alk/tunnel"),
|
|
dial,
|
|
Some(witness),
|
|
)
|
|
.expect("register tunnel direct 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;
|
|
})
|
|
});
|
|
|
|
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);
|
|
consumer_conn
|
|
.set_identity(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;
|
|
});
|
|
|
|
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
|
|
/// echoes the other end. No `local` feature, no sockets.
|
|
pub fn echo_dial() -> alktunnels::producer::DialFn {
|
|
Arc::new(move |substrate: Substrate, backing: &str| {
|
|
let backing = backing.to_string();
|
|
Box::pin(async move {
|
|
match substrate {
|
|
Substrate::Tcp | Substrate::Unix => {
|
|
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
|
|
let (mut t_read, mut t_write) = tokio::io::split(target_side);
|
|
tokio::spawn(async move {
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
let mut buf = vec![0u8; 8192];
|
|
loop {
|
|
match t_read.read(&mut buf).await {
|
|
Ok(0) | Err(_) => return,
|
|
Ok(n) => {
|
|
if t_write.write_all(&buf[..n]).await.is_err() {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
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::Udp => Err(alktunnels::producer::TunnelEstablishError::DialFailed(
|
|
format!("udp dial not wired in this harness: {backing}"),
|
|
)),
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// 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 framed UDP echo dial whose target end CLOSES after the first
|
|
/// echoed datagram — the EOF-path harness. `payload_completes`
|
|
/// controls the truncation shape: `true` forwards the whole frame
|
|
/// (echo + payload intact) then drops → the clean-EOF path;
|
|
/// `false` forwards only the 2-byte length prefix then drops → the
|
|
/// mid-datagram EOF path (`TruncatedDatagram`).
|
|
pub fn closing_udp_echo_dial(payload_completes: bool) -> alktunnels::producer::DialFn {
|
|
Arc::new(move |substrate: Substrate, backing: &str| {
|
|
let backing = backing.to_string();
|
|
Box::pin(async move {
|
|
match substrate {
|
|
Substrate::Udp => {
|
|
use alktunnels::wire::DatagramReader;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
|
|
tokio::spawn(async move {
|
|
let (mut t_read, mut t_write) = tokio::io::split(target_side);
|
|
let mut reader = DatagramReader::new();
|
|
let mut buf = vec![0u8; 16 * 1024];
|
|
// Read until the first complete frame (the
|
|
// echo-closes-after-first shape): forward it
|
|
// whole (clean-EOF shape) or forward only its
|
|
// length prefix (truncated shape), then drop
|
|
// the far end — the channel read half EOFs.
|
|
let n = match t_read.read(&mut buf).await {
|
|
Ok(0) | Err(_) => return,
|
|
Ok(n) => n,
|
|
};
|
|
let dgs = reader.feed(&buf[..n]);
|
|
if let Some(dg) = dgs.into_iter().next() {
|
|
let framed = match alktunnels::wire::frame_datagram(&dg) {
|
|
Ok(f) => f,
|
|
Err(_) => return,
|
|
};
|
|
if payload_completes {
|
|
if t_write.write_all(&framed).await.is_err() {
|
|
return;
|
|
}
|
|
let _ = t_write.flush().await;
|
|
} else {
|
|
// The truncated shape: forward ONLY
|
|
// the 2-byte length prefix, then drop —
|
|
// the declared payload never arrives.
|
|
if t_write.write_all(&framed[..2]).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}"),
|
|
))
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// 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| {
|
|
Box::pin(async move {
|
|
Err(alktunnels::producer::TunnelEstablishError::DialFailed(
|
|
message.to_string(),
|
|
))
|
|
})
|
|
})
|
|
}
|
|
|
|
/// 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 = reader.feed(&buf[..n]);
|
|
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}"),
|
|
))
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|