feat(tunnel): land the -R listen flow + the forwarded-tcpip data leg (w3-tunnel-listen)

The tunnel branch's other half is served for real against published
alktunnels 0.4.1 / alkcall 0.10.1, with POC-3 §C.4 as the template:

- tcpip_forward/streamlocal_forward (+ both cancel forms) map the
  vanilla listen request family-ward through TunnelListener::open on
  the held ChannelClient (bounded inline dial — the global request's
  reply + the &mut u32 bound-port out-param are the hook's); the
  wire bind endpoint rides as the resource string verbatim (OpenSSH
  client-side normalization parity). A reply without the minted
  listen token/bound report declines and reaps its fabric listener.
- the door serves the fabric's inbound tunnel/forwarded opens: the
  serve config gains TunnelListenSeam (the seam-config second
  course-correction — the shared ServingConfig registry the assembly
  wired at from_connection_with_serving); alkssh registers the
  forwarded openable (ChannelCore over a clone of the held client's
  manager) at SshAdapter::new — a failed registration is a failed
  construction (AdapterError::ListenRegistration).
- the forwarded-tcpip data leg: token correlation through the shared
  ListenRouter → the in-crate per-session channel cap at the accept
  surface (the acceptor vocabulary) → Handle::
  channel_open_forwarded_tcpip streaming the listen request's own
  args + the actually-bound port (sshd parity) with
  TunnelForwardedParams.peer as the originator → pump_bidi(ssh,
  fabric). Correlation-miss → teardown-at-birth, never a half-open
  SSH channel. Unix arrivals ride forwarded-streamlocal@openssh.com.
- bounded bookkeeping: the per-session listener map packet-loop-only
  with a dedicated max_concurrent_channels-ceiling counter; the
  route map bounded by construction (mints at accepted listens,
  removed at cancels + session teardown — the handler's Drop).
- the harness grew the client-side accept hook
  (server_channel_open_forwarded_tcpip/_streamlocal,
  acceptor-driven) + a hand-rolled tracing subscriber; 18 contract
  rows + both real-tool smokes (ssh -L, ssh -R + curl) green in
  tests/tunnel_mapping.rs; the wave-2 stub listen rows migrated per
  build (unfeatured / seam-unconfigured).

Verified: cargo test across default/git/session/sftp/tunnel and all
combos + --all-features; clippy -D warnings (default, all-features,
combos); cargo fmt --check; cargo doc --no-deps clean;
taskgraph validate (26 tasks).
This commit is contained in:
glm-5.3-flash committed 2026-10-29 04:24:50 +00:00
1 parent 6443feac3e
commit 3d0253d981
15 files changed
+2839 -236

No files matched your search

+17
View File
@@ -50,6 +50,8 @@ use tokio::io::{AsyncRead, AsyncWrite};
use crate::config::ServeConfig;
use crate::error::{AdapterError, KeySourceError};
use crate::handler::SessionHandler;
#[cfg(feature = "tunnel")]
use crate::tunnel_map;
/// The constructed server stack — the substrate every channel-type
/// mapping composes into (ADR-001's `SshAdapter`).
@@ -70,6 +72,21 @@ impl SshAdapter {
/// server, with the [`KeySourceError`] refused through
/// [`AdapterError::Construction`].
pub async fn new(serve: ServeConfig) -> Result<Self, AdapterError> {
let serve = {
#[cfg(not(feature = "tunnel"))]
{
serve
}
#[cfg(feature = "tunnel")]
{
let mut serve = serve;
if serve.channel_client.is_some() && serve.tunnel_listen_seam.is_some() {
tunnel_map::install_forwarded_surface(&mut serve)
.map_err(AdapterError::ListenRegistration)?;
}
serve
}
};
let keys = serve.host_key_source.load().await?;
if keys.is_empty() {
return Err(AdapterError::Construction(KeySourceError::Absent));
+40 -45
View File
@@ -16,13 +16,15 @@
//! [`crate::dispatch::cap_exceeded_open_decline`]). Release
//! ([`ChannelCap::release`]) fires from the `channel_close` hook for
//! established channels — and, since `w3-tunnel-direct`, from a
//! spawned route-1 bridge task on a fabric-open failure, where the
//! open was *admitted but never established* (the reject shape means
//! no `CHANNEL_CLOSE` ever cycles back): see the release-sites section.
//! spawned route-1 bridge task on a fabric-open failure — and, since
//! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the
//! never-established paths (client-rejected open, dead session): see
//! the release-sites section.
//!
//! ## Release sites (the `w3-tunnel-direct` recorded decision)
//! ## Release sites (the recorded decision, three sites since
//! `w3-tunnel-listen`)
//!
//! Every admission has exactly one release, and the two paths are
//! Every admission has exactly one release, and the paths are
//! disjoint by the wire shape:
//!
//! 1. **The established-channel close** — the `channel_close` hook.
@@ -31,37 +33,39 @@
//! is consumed at most once (russh removes the channel before the
//! hook).
//! 2. **The admitted-but-rejected bridge** — the route-1 bridge's
//! failure path. The wave-3 flow admits at the open hook (the
//! packet-loop single-writer site — no race window against the cap
//! check) and holds the count across the fabric open; on fabric
//! failure the open is rejected with the explicit `ConnectFailed`
//! failure path (`w3-tunnel-direct`). The open hook admits and
//! holds the count across the fabric open; on fabric failure the
//! open is rejected with the explicit `ConnectFailed`
//! (ADR-006 decision 1: accept only after the fabric open
//! succeeds), the channel never establishes, and *no `channel_close`
//! can ever fire* — so the count must be released from where the
//! failure is known: inside the spawned bridge task, immediately
//! after the reject is answered. A success never releases there
//! (its close rides path 1), so a failed bridge cannot leak budget
//! and cannot double-release.
//! succeeds), the channel never establishes, and *no
//! `channel_close` can ever fire* — so the count must be released
//! from where the failure is known: inside the spawned bridge
//! task, immediately after the reject is answered. A success never
//! releases there (its close rides path 1).
//! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric
//! arrival is admitted into the owning route's count and opens the
//! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward
//! the vanilla client through the session handle. A rejected open —
//! the client's own refusal, or a dead session's send failure —
//! never establishes the channel, so the count releases inside the
//! arrival task exactly once (an established channel's release is
//! path 1's). These are the mirror of site 2 on the server-initiated
//! open direction — the acceptor vocabulary's cap rows.
//!
//! ## Why the mutex (the single-writer reasoning, updated)
//! ## Why the mutex (the single-writer reasoning, updated twice)
//!
//! The wave-2 shape was a bare `usize` field written only by the
//! session's packet loop — sound because every hook runs inline on
//! that loop with exclusive `&mut self` access. The bridge release
//! breaks the single-writer property: the spawned task owns the
//! failure answer and the packet loop cannot be reached from it. The
//! recorded decision is a `parking_lot::Mutex<usize>`: **admissions
//! stay packet-loop-only** (every `admit` call site is an
//! `channel_open_*` hook — the cap check and the increment are one
//! atomic section against a shared writer), while **releases have the
//! two sites above** (packet loop + spawned bridge tasks). The mutex
//! is poisoned-lock-resistant (`unwrap_or_else(|e| e.into_inner())` —
//! AGENTS.md convention 2: a panic in one operation must not cascade
//! to other operations), and both release sites saturate
//! (`saturating_sub`), so no path can manufacture capacity with a
//! stray decrement. Channels never closed before session teardown
//! stay counted until the handler dies with the session — per-session
//! state, gone with the connection.
//! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`)
//! both break the single-writer property: those tasks are alkcall
//! serving tasks, unreachable from the packet loop. The recorded
//! decision is a `parking_lot::Mutex<usize>`: **packet-loop hooks and
//! spawned tasks both touch the count only through the mutex**, their
//! critical sections are single integer ops (no await inside the
//! lock), and all sites saturating. Channels never closed before
//! session teardown stay counted until the handler dies with the
//! session — per-session state, gone with the connection.
use parking_lot::Mutex;
@@ -76,22 +80,15 @@ pub(crate) struct ChannelCap {
}
impl ChannelCap {
/// Whether the cap has room — the check form for the rows whose
/// decline is ordered *before* the accept decision without
/// consuming a count: the `forwarded-tcpip` stub (the `-R` data
/// leg's flow is wave-3 material — a declined stub opens nothing,
/// so it must not consume an admission). The accept paths use
/// [`ChannelCap::admit`]; a plain check never mutates.
pub(crate) fn has_room(&self, cap: usize) -> bool {
*self.active.lock() < cap
}
/// The accept-path admission decision: room under `cap` increments
/// the count and admits; otherwise refuses (fail-closed — the
/// caller answers the open with a clean decline). `cap == 0`
/// refuses every open. The caller site is the packet loop's open
/// hook (the single admit writer — the held count is what the
/// spawned bridge holds across the fabric open).
/// refuses every open. Call sites: the packet loop's open hooks
/// `w2`-style, and — since `w3-tunnel-listen` — the `-R` arrival
/// tasks (the door's forwarded-arrival admission: an arrival is
/// admitted on the owning route's count inside the spawned
/// arrival pump — see the module doc's updated single-writer
/// reasoning). The mutex makes both sound.
pub(crate) fn admit(&self, cap: usize) -> bool {
let mut active = self.active.lock();
if *active < cap {
@@ -134,7 +131,6 @@ mod tests {
assert!(cap.admit(CAP));
assert!(cap.admit(CAP));
assert!(!cap.admit(CAP), "beyond the cap: refused");
assert!(!cap.has_room(CAP));
assert_eq!(cap.active(), CAP);
}
@@ -148,7 +144,6 @@ mod tests {
assert!(!cap.admit(CAP));
cap.release();
assert!(cap.has_room(CAP));
assert!(cap.admit(CAP), "an open succeeds again after a close");
assert!(!cap.admit(CAP));
}
+138 -7
View File
@@ -31,9 +31,15 @@
//! decline even though the `session` feature is on;
//! [`ServeConfig::git_seam`] unset → the git row declines even
//! though `git` is on (the decline matrix's config-driven notes;
//! the feature alone configures nothing). The tunnel handle stays
//! the flat `Option<Arc<ChannelClient>>` as landed — a single-field
//! seam needs no struct.
//! the feature alone configures nothing). The tunnel branch carries
//! its flat `Option<Arc<ChannelClient>>` — an egress-only
//! (route-1-only) door needs no struct for leg 1 — and the `-R`
//! listen leg's registration seam landed with `w3-tunnel-listen`
//! (`ServeConfig::tunnel_listen_seam` — the wave-3 seam-config's
//! second course-correction; the shape decision recorded on
//! [`TunnelListenSeam`]: unset → the listen rows decline even with
//! a fabric leg present, because the door-side inbound-op serving
//! loop only exists where the assembly wired it).
//! - **Injected handles, never crate-resolved identity or trust**:
//! the [`IdentityProvider`], [`HostKeySource`] and
//! [`HostKeyPolicy`] handles are `Arc<dyn ..>` the assembly
@@ -61,6 +67,8 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "tunnel")]
use crate::tunnel_map::ListenRouter;
use alkcall::channels::client::ChannelClient;
use alkcall::core::auth::IdentityProvider;
#[cfg(feature = "session")]
@@ -298,6 +306,69 @@ impl std::fmt::Debug for GitSeam {
}
}
/// The `tunnel` feature's `-R` listen-leg registration seam (the
/// `w3-tunnel-listen` shape — the wave-3 seam-config's second
/// course-correction, resolved per its own decision record): the
/// assembly-supplied `Arc<ChannelClient>` alone cannot carry the
/// door-side acceptance of the fabric's inbound forwarded opens,
/// because alkcall serves those opens on the **door's channel-0
/// serving loop**, which exists only when the consumer constructed its
/// `ChannelClient` with
/// [`ChannelClient::from_connection_with_serving`] against a registry
/// the assembly owns. The published alkcall surface has no
/// post-construction way to add that loop, so the serve-config's
/// tunnel handle grows this seam.
///
/// Same whole-`Option`-struct decision as [`SessionSeam`]/[`GitSeam`]:
/// **`ServeConfig::tunnel_listen_seam` unset → the `-R` listen rows
/// (`tcpip_forward` / `streamlocal_forward` / both cancel forms)
/// decline even under the `tunnel` feature with a fabric leg present**
/// — an egress-only door (the POC-3 stage-C posture) is legitimate
/// config; the feature alone configures nothing. Checked at request
/// time by the mappings; no construction-time validation. The
/// registration itself (the alkcall openable registration the seam
/// fronts) is alkssh-side work at [`crate::SshAdapter::new`] — a
/// failed registration is a failed construction (fail-closed, never a
/// half-configured adapter).
#[cfg(feature = "tunnel")]
pub struct TunnelListenSeam {
/// The **shared** per-connection `OperationRegistry` the fabric
/// leg's inbound ops dispatch against: the exact `Arc` the
/// assembly passed in this leg's `ChannelClient` construction
/// (`ServingConfig.registry`) — alkssh registers the
/// `tunnel/forwarded` openable into it, and alkcall's dispatch
/// reads it per call (the W2 post-hoc-registration posture). The
/// door's own ACL entry rides the registration; arrivals lacking
/// the `tunnel:forwarded` scope are gated by alkcall's
/// `AccessControl::check` fail-closed.
pub registry: Arc<OperationRegistry>,
}
/// The `OperationRegistry` type behind the seam's shared dispatcher.
/// (Re-imported here for the field's shape; the registration work
/// lives in [`crate::tunnel_map`].)
#[cfg(feature = "tunnel")]
use alkcall::registry::registration::OperationRegistry;
#[cfg(feature = "tunnel")]
impl TunnelListenSeam {
/// Builds the seam over the assembly's shared per-connection
/// registry (the `ServingConfig.registry` the same fabric leg's
/// `ChannelClient` was constructed with — one `Arc`, both sides).
pub fn new(registry: Arc<OperationRegistry>) -> Self {
Self { registry }
}
}
#[cfg(feature = "tunnel")]
impl std::fmt::Debug for TunnelListenSeam {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TunnelListenSeam")
.field("registry", &"<shared OperationRegistry>")
.finish()
}
}
/// The server direction's assembly-supplied serving configuration
/// (`overview.md` §Server stack): everything wave-2's serving loop
/// takes at construction. Pure data + injected handles — no russh
@@ -376,6 +447,20 @@ pub struct ServeConfig {
/// 2: absent + unfeatured both decline; present + featured
/// serves.
pub channel_client: Option<Arc<ChannelClient>>,
/// The `-R` listen leg's registration seam ([`TunnelListenSeam`] —
/// the shared per-connection registry the fabric's inbound
/// forwarded opens dispatch against). **Unset → the listen rows
/// decline even under the `tunnel` feature with a fabric leg
/// present** (the egress-only door is legitimate config — the
/// POC-3 stage-C posture; shape decision recorded on the type).
#[cfg(feature = "tunnel")]
pub tunnel_listen_seam: Option<TunnelListenSeam>,
/// The listen leg's per-arrival bookkeeping — installed by
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
/// present (the registration site). Crate-private: never assembly
/// data.
#[cfg(feature = "tunnel")]
pub(crate) listen_router: Option<Arc<ListenRouter>>,
/// The bounded-resources budget this serving direction enforces
/// (see [`Limits`]).
pub limits: Limits,
@@ -404,6 +489,10 @@ impl ServeConfig {
identity_provider,
host_key_source,
channel_client: None,
#[cfg(feature = "tunnel")]
tunnel_listen_seam: None,
#[cfg(feature = "tunnel")]
listen_router: None,
limits: Limits::default(),
}
}
@@ -431,6 +520,16 @@ impl ServeConfig {
self.channel_client.is_some()
}
/// Whether the `-R` listen rows' registration seam is configured
/// in this configuration ([`TunnelListenSeam`] present; the
/// `tunnel` feature gate applies at the dispatch on top): `false`
/// means the listen rows decline — the unset-declines contract,
/// checked at request time by the mappings, never here.
#[cfg(feature = "tunnel")]
pub fn tunnel_listen_seam_configured(&self) -> bool {
self.tunnel_listen_seam.is_some()
}
/// Whether the session rows' family seam is configured in this
/// configuration ([`SessionSeam`] present; the `session` feature
/// gate applies at the dispatch on top): `false` means the rows
@@ -470,10 +569,12 @@ impl std::fmt::Debug for ServeConfig {
{
debug.field("git_seam", &self.git_seam.is_some());
}
debug
.field("channel_client", &self.channel_client.as_ref().is_some())
.field("limits", &self.limits)
.finish()
debug.field("channel_client", &self.channel_client.as_ref().is_some());
#[cfg(feature = "tunnel")]
{
debug.field("tunnel_listen_seam", &self.tunnel_listen_seam.is_some());
}
debug.field("limits", &self.limits).finish()
}
}
@@ -982,6 +1083,36 @@ mod tests {
}
}
/// - *Contract stamp:* the `w3-tunnel-listen` seam shape (the
/// unset-declines contract's tunnel extension): the seam starts
/// unset (the listen rows decline — an egress-only door is
/// legitimate config), sets independently of the fabric handle,
/// and Debug-shapes without registry internals.
#[cfg(feature = "tunnel")]
#[test]
fn serve_config_listen_seam_unset_declines_and_sets_independently() {
let serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source());
assert!(serve.tunnel_listen_seam.is_none());
assert!(!serve.tunnel_listen_seam_configured());
assert!(!serve.tunnel_configured());
let debug = format!("{serve:?}");
assert!(debug.contains("tunnel_listen_seam: false"));
let mut serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source());
serve.tunnel_listen_seam = Some(super::super::TunnelListenSeam::new(Default::default()));
assert!(serve.tunnel_listen_seam_configured());
let debug = format!("{serve:?}");
assert!(debug.contains("tunnel_listen_seam: true"));
let seam_debug = format!("{:?}", serve.tunnel_listen_seam.as_ref().expect("holds"));
assert!(
seam_debug.contains("<shared OperationRegistry>"),
"the seam Debug shapes the registry without internals: {seam_debug}"
);
}
#[tokio::test]
async fn dial_config_key_source_stays_fail_closed_through_the_config() {
let dial = DialConfig::new("fleet.example:22", key_source(), Arc::new(RejectAll));
+30 -12
View File
@@ -104,15 +104,19 @@ pub enum KeySourceError {
Decode(String),
}
/// The adapter (server-direction) error surface — the two failure
/// classes the mount point can reach, deliberately closed:
/// The adapter (server-direction) error surface — the failure classes
/// the mount point can reach, deliberately closed:
///
/// - **Construction** — the [`SshAdapter::new`](crate::SshAdapter::new)
/// host-key load failed: the [`KeySourceError`] variant is preserved
/// verbatim (locked / absent / decryption / unknown-version / decode
/// each stays itself) and **no adapter is constructed** (ADR-005
/// decision 6 — never a half-configured adapter, never a keyless
/// server that fails later).
/// setup failed *before any session exists*: a failed host-key load
/// (the [`KeySourceError`] variant is preserved verbatim — locked /
/// absent / decryption / unknown-version / decode each stays itself)
/// or a refused `-R` listen-leg registration (`ListenRegistration`,
/// `w3-tunnel-listen` — a fabric that could mint listens the door
/// cannot serve is worse than a half-configured adapter).
/// **No adapter is constructed** (ADR-005 decision 6 — never a
/// half-configured adapter, never a keyless server that fails
/// later).
/// - **Wire** — the russh session stack's own error, mapped
/// transparently (`From<russh::Error>`, the wave-1 error-model's
/// reserved `From` requirement of the per-connection
@@ -122,7 +126,7 @@ pub enum KeySourceError {
/// dispatch's SSH-shaped responses, never this taxonomy.
///
/// There is no catch-all variant: everything the crate's server
/// direction can fail is one of these two classes or a decline.
/// direction can fail is one of these classes or a decline.
#[derive(Debug, Error)]
pub enum AdapterError {
/// The host-key load at adapter construction refused — fail-closed
@@ -131,6 +135,14 @@ pub enum AdapterError {
#[error("ssh server construction refused: {0}")]
Construction(#[from] KeySourceError),
/// The `-R` listen leg's door-side openable registration refused
/// at construction (`w3-tunnel-listen`): fail-closed, never a
/// half-configured adapter — a construction where the fabric could
/// mint listens the door cannot serve is worse than no adapter.
/// The string carries the registration failure detail.
#[error("ssh server construction refused (listen registration): {0}")]
ListenRegistration(String),
/// A mounted russh session errored (transport / KEX / IO class).
#[error("ssh session error: {0}")]
Wire(#[from] russh::Error),
@@ -209,21 +221,27 @@ mod tests {
.into_iter()
.map(AdapterError::Construction)
.collect();
all.push(AdapterError::ListenRegistration("duplicate".into()));
all.push(AdapterError::Wire(russh::Error::UnknownKey));
all
}
#[test]
fn adapter_error_closed_taxonomy_two_classes() {
let exhaustive: fn(&AdapterError) -> &'static str = |e| match e {
AdapterError::Construction(_) => "construction",
AdapterError::Wire(_) => "wire",
};
let exhaustive: fn(&AdapterError) -> &'static str = match_fn();
for v in all_adapter_variants() {
assert!(!exhaustive(&v).is_empty());
}
}
fn match_fn() -> fn(&AdapterError) -> &'static str {
|e| match e {
AdapterError::Construction(_) => "construction",
AdapterError::ListenRegistration(_) => "construction",
AdapterError::Wire(_) => "wire",
}
}
#[test]
fn construction_preserves_the_key_source_variant() {
for source in all_variants() {
+350 -79
View File
@@ -56,8 +56,12 @@
//! only after the fabric open succeeds, `ConnectFailed` on failure
//! ([`crate::dispatch::connect_failed_open_decline`], the wave-2
//! seam stub's reason surviving as the real decline).
//! `forwarded-tcpip` stays the stub until `w3-tunnel-listen` lands
//! the `-R` data leg's flow (the mapping row's listen-flow shape).
//! An **inbound** `forwarded-tcpip` open — arriving *from* the vanilla
//! client — declines with the same explicit `ConnectFailed` as the
//! unvanilla-arrival class (`w3-tunnel-listen`: the real `-R` data
//! leg is the door's own server-initiated opens, driven by the
//! [`crate::tunnel_map`] arrival pump; a vanilla client never
//! initiates the type, OpenSSH parity).
//! The bare
//! `auth-agent@openssh.com` channel *type*
//! (`forwarded-streamlocal@openssh.com` likewise) has no russh handler
@@ -154,7 +158,14 @@
//! the open *before* any channel is owned — nothing is served yet,
//! so nothing spawns. The route-1 bridge (`w3-tunnel-direct`) moved
//! the direct rows' accept past the fabric open: the accept and the
//! pump ride the spawned task, per the same constraint.
//! pump ride the spawned task, per the same constraint. The `-R`
//! listen rows' family-ward dial is the one **inline await** on the
//! loop (`w3-tunnel-listen`'s recorded decision: the global request's
//! reply + the `&mut u32` bound-port out-param are the hook's own —
//! no spawned-task answer exists at this pin — and the fabric's
//! answer rides the fabric leg's connections, not this session's
//! routing; bounded by `LISTEN_DIAL_TIMEOUT`), while everything under
//! the reply — the arrivals, the pumps — stays spawned.
//!
//! Request-level declines: `subsystem_request` declines every name —
//! `sftp` is the OQ-13 placeholder (the feature exists, the route
@@ -164,9 +175,11 @@
//! declines unconditionally (OQ-09 — no allowlist ships: wire env is
//! not family config). `x11_request` and `agent_request` decline
//! (ADR-003 §5). The global listen requests (`tcpip_forward`
//! / `streamlocal_forward` / the cancel forms) decline — they are
//! `tunnel` rows whose serving flow is wave-3 material, so featured
//! builds decline them too this wave.
//! / `streamlocal_forward` + their cancel forms) — the `-R` rows
//! (`w3-tunnel-listen`): the real flow under `tunnel` with the listen
//! seam configured (the family-ward `TunnelListener::open` dial, the
//! arrivals' arrival pump through the shared router); every decline
//! class keeps a global-request-level reason (see the hooks).
//!
//! ## Wire decline reasons (the wave-1 error-model reservation)
//!
@@ -179,7 +192,8 @@
//! prohibited; the direct rows' cannot-bridge classes (the fabric
//! entry unconfigured under the feature, the failed fabric open, an
//! impossible wire target), the off-Unix `direct-streamlocal`
//! decline, and the `forwarded-tcpip` stub → connect failed). These
//! decline, and the unvanilla inbound `forwarded-tcpip` open →
//! connect failed). These
//! happen before the channel exists, so the SSH open-failure code is
//! the honest, richest reason surface.
//! - **Request-level declines** — [`Session::channel_failure`]: the
@@ -233,7 +247,14 @@
//! spawned task on a fabric-open failure (the admitted-but-rejected
//! shape: ADR-006 decision 1 rejects the open, so no `channel_close`
//! can ever fire for it; the release mechanism is the shared
//! [`ChannelCap`] `Arc`, recorded in that module). The order of the
//! [`ChannelCap`] `Arc`, recorded in that module). The `-R` leg adds
//! its two budget surfaces (`w3-tunnel-listen`, recorded in
//! `channel_cap.rs`): the arrivals admit into the owning route's
//! count inside the spawned arrival tasks (a beyond-cap arrival never
//! opens an SSH channel — teardown-at-birth), and the listen count is
//! its own dedicated counter against the same ceiling (the
//! listener/channels mix stays unconstrained; both stay bounded). The
//! order of the
//! gates: the feature gate first (an unfeatured build declines
//! everything before the cap matters), then the pure-decline classes
//! (the tunnel rows' unset-handle/unix-gate/identity/invalid-target
@@ -277,7 +298,7 @@ use crate::fingerprint::fingerprint;
#[cfg(feature = "git")]
use crate::git_map;
use crate::observe;
#[cfg(any(feature = "git", feature = "session"))]
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
use crate::serve;
#[cfg(feature = "session")]
use crate::tty_map;
@@ -298,14 +319,30 @@ use alktty::negotiation::NegotiateRequest;
/// [`Self::resolved_identity`] — the wave-3 seam reads it).
pub struct SessionHandler {
serve: Arc<ServeConfig>,
/// The in-crate concurrent-channel cap (`ChannelCap`), shared as
/// an `Arc` since `w3-tunnel-direct`: the route-1 bridge's spawned
/// task releases the count on a fabric-open failure (the
/// The in-crate concurrent-channel cap (`w2-bounded-resources`),
/// shared as an `Arc` since `w3-tunnel-direct`: the route-1 bridge's
/// spawned task releases the count on a fabric-open failure (the
/// admitted-but-rejected shape — no `channel_close` can ever fire
/// for it; see `channel_cap.rs`'s release-sites record). Admits
/// stay packet-loop-only (every `admit` call site is an open
/// hook); the mutex makes the bridge-task releases sound.
/// for it; see `channel_cap.rs`'s release-sites record). Admissions
/// have two sites since `w3-tunnel-listen` — the packet loop's
/// open hooks and the `-R` arrival tasks — and releases three; the
/// mutex makes them sound (the reasoning, recorded in
/// `channel_cap.rs`).
channel_cap: Arc<ChannelCap>,
/// The `-R` listen leg's per-session bookkeeping
/// (`w3-tunnel-listen`): the minted-listen map (token → pending
/// listen; written only by the packet loop's hooks — the listen
/// global requests mints, the cancel forms + this handler's
/// teardown remove, single-writer/no-lock like the `channels`
/// store) and the listen count's dedicated
/// `max_concurrent_channels`-ceiling counter (a listen is at least
/// one future channel's potential; its own counter keeps the
/// listener/channels mix unconstrained while both stay bounded —
/// no unbounded listener map).
#[cfg(feature = "tunnel")]
listeners: HashMap<u32, tunnel_map::PendingListen>,
#[cfg(feature = "tunnel")]
listener_cap: tunnel_map::ListenerCap,
resolved_identity: Option<Identity>,
/// The store-and-take handoff (`w3-git-mapping`): confirmed
/// session channels held from the open until their row takes or
@@ -334,14 +371,28 @@ pub struct SessionHandler {
teardown: tty_map::Teardown,
}
#[cfg(feature = "session")]
impl Drop for SessionHandler {
/// Fires the session rows' teardown signal (`w3-tty-session`):
/// every spawned serving task of this connection watches it and
/// cancels its alktty session (the kill-on-drop cleanup) instead
/// of outliving the connection.
/// of outliving the connection. The `tunnel` build adds the `-R`
/// leg's teardown (`w3-tunnel-listen`): the session's pending
/// listens' tokens leave the shared router (a fabric arrival after
/// the session is gone is the correlation-miss class —
/// teardown-at-birth) and the listener objects drop here (the
/// fabric-side reap). Any in-flight arrival already past the
/// route lookup fails its dead-handle open and releases its
/// admission — the never-established path.
fn drop(&mut self) {
#[cfg(feature = "session")]
self.teardown.fire();
#[cfg(feature = "tunnel")]
{
let tokens: Vec<u32> = self.listeners.keys().copied().collect();
if let Some(router) = &self.serve.listen_router {
router.remove_tokens(&tokens);
}
}
}
}
@@ -362,6 +413,10 @@ impl SessionHandler {
bridges: HashMap::new(),
#[cfg(feature = "session")]
teardown: tty_map::Teardown::new(),
#[cfg(feature = "tunnel")]
listeners: HashMap::new(),
#[cfg(feature = "tunnel")]
listener_cap: tunnel_map::ListenerCap::default(),
}
}
@@ -405,19 +460,6 @@ impl SessionHandler {
.admit(self.serve.limits.max_concurrent_channels)
}
/// Whether the cap is already exhausted — the gate slot for the
/// one row whose decline is still ordered *before* an accept
/// decision without consuming an admission: the `forwarded-tcpip`
/// stub (the `-R` data leg's flow is `w3-tunnel-listen` material —
/// a declined stub opens nothing, so it must not consume a count;
/// the direct rows moved to [`Self::admit_channel_open`]-at-the-
/// accept flow when `w3-tunnel-direct` landed the route-1 bridge).
fn channel_cap_exhausted(&self) -> bool {
!self
.channel_cap
.has_room(self.serve.limits.max_concurrent_channels)
}
/// The ADR-004 decision 1 bridge core, structured out of the
/// [`Handler::auth_publickey`] hook so the contract tests can drive
/// it directly: the hook's russh-owned argument shape
@@ -690,12 +732,16 @@ impl russh::server::Handler for SessionHandler {
Ok(())
}
/// The `forwarded-tcpip` open gate (`tunnel` — the `-R` data
/// leg): the same gate/cap/stub split as `direct-tcpip`. The
/// harness's russh client cannot initiate this channel type (the
/// server opens it toward the client), so its wire rows are
/// wave-3 acceptor material; the gate itself is straight-line
/// identical to the other tunnel rows.
/// The `forwarded-tcpip` open gate (`tunnel`): an **inbound** open
/// of this channel type — arriving *from* the vanilla client — is
/// not the `-R` leg at all (the `-R` data leg is the door's own
/// server-initiated opens via [`crate::tunnel_map`]'s arrival
/// pump; a vanilla client never initiates `forwarded-tcpip`,
/// OpenSSH parity). The gate + the unvanilla decline stay
/// straight-line and inline: nothing serves, nothing was ever in
/// flight, so no admission is touched, and the explicit
/// `ConnectFailed` reject keeps the wave-2 wire shape (the event's
/// class records it as the unvanilla-arrival decline).
async fn channel_open_forwarded_tcpip(
&mut self,
channel: Channel<Msg>,
@@ -712,12 +758,8 @@ impl russh::server::Handler for SessionHandler {
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.channel_cap_exhausted() {
observe::decline_open(ROW, "cap-exceeded", channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
return Ok(());
}
tunnel_row_stub(ROW, channel.id().number(), reply).await;
observe::decline_open(ROW, "unvanilla-inbound", channel.id().number());
reply.reject(dispatch::connect_failed_open_decline()).await;
Ok(())
}
@@ -1098,57 +1140,297 @@ impl russh::server::Handler for SessionHandler {
Ok(false)
}
/// The `tcpip-forward` listen request (`-R` ingress): a `tunnel`
/// row whose serving flow is wave-3 material — the gate exists
/// ([`Self::serves_tunnels`]) but even a featured build declines
/// this wave (never advertise a listen that cannot serve). The
/// bound-port `port = 0` picking rule rides the wave-3 flow.
/// The `tcpip-forward` listen request (`-R` ingress, `w3-tunnel-
/// listen`): the gate is [`Self::serves_tunnels`]; the real flow
/// rides [`listen_request_dispatch`] (`tunnel` builds) — the
/// family-ward `TunnelListener::open` dial, the bound-port out-param
/// (`port = 0` → the fabric's actually-bound number, the sshd
/// parity), the route + listener registration. Unfeatured builds
/// decline with the global-request class; never a minted listen a
/// door cannot serve.
async fn tcpip_forward(
&mut self,
_address: &str,
_port: &mut u32,
address: &str,
port: &mut u32,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("tcpip_forward", "unimplemented");
Ok(false)
#[cfg(feature = "tunnel")]
{
listen_request_dispatch(
self,
"tcpip_forward",
address,
alktunnels::params::Substrate::Tcp,
Some(port),
_session,
)
.await
}
#[cfg(not(feature = "tunnel"))]
{
let _ = (address, port);
observe::decline_global_request("tcpip_forward", "unfeatured");
Ok(false)
}
}
/// Nothing is ever registered this wave — the cancel form
/// declines accordingly.
/// The `cancel-tcpip-forward` cancel form (`-KR`, mux teardown,
/// `ExitOnForwardFailure`): 1:1 into the family listener's teardown —
/// the pending listen matched by (the wire address as the request
/// carried it, the bound port), the route removed **before** any
/// teardown (a racing arrival is already the correlation-miss
/// class), the listener count released, the fabric channel's
/// teardown spawned (`TunnelListener::close`). `false` for a
/// (address, port) pair with no pending listen — the sshd parity
/// (`cancel of a not-found forward`).
async fn cancel_tcpip_forward(
&mut self,
_address: &str,
_port: u32,
address: &str,
port: u32,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("cancel_tcpip_forward", "unimplemented");
Ok(false)
#[cfg(feature = "tunnel")]
{
cancel_listen_dispatch(self, "cancel_tcpip_forward", address, Some(port))
}
#[cfg(not(feature = "tunnel"))]
{
let _ = (address, port);
observe::decline_global_request("cancel_tcpip_forward", "unfeatured");
Ok(false)
}
}
/// The `streamlocal-forward` listen request: the same decline as
/// `tcpip-forward`, plus its runtime-Unix gate (OQ-11) riding the
/// wave-3 serving flow it will gate.
/// The `streamlocal-forward` listen request: the same listen flow
/// as `tcpip_forward` over the unix substrate — with the OQ-11
/// runtime-Unix gate riding the serving flow it serves (off-Unix
/// it declines before any fabric call; universal compile, no
/// per-platform feature fork). No port out-param (the unix listen
/// carries no port semantics; the path echoes verbatim).
async fn streamlocal_forward(
&mut self,
_socket_path: &str,
socket_path: &str,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("streamlocal_forward", "unimplemented");
Ok(false)
#[cfg(feature = "tunnel")]
{
if !cfg!(unix) {
observe::decline_global_request("streamlocal_forward", "unix-unavailable");
return Ok(false);
}
listen_request_dispatch(
self,
"streamlocal_forward",
socket_path,
alktunnels::params::Substrate::Unix,
None,
_session,
)
.await
}
#[cfg(not(feature = "tunnel"))]
{
let _ = socket_path;
observe::decline_global_request("streamlocal_forward", "unfeatured");
Ok(false)
}
}
/// Nothing is ever registered this wave — the cancel form
/// declines accordingly.
/// The `cancel-streamlocal-forward` form: the tcpip cancel's shape
/// over the unix substrate (path-match only; no port).
async fn cancel_streamlocal_forward(
&mut self,
_socket_path: &str,
socket_path: &str,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("cancel_streamlocal_forward", "unimplemented");
Ok(false)
#[cfg(all(feature = "tunnel", unix))]
{
cancel_listen_dispatch(self, "cancel_streamlocal_forward", socket_path, None)
}
#[cfg(all(feature = "tunnel", not(unix)))]
{
let _ = socket_path;
observe::decline_global_request("cancel_streamlocal_forward", "unix-unavailable");
Ok(false)
}
#[cfg(not(feature = "tunnel"))]
{
let _ = socket_path;
observe::decline_global_request("cancel_streamlocal_forward", "unfeatured");
Ok(false)
}
}
}
/// The `tcpip_forward` / `streamlocal_forward` global requests' real
/// flow (`w3-tunnel-listen`; `mapping.md` §Global-request rows): the
/// gate order is decline-first-alloc-never, each consumer checked
/// before the next allocation is taken:
///
/// 1. **The `no-identity` guard** — unreachable on the wire (russh
/// authorizes before requests serve) and fail-closed: never
/// anonymous listens.
/// 2. **The seam checks** — the fabric handle or the listen seam
/// (`listen_router` absent: an egress-only door — the seam unset or
/// the registration never ran) decline `seam-unconfigured` before
/// any dial.
/// 3. **The OQ-11 unix gate** — the streamlocal variant's off-Unix
/// decline rides its hook (checked before this fn runs, so the
/// unreachable-on-Unix-only row never here).
/// 4. **The listener cap** — the listen count's dedicated
/// `max_concurrent_channels`-ceiling admission; beyond it
/// `cap-exceeded`.
///
/// Then the family-ward dial ([`crate::tunnel_map::open_listen`], the
/// `resource = <the wire bind endpoint>` naming convention) — awaited
/// inline (the recorded inline decision: the global request's reply +
/// the `&mut u32` out-param are the hook's; the fabric answer rides
/// the fabric leg, not this session's packet loop; the dial is
/// bounded by `LISTEN_DIAL_TIMEOUT`, so the stall is bounded). The
/// reply's `bound` port writes the out-param (`port = 0` → the
/// actually-bound number, sshd parity); the route registers into the
/// shared router (session-scope: the owning session's handle + its
/// channel-cap `Arc`) and the pending listen into the session's
/// packet-loop-only map. `Ok(true)`'s answer is the only success
/// shape; every decline path answers the vanilla client's
/// `RequestDenied` via `Ok(false)`.
#[cfg(feature = "tunnel")]
async fn listen_request_dispatch(
handler: &mut SessionHandler,
row: &'static str,
wire_endpoint: &str,
substrate: alktunnels::params::Substrate,
port: Option<&mut u32>,
session: &mut Session,
) -> Result<bool, AdapterError> {
if handler.resolved_identity().is_none() {
observe::decline_global_request(row, "no-identity");
return Ok(false);
}
let (Some(client), Some(router)) = (
handler.serve.channel_client.as_ref(),
handler.serve.listen_router.as_ref(),
) else {
observe::decline_global_request(row, "seam-unconfigured");
return Ok(false);
};
if !handler
.listener_cap
.admit(handler.serve.limits.max_concurrent_channels)
{
observe::decline_global_request(row, "cap-exceeded");
return Ok(false);
}
let minted = match tunnel_map::open_listen(
client,
tunnel_map::listen_resource(wire_endpoint, substrate, port.as_deref().copied()),
substrate,
)
.await
{
Ok(minted) => minted,
Err(_) => {
handler.listener_cap.release();
observe::decline_global_request(row, "listen-failed");
return Ok(false);
}
};
let bound = minted.bound;
let echo = match (&substrate, &bound) {
(
alktunnels::params::Substrate::Tcp,
alktunnels::params::SubstrateAddr::HostPort {
port: bound_port, ..
},
) => tunnel_map::RouteEcho::Tcp {
address: wire_endpoint.to_owned(),
port: u32::from(*bound_port),
},
(alktunnels::params::Substrate::Unix, alktunnels::params::SubstrateAddr::Path { path }) => {
tunnel_map::RouteEcho::Unix { path: path.clone() }
}
_ => {
handler.listener_cap.release();
observe::decline_global_request(row, "listen-failed");
return Ok(false);
}
};
if let (Some(port), Some(bound_port)) = (port, tunnel_map::bound_port(&bound)) {
*port = bound_port;
}
let token = minted.token;
router.register(
token,
tunnel_map::ListenRoute {
echo,
substrate,
handle: Arc::new(session.handle()),
cap: Arc::clone(&handler.channel_cap),
cap_value: handler.serve.limits.max_concurrent_channels,
},
);
handler.listeners.insert(
token,
tunnel_map::PendingListen {
wire_address: wire_endpoint.to_owned(),
bound_port: tunnel_map::bound_port(&bound).unwrap_or(0),
substrate,
listener: minted.listener,
},
);
Ok(true)
}
/// The `cancel_tcpip_forward` / `cancel_streamlocal_forward` global
/// requests' real flow (`w3-tunnel-listen`): 1:1 into the family
/// listener's teardown. Match rule (sshd parity): the wire endpoint as
/// the original request carried it + the bound port for the tcp
/// variants (the client sends the same normalized string it sent
/// originally, and the *allocated* port for `port = 0` mints — the
/// value the listen reply carried); the path alone for the unix
/// variant. Order: the route leaves the shared router **before** the
/// entry leaves the session map (so a racing arrival is already the
/// correlation-miss class — no post-cancel half-serve), then the
/// listener count releases, then the family teardown spawns
/// (`TunnelListener::close` inside the spawn — a bounded local
/// shutdown + entry reap, not a serving path the hook may await).
/// `Ok(true)` iff a pending listen matched.
#[cfg(feature = "tunnel")]
fn cancel_listen_dispatch(
handler: &mut SessionHandler,
row: &'static str,
wire_endpoint: &str,
port: Option<u32>,
) -> Result<bool, AdapterError> {
if handler.resolved_identity().is_none() {
observe::decline_global_request(row, "no-identity");
return Ok(false);
}
let token = handler.listeners.iter().find_map(|(token, pending)| {
let port_matched = match port {
Some(port) => pending.bound_port == port,
None => pending.substrate == alktunnels::params::Substrate::Unix,
};
(pending.wire_address == wire_endpoint && port_matched).then_some(*token)
});
let Some(token) = token else {
observe::decline_global_request(row, "no-such-listen");
return Ok(false);
};
let Some(entry) = handler.listeners.remove(&token) else {
observe::decline_global_request(row, "no-such-listen");
return Ok(false);
};
if let Some(router) = &handler.serve.listen_router {
router.cancel(&token);
}
handler.listener_cap.release();
serve::spawn_serving_path(async move {
entry.listener.close().await;
});
Ok(true)
}
/// The request-level decline shape for an unfeatured/unimplemented
/// capability: fail the request and leave the channel usable (POC-1
/// §5 — session survives declines, no state poisoning). The decline
@@ -1440,14 +1722,3 @@ async fn direct_open_dispatch(
Arc::clone(&handler.channel_cap),
);
}
/// The `forwarded-tcpip` seam stub — the `-R` data leg's decline until
/// `w3-tunnel-listen` lands the listen flow this leg rides (the
/// harness's russh client cannot even initiate this channel type —
/// the server opens it toward the client — so its wire rows are
/// acceptor material). The open-level decline event is emitted here:
/// the explicit `ConnectFailed` reject ADR-006 decision 1 pins.
async fn tunnel_row_stub(row: &'static str, channel_id: u32, reply: ChannelOpenHandle) {
observe::decline_open(row, "connect-failed", channel_id);
reply.reject(dispatch::connect_failed_open_decline()).await;
}
+2
View File
@@ -47,6 +47,8 @@ pub use adapter::SshAdapter;
pub use config::GitSeam;
#[cfg(feature = "session")]
pub use config::SessionSeam;
#[cfg(feature = "tunnel")]
pub use config::TunnelListenSeam;
pub use config::{DialConfig, Limits, ServeConfig};
pub use error::{AdapterError, KeySourceError};
pub use fingerprint::fingerprint;
+59 -3
View File
@@ -55,7 +55,8 @@
//!
//! Decline events additionally carry exactly:
//!
//! - `decline_class` — `"open"` | `"request"` | `"serving"` | `"auth"`:
//! - `decline_class` — `"open"` | `"request"` | `"serving"` |
//! `"arrival"` | `"auth"`:
//! the two wire decline classes of `mapping.md` §Wire decline
//! reasons, plus the serving-path class (this wave the seam stub's
//! decline *is* the serving path; the wave-3 mappings emit the same
@@ -93,8 +94,16 @@
//! path guards — unreachable in practice, fail-closed; the tunnel
//! rows' open-level `no-identity` guard rides the same value —
//! reachable never, on the wire, for the same reason),
//! `"unimplemented"` (the subsystem rows — OQ-13 — and the wave-3
//! listen forms), `"declined-by-design"` (env, OQ-09), and on the
//! `"unimplemented"` (the subsystem rows — OQ-13),
//! `"declined-by-design"` (env, OQ-09),
//! `"correlation-miss"` / `"cap-exceeded"` / `"accept-refused"` /
//! `"unvanilla-inbound"` (`w3-tunnel-listen`: the `-R` leg's arrival
//! declines — the class whose events carry the minted `listen` token
//! instead of a channel id — and the unvanilla inbound
//! `forwarded-tcpip` open),
//! `"listen-failed"` / `"no-such-listen"` (`w3-tunnel-listen`: the
//! listen dial's failed/bounded outcome and the cancel forms'
//! not-found pair) — the global-request and arrival rows, and on the
//! auth class `"method-unimplemented"` / `"cert-unsupported"` /
//! `"non-ed25519"` / `"fingerprint-unresolved"`.
//! - `exec_route` — only on `exec_request`-row events: the dispatch
@@ -225,6 +234,28 @@ pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
);
}
/// A decline on the `-R` leg's **arrival** surface (`w3-tunnel-
/// listen`): a fabric inbound forwarded open that cannot become an
/// SSH channel — `correlation-miss` (no route for the minted token —
/// teardown-at-birth), `cap-exceeded` (the per-arrival admission), or
/// `accept-refused` (the vanilla client rejected the SSH open — the
/// count released, never a half-open channel). No SSH channel id
/// exists yet at this surface (that is the point of the declines), so
/// the minted `listen` token — the correlation key itself — rides
/// instead; the token is bookkeeping metadata (the producer's minted
/// correlation number), never secret material.
#[cfg(feature = "tunnel")]
pub(crate) fn decline_arrival(row: &'static str, reason: &'static str, listen: u32) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
listen,
decline_class = "arrival",
reason,
"decline: forwarded arrival rejected"
);
}
/// A serving-path decline: a routed row whose serving task answers
/// failure-then-close rather than serving. This wave that is the seam
/// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3
@@ -467,6 +498,31 @@ mod tests {
assert_eq!(field(&fields, "error"), "Probe");
}
/// - *Contract stamp:* the arrival vocabulary (`w3-tunnel-listen`)
/// — an arrival decline carries the minted `listen` token
/// instead of a channel id (none exists yet — that is the point
/// of the declines; the token is bookkeeping metadata, never
/// secret material).
#[cfg(feature = "tunnel")]
#[test]
fn decline_arrival_carries_the_listen_token_not_a_channel_id() {
let fields = capture(|| decline_arrival("forwarded-tcpip", "correlation-miss", 9182));
assert_eq!(field(&fields, "feature_build"), FEATURE_BUILD);
assert_eq!(field(&fields, "row"), "forwarded-tcpip");
assert_eq!(field(&fields, "listen"), "9182");
assert_eq!(field(&fields, "decline_class"), "arrival");
assert_eq!(field(&fields, "reason"), "correlation-miss");
assert_eq!(
field(&fields, "message"),
"decline: forwarded arrival rejected"
);
let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
assert!(
!names.iter().any(|n| n == "channel_id"),
"an arrival decline carries the listen token, never a channel id"
);
}
/// The feature-build string: every enabled feature appears; a
/// featureless build reads `none`. The expected half is derived
/// from `cfg!` independently of the crate's match — divergence
+512 -38
View File
@@ -30,6 +30,129 @@
//! observes a clean, channel-scoped open refusal (POC-1 §5 posture)
//! — vanilla clients retry-observe a down forward instead of a hang.
//!
//! ## The `-R` listen flow (`w3-tunnel-listen` — the fabric listens,
//! the client dials)
//!
//! The server direction of the route-1 bridge (`mapping.md`
//! §Channel-type rows, the `forwarded-tcpip` row + §Global-request
//! rows). Per the family door semantics: the door owns nothing local
//! — **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:
//!
//! 1. **The listen dial** (`tcpip_forward` / `streamlocal_forward`):
//! the door maps the vanilla listen request family-ward as
//! `TunnelParams { resource: <the wire bind endpoint string>,
//! substrate: Tcp/Unix }` — the **resource-naming convention
//! recorded** (the one open surface this flow's composition had):
//! ADR-001 keeps the wire field a *resource identifier*, and a
//! fabric serving vanilla `-R` opt into dynamic-address binding by
//! its own establisher/bind-closure treating the mapped string as
//! its bind endpoint (`bindfirst_establisher` +
//! `register_openable_with_establisher`, the alkcall ADR-049
//! establisher shape — who may bind what is the fabric's ACL +
//! its bind closure, assembly policy at the family's provider,
//! never the door's). The dial is the published consumer surface —
//! `TunnelListener::open(client, params)` on the held
//! `ChannelClient` — bounded by [`LISTEN_DIAL_TIMEOUT`]; the
//! reply's `bound` + minted `listen` token (the alkcall ADR-049
//! amendment-3 establisher fields) are read through
//! `parse-listen-reply`-class accessors; **a reply without both is
//! a declined listen with the fabric listener torn down
//! immediately** (never a minted-but-uncorrelatable listen — a
//! declined listen never left a half state).
//! 2. **The door-side arrival surface** — the composition decision
//! (the task's second `w3-seam-config` course correction): the
//! fabric dials `tunnel/forwarded` *toward the door* per alktunnels'
//! bind-first dispatch (`make_bindfirst_accept_handler` +
//! `ForwardedOpener` — the producer calls the op on the channel-0
//! call plane toward the listen opener, and the door reply carries
//! the door-allocated channel id the producer adopts; alkcall's
//! odd/even split keeps the two open directions collision-free).
//! Serving that op needs the door's channel-0 serving loop — an
//! alkcall `ChannelClient` built with
//! `from_connection_with_serving`, whose registry the assembly owns
//! — so the serve-config carries [`crate::config::TunnelListenSeam`]
//! (the same `Arc<OperationRegistry>` the assembly wired); alkssh
//! registers the forwarded openable itself in
//! [`install_forwarded_surface`] (called from
//! [`crate::SshAdapter::new`]; the registration failure is a failed
//! construction): a `ChannelCore` over a clone of the held
//! 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.
//! 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
//! 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
//! connected address as the wire `tcpip_forward` request carried it
//! verbatim (the OpenSSH client matches that exact string; for
//! `port = 0` mints the *actually bound* port from the fabric's
//! `bound` reply — the sshd parity recorded in
//! [`ListenRoute`]) — with the `TunnelForwardedParams.peer` arrival
//! mapped to the originator fields (`HostPort` → address/port;
//! `Path` → path + port 0 — the degenerate but typed mapping,
//! unreachable for a TCP-minted listen). The accepted-or-rejected
//! decision is the in-crate cap (the wave-2 gate's booked acceptor
//! vocabulary — the per-arrival admission; a client-side rejection
//! or a dead session releases the count, the established channel's
//! release rides [`crate::handler`]'s `channel_close` like every
//! established channel). Accept → the fabric `BiStream` pumped
//! against the SSH channel ([`alkcall::channels::pump::pump_bidi`]).
//! Unix-substrate arrivals (a `streamlocal_forward`-minted
//! route) open `forwarded-streamlocal@openssh.com` with the listen
//! path echoed (russh's server `Handle` carries the channel-open at
//! this pin, and the vanilla client's `-R` unix listener is the
//! consumer of that leg).
//!
//! **The inline listen dial (recorded decision):** the
//! spawn-and-return rule keeps *serving paths* out of the packet
//! loop — but the `tcpip_forward` global request's reply is bound to
//! the hook itself (russh answers the want-reply from the hook's
//! return + the `&mut u32` out-param; no handle surface responds to a
//! global request from a spawned task at this pin), so the listen
//! dial awaits inline. This cannot deadlock against the peer's
//! follow-up bytes the way an inline serving path can (the vanilla
//! client waits for the global reply before proceeding; the fabric's
//! answer rides the fabric leg's connections, not this session's
//! packet loop), and its stall is bounded: other channels of the
//! connection pause only for [`LISTEN_DIAL_TIMEOUT`], and the
//! fabric-dial failure answers a denied listen. All serving below the
//! reply (none — the listen returns) and all arrival paths stay
//! spawned.
//!
//! ## Bounded bookkeeping (AGENTS.md convention 9)
//!
//! - **The per-session listener map** (the session-handler's
//! `listeners` — packet-loop-only writer, like the `channels`/
//! `ptys` stores) maps the minted token → the pending listen (wire
//! args, bound port, the [`alktunnels::consumer::TunnelListener`]
//! whose `close`/`Drop` perform the fabric teardown). Its bound:
//! the listen count shares the `Limits::max_concurrent_channels`
//! *ceiling* in a dedicated counter — a listen is at least one
//! future channel's potential, sessions minting unbounded listeners
//! would mint an unbounded fabric-channel map on the shared leg.
//! - **The route map** ([`ListenRouter`], shared with the registered
//! arrival pump across every session riding one fabric leg) is
//! bounded by construction: entries are minted exactly at accepted
//! listens (listener-cap gated), removed 1:1 at the cancel forms
//! and at the owning session's teardown (the handler's `Drop`
//! removes its tokens — a stale route to a dead handle only ever
//! costs one failed open that returns teardown-at-birth, never a
//! half-open SSH channel).
//! - **A cancelled / torn-down listen never half-serves**: the cancel
//! path removes the route before any teardown (a racing arrival
//! misses → teardown-at-birth), and `cancel_*` of an unknown
//! (address, bound-port) pair answers `false` — the vanilla
//! cancel contract (sshd parity: match by the raw wire string +
//! the bound port).
//!
//! ## The runtime-Unix gate (OQ-11)
//!
//! `direct-streamlocal` compiles wherever `tunnel` compiles (universal
@@ -38,7 +161,8 @@
//! (never a partial bridge), on-Unix the real bridge. The gate lives
//! in the dispatch hook ([`crate::handler`]'s `direct-streamlocal`
//! row), before the cap admission — a platform-refused open consumes
//! nothing.
//! nothing. The gate rides the `streamlocal_forward` listen row the
//! same way (`w3-tunnel-listen`).
//!
//! ## The identity ride (the visible-surface invariant's gate)
//!
@@ -51,53 +175,50 @@
//! open that proceeds then rides the channels connection into the
//! fabric, whose own `AccessControl::check(peer_identity)` gates the
//! mapped `channels/tunnel/direct` open (alkcall CF-005/CF-006) —
//! the composed-gate witness is the harness's identity rows.
//! the composed-gate witness is the harness's identity rows. The
//! `-R` leg composes the same gates on both of its directions: the
//! listen dial rides the fabric leg's identity (`tunnel:open` scope —
//! the assembly's dial-identity composition, unchanged), and the
//! arrivals are gated by the door-side registry's ACL
//! (`tunnel:forwarded` scope, per arrival) exactly when the assembly's
//! fabric leg carries it.
//!
//! **`set_identity` mechanics (recorded — the task's decided point):**
//! the alkhub `hub_leg.rs` one-time-set precedent
//! (`channel0_conn.set_identity(identity)`) is a **serving-side** set —
//! the hub resolves the peer identity and sets it on its accept-side
//! channel-0 `Connection` inside the channel-0 install, before any
//! dispatch. The client-side surface
//! (`ChannelClient::take_call_connection()` → `CallConnection::
//! connection()` → `Connection::set_identity`) is **rejected for this
//! door**, verified against alkcall 0.10.1: the take is a one-shot
//! single-slot detach ("taking it detaches the client's own calling
//! surface" — every later `call_open_op`, and with it every
//! `TunnelSession::open_direct`, errors "channel client closed"), and
//! the slot is the **shared assembly handle**: `SshAdapter` hands one
//! `Arc<ServeConfig>` to every mounted session, so the first session's
//! take would detach the open surface for every later session of the
//! whole serving direction — a strictly worse version of the relay
//! note's "the detach is wrong for hubs" (three claimants). The
//! identity that alkcall's protocol consults at the fabric's serving
//! dispatch is the channels connection's dial identity (CF-005: set
//! once at dial, propagated to channel 0 at `from_connection`) —
//! crossing sides is not an alkcall wire concept. The composition —
//! which channel identity the fabric leg dials with, against which the
//! fabric's ACL gates alkssh's mapped opens — is therefore **assembly
//! policy**: the assembly supplies both the injected
//! `IdentityProvider` (the SSH-peer resolution) and the fabric leg
//! (the handle and its dial identity); alkssh carries no deployment
//! policy, and it structurally refuses anonymous serving (the
//! `no-identity` guard) so no SSH-shaped exception to the
//! visible-surface invariant exists. The upstream ask that would let
//! alkssh do more (a non-detaching post-construction identity seam on
//! `ChannelClient`, per-session fabric legs) is booked for the
//! wave-3 gate.
//! **`set_identity` mechanics (recorded — `w3-tunnel-direct`'s decided
//! point, carried unchanged):** the alkhub `hub_leg.rs` one-time-set
//! precedent (`channel0_conn.set_identity(identity)`) is a
//! **serving-side** set — the identity the fabric leg dials with, and
//! against which the fabric's ACL gates alkssh's mapped opens, is the
//! assembly-supplied `ChannelClient`'s own dial identity (CF-005: set
//! once at dial, propagated to channel 0 at `from_connection`); the
//! door's `ChannelClient::take_call_connection` surface is rejected
//! for this door (the detach would strand every later session), and
//! the identity composition is assembly policy, structurally refused
//! anonymous by the `no-identity` guard. The door-side arrival surface
//! adds no new identity mechanics: alkssh registers the openable with
//! an install-time anonymous `AuthContext` (transport-truthful fields
//! are not alkssh's to know) and alkcall's per-call CF-005 overlay +
//! the assembly's `ServingConfig` composition decides what identity
//! the arriving forwarded call carries.
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use alkcall::channels::client::ChannelClient;
use alkcall::channels::operations::ChannelCore;
use alkcall::channels::pump::pump_bidi;
use alktunnels::consumer::TunnelSession;
use alktunnels::params::{Substrate, SubstrateAddr};
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 parking_lot::Mutex;
use russh::Channel;
use russh::server::ChannelOpenHandle;
use russh::server::{ChannelOpenHandle, Handle};
use crate::channel_cap::ChannelCap;
use crate::serve;
use crate::{dispatch, observe};
use crate::{config, dispatch, observe};
/// The SSH data channel type: russh's server-side channel over its own
/// message type (the store-and-take handoff and the serving rows use
@@ -159,3 +280,356 @@ pub(crate) fn spawn_direct_row(
row, channel, reply, target, substrate, client, cap,
));
}
// ---------------------------------------------------------------------------
// The `-R` listen flow (w3-tunnel-listen)
// ---------------------------------------------------------------------------
/// The wall-clock bound on the family-ward listen dial (`TunnelListener::
/// open`): the fabric's establisher is already bound by alkcall's own
/// establisher deadline (10 s, alkcall ADR-049 §2), so this matches it —
/// a fabric that never answers a listen dial cannot hold the packet
/// loop longer than a bounded dial.
pub(crate) const LISTEN_DIAL_TIMEOUT: Duration = Duration::from_secs(10);
/// One route in the listen-arrival bookkeeping — registered at listen
/// acceptance, consulted per arrival. The echo fields are the vanilla
/// `-R` convention decoded (sshd parity, recorded on [`ListenRouter`]):
/// the connected address rides the wire `tcpip_forward` string
/// **verbatim** (the OpenSSH client sent that exact string and matches
/// the open's address field against it exactly), the port is the
/// **actually bound** one (the fabric's `bound` reply — the sshd
/// `get_local_port`-of-accepted-socket parity, which is what makes
/// `port = 0` mints round-trip), and the unix path rides the
/// `streamlocal_forward` request verbatim.
#[derive(Clone, Debug)]
pub(crate) struct ListenRoute {
/// The SSH args the arrivals' opens echo — keyed by the route's
/// substrate (`Tcp`/`Udp` → the vanilla `-R` trio's first two
/// fields; `Unix` → the `forwarded-streamlocal` path).
pub(crate) echo: RouteEcho,
pub(crate) substrate: Substrate,
/// The owning session's packet-loop handle — the arrival's SSH
/// channel opens through it; a session that died fails the open
/// (the arrival's teardown-at-birth path).
pub(crate) handle: Arc<Handle>,
/// The owning session's in-crate concurrent-channel cap: the
/// per-arrival admission (the acceptor vocabulary) + the release
/// on the never-established paths.
pub(crate) cap: Arc<ChannelCap>,
pub(crate) cap_value: usize,
}
/// The echoed SSH args of a route (see [`ListenRoute`]).
#[derive(Clone, Debug)]
pub(crate) enum RouteEcho {
Tcp { address: String, port: u32 },
Unix { path: String },
}
/// The shared per-fabric-leg map of accepted listens
/// (`token → route`), the arrival pump's correlation registry and the
/// bounded bookkeeping's spine: entries are minted exactly at accepted
/// listens (the per-session listener cap gated), taken at the cancel
/// forms, and removed at the owning session's teardown (the handler's
/// `Drop`). Arrivals that find no route are the correlation-miss
/// class — teardown-at-birth, never a half-open SSH channel.
///
/// The map is behind a `parking_lot::Mutex` because arrivals arrive on
/// alkcall serving tasks while the packet loop mints/removes — the
/// held region is a HashMap op, never an await.
pub(crate) struct ListenRouter {
routes: Mutex<HashMap<u32, ListenRoute>>,
}
impl ListenRouter {
/// Minted-entry registration (the listen-acceptance step).
pub(crate) fn register(&self, token: u32, route: ListenRoute) {
self.routes.lock().insert(token, route);
}
/// The per-arrival lookup — cloned out, never taken: one route
/// serves unboundedly many arrivals; only the cancel forms and
/// session teardown remove.
pub(crate) fn route_for(&self, token: u32) -> Option<ListenRoute> {
self.routes.lock().get(&token).cloned()
}
/// The cancel-form removal (1:1 into the family listener's
/// teardown): removes first, so a racing arrival already misses —
/// teardown-at-birth, never a post-cancel half-serve.
pub(crate) fn cancel(&self, token: &u32) -> bool {
self.routes.lock().remove(token).is_some()
}
/// The owning session's teardown (the handler's `Drop`): remove
/// the session's tokens; any listener objects left drop with the
/// handler (the fabric-side reap — see `PendingListen`).
pub(crate) fn remove_tokens(&self, tokens: &[u32]) {
let mut routes = self.routes.lock();
for token in tokens {
routes.remove(token);
}
}
}
impl Default for ListenRouter {
fn default() -> Self {
Self {
routes: Mutex::new(HashMap::new()),
}
}
}
/// The door-side arrival surface's install step (called from
/// [`crate::SshAdapter::new`] when the fabric leg + seam are both
/// present): builds the router, registers alktunnels' forwarded
/// openable into the seam's shared registry over a `ChannelCore`
/// wrapping a clone of the held `ChannelClient`'s manager — the
/// alkcall shape whose wrapper performs the door-side arrival's
/// channel allocation, ACL (`tunnel:forwarded`, per call) and budget
/// (alkcall's secure default per-identity policy, `256`, plus the
/// `DEFAULT_MAX_CHANNELS` per-connection bound — alkssh composes no
/// 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 one-per-op-id-
/// per-registry (alkcall's rule): a conflicting registration is a
/// failed construction (fail-closed — never a route to a handler the
/// door does not own).
///
/// On success the router rides back into the serve-config (the
/// session handlers read it) — `Ok` with the router installed, `Err`
/// is the caller's constructor failure.
pub(crate) fn install_forwarded_surface(serve: &mut config::ServeConfig) -> Result<(), String> {
let Some(seam) = serve.tunnel_listen_seam.as_mut() else {
return Err("tunnel listen surface requested without a tunnel_listen_seam".to_string());
};
let Some(client) = serve.channel_client.as_ref() else {
return Err("tunnel listen seam configured without a fabric leg".to_string());
};
let router = Arc::new(ListenRouter::default());
let handler: ForwardedHandler = {
let router = Arc::clone(&router);
Arc::new(move |params: TunnelForwardedParams, bidi: BiStream| {
tokio::spawn(pump_forwarded_arrival(Arc::clone(&router), params, bidi))
})
};
let core = ChannelCore::new(
client.manager().clone(),
alkcall::channels::policy::default_policy(),
);
register_tunnel_forwarded_openable(
&core,
&seam.registry,
AuthContext::anonymous(b"alk/tunnel"),
handler,
)
.map_err(|failure| format!("tunnel/forwarded openable registration: {failure}"))?;
serve.listen_router = Some(Arc::clone(&router));
Ok(())
}
/// The registered arrival pump — the `forwarded-tcpip` data leg
/// (mapping.md's row, "hand the channel duplex to the handler"): the
/// alkcall openable registration hands every fabric arrival here with
/// the parsed [`TunnelForwardedParams`] and the arrival's `BiStream`;
/// the flow is the recorded shape:
///
/// 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).
/// 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
/// channel. The count releases here on every never-established
/// path; an established one releases at its close (the packet
/// loop's release site), the direct rows' disjoint-release shape.
/// 3. **The SSH open toward the client** — the echoed listen args
/// (`forwarded-tcpip` for the tcp/udp substrates with the originator
/// fields mapped from `TunnelForwardedParams.peer`; `forwarded-
/// streamlocal@openssh.com` with the echoed path for `Unix`) — per
/// the spawn-and-return constraint this runs on this spawned task;
/// a rejected/dead-open releases the count and drops the fabric
/// half (never a half-open SSH channel).
/// 4. **The pump** — the fabric `BiStream` ↔ the SSH channel duplex
/// (the route-1 bridge's primitive, reversed).
async fn pump_forwarded_arrival(
router: Arc<ListenRouter>,
params: TunnelForwardedParams,
bidi: BiStream,
) {
let row: &'static str = match params.substrate {
Substrate::Unix => "forwarded-streamlocal",
Substrate::Tcp | Substrate::Udp => "forwarded-tcpip",
};
let Some(route) = router.route_for(params.listen) else {
observe::decline_arrival(row, "correlation-miss", params.listen);
drop(bidi);
return;
};
if !route.cap.admit(route.cap_value) {
observe::decline_arrival(row, "cap-exceeded", params.listen);
drop(bidi);
return;
}
let opened = match route.substrate {
Substrate::Unix => match route.echo {
RouteEcho::Unix { path } => route.handle.channel_open_forwarded_streamlocal(path).await,
_ => {
route.cap.release();
observe::decline_arrival(row, "correlation-miss", params.listen);
return;
}
},
Substrate::Tcp | Substrate::Udp => match (route.echo, &params.peer) {
(
RouteEcho::Tcp { address, port },
SubstrateAddr::HostPort {
host,
port: peer_port,
},
) => {
route
.handle
.channel_open_forwarded_tcpip(
address,
port,
host.clone(),
u32::from(*peer_port),
)
.await
}
(RouteEcho::Tcp { address, port }, SubstrateAddr::Path { path }) => {
// The degenerate-but-typed mapping (a path-shaped peer on
// a tcp listen — unreachable from a well-behaved producer):
// the path rides the originator string, port 0.
route
.handle
.channel_open_forwarded_tcpip(address, port, path.clone(), 0)
.await
}
_ => {
route.cap.release();
observe::decline_arrival(row, "correlation-miss", params.listen);
return;
}
},
};
match opened {
Ok(channel) => {
let channel_number = channel.id().number();
observe::serving_path_started(row, channel_number, None);
let (ssh_read, ssh_write) = tokio::io::split(channel.into_stream());
pump_bidi(bidi, ssh_read, ssh_write).await;
observe::serving_path_ended(row, channel_number, None);
}
Err(_) => {
route.cap.release();
observe::decline_arrival(row, "accept-refused", params.listen);
}
}
}
/// A minted listen — the door-side outcome of a `tcpip_forward` /
/// `streamlocal_forward` request's family-ward dial (the published
/// `TunnelListener::open`, bounded by [`LISTEN_DIAL_TIMEOUT`], with
/// the reply's `bound` + `listen` establisher fields required: a
/// producer that minted no correlation token or no bound report
/// cannot serve — [`Result`]'s `Err` declines the SSH request before
/// anything registers).
pub(crate) struct MintedListen {
pub(crate) listener: TunnelListener,
pub(crate) token: u32,
pub(crate) bound: SubstrateAddr,
}
/// The listen dial (the mapping.md §Global-request rows' family-ward
/// step): `TunnelParams { resource: <endpoint string>, substrate }`
/// — the wire-named bind endpoint as the resource, the recorded
/// door-side naming convention whose fabric-side interpretation is
/// the assembly's establisher — then the reply projection. Called
/// inline from the handler hook (see the recorded inline-decision in
/// the module doc).
pub(crate) async fn open_listen(
client: &Arc<ChannelClient>,
resource: String,
substrate: Substrate,
) -> Result<MintedListen, String> {
let future = TunnelListener::open(
client,
TunnelParams {
resource,
substrate,
},
);
let listener = match tokio::time::timeout(LISTEN_DIAL_TIMEOUT, future).await {
Ok(Ok(listener)) => listener,
Ok(Err(error)) => return Err(format!("listen dial failed: {error:?}")),
Err(_) => return Err("listen dial timed out".to_string()),
};
let Some(token) = listener.listen() else {
return Err("the fabric minted no listen token".to_string());
};
let Some(bound) = listener.bound().cloned() else {
return Err("the fabric reported no bound endpoint".to_string());
};
Ok(MintedListen {
listener,
token,
bound,
})
}
/// The bound port the listen request's out-param carries
/// (the `&mut u32` sshd parity): the fabric's bound report's port.
/// Unix listens have no port semantics — the tcp variants only.
pub(crate) fn bound_port(bound: &SubstrateAddr) -> Option<u32> {
match bound {
SubstrateAddr::HostPort { port, .. } => Some(u32::from(*port)),
_ => None,
}
}
/// The family-ward resource string for a listen request — the wire
/// bind endpoint verbatim (`"localhost"` / `"0.0.0.0"` / explicit
/// host: the OpenSSH client has already normalized the no-address case
/// to `"localhost"` and `""` before this surface exists — sshd
/// `channel_rfwd_bind_host` parity), with the requested port for the
/// tcp substrates (`host:port`; port `0` stays `0` — the OS-picking is
/// the fabric's bind step, whose bound report feeds the out-param).
pub(crate) fn listen_resource(endpoint: &str, substrate: Substrate, port: Option<u32>) -> String {
match substrate {
Substrate::Unix => endpoint.to_owned(),
_ => format!("{}:{}", endpoint, port.unwrap_or(0)),
}
}
/// The per-session pending listen (the packet-loop-only store, like
/// the `channels`/`ptys` stores): the minted token is the map's key;
/// the entry carries what the cancel forms match on (the wire endpoint
/// verbatim + the bound port — the sshd `c->path == host &&
/// c->listening_port == port` parity) over the substrate (a unix
/// entry's port is 0-carrying and never matched — the unix cancel
/// forms match on the path alone) and the session-owned
/// [`TunnelListener`] whose close/Drop performs the family teardown.
pub(crate) struct PendingListen {
pub(crate) wire_address: String,
pub(crate) bound_port: u32,
pub(crate) substrate: Substrate,
pub(crate) listener: TunnelListener,
}
/// The listen count's dedicated bound — the same mechanism as
/// [`ChannelCap`], instantiated per session next to it, against the
/// same `Limits::max_concurrent_channels` ceiling: listeners and
/// channels stay independently bounded while their mix stays
/// unconstrained (AGENTS.md convention 9 — no unbounded listener
/// map). Admits are packet-loop-only (the listen hooks are the only
/// mint site); releases fire at the cancel forms, the listen-dial
/// failures (admitted-but-never-minted — the route 1 bridge's
/// release-site shape), and nowhere else (a minted listener's release
/// is... never — listeners hold until cancel or session teardown, so
/// the session's count dies with the handler, per the channels
/// cap's same posture).
pub(crate) type ListenerCap = ChannelCap;