Files
alkssh/src/handler.rs
T
glm-5.3-flash d300095687 feat(tunnel): ListenRouter token-collision pre-check — refuse, never replace (w4-router-collision)
Closes the wave-3 gate's booked finding item 4 (review-wave-3
§Booked findings): register on an already-held token now refuses
fail-closed instead of silently replacing the live route — a collided
mint (the ADR-008 discipline's upstream failure) can never steal a
live adapter's arrival routes.

- ListenRouter::register returns Result<(), String>; collision → Err,
  the held route untouched (src/tunnel_map.rs)
- Error surface decided + recorded: the wire-time collision is the
  request-time decline disposition (RequestDenied, reason
  token-collision), not a new AdapterError variant — a global-request
  hook's Err propagates out of russh 0.65's packet loop and kills the
  session, while a refused listen's vanilla contract is RequestDenied;
  AdapterError::ListenRegistration keeps its construction-time class
  (src/error.rs doc records the sibling)
- listen_request_dispatch: register moved before the bound-port
  out-param write; on refusal the listener cap releases, the uniform
  decline event emits, and the minted fabric listener tears down
  immediately (never a half state) (src/handler.rs)
- Wire row a_collided_mint_is_refused_and_never_steals_the_live_route
  pins the full shape through a mint-strategy construct on the fabric
  double (MintStrategy::WithCollisions — the test forces what the
  fabric's discipline prevents): second register refuses, first route
  still serves end to end, refused endpoint never serves, count
  released (tests/tunnel_mapping.rs)

Verification: cargo test (default + git/session/tunnel/sftp +
--all-features) · clippy ×2 -D warnings · fmt --check · cargo doc
--no-deps · taskgraph validate ✓ (35 tasks)
2026-10-29 07:56:24 +00:00

1745 lines
74 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The per-connection server handler — the ADR-004 dispositional auth
//! bridge, plus the exec-grammar dispatch and the fail-closed decline
//! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth
//! hooks; `w3-git-mapping` replaced the skeleton probe with the full
//! grammar, landed the git row's alkgit dispatch, and with it the
//! store-and-take channel handoff).
//!
//! ## The auth bridge (ADR-004, decisions 1–5)
//!
//! russh performs all cryptographic work: [`Handler::auth_publickey`]
//! fires *after* signature and ownership verification, so this handler
//! never sees or verifies a signature — its surface is dispositions
//! only. The single resolution path: ed25519-only gate → the crate's
//! one fingerprint path ([`crate::fingerprint::fingerprint`]) → the
//! injected `IdentityProvider::resolve_from_fingerprint` → `Some`
//! accepts with the resolved identity stored in per-connection handler
//! state (it rides the session for the wave-3 seam propagation —
//! [`Handler::auth_succeeded`] is the ride-along hook); `None` rejects
//! fail-closed — the resolved-identity-`None` path never creates a
//! weakly authorized session.
//!
//! Every other method is an **explicit** reject — [`Handler::auth_none`],
//! [`Handler::auth_password`] and [`Handler::auth_keyboard_interactive`]
//! each implemented, never inherited — so the posture survives a pin
//! bump that could silently change russh's defaults (ADR-004 decision
//! 3). The non-ed25519 algorithm decline is explicit-over-inherited too
//! (ADR-002 decision 4). [`Handler::auth_publickey_offered`]
//! accepts-and-delegates explicitly (the probe hook consumes no attempt
//! — POC-1 verified; the delegation keeps `max_auth_attempts` counting
//! honest).
//!
//! The username carries no semantics (OQ-03 resolved): resolution is
//! fingerprint-only — the bridge core takes no username parameter at
//! all, structurally refusing any identity input from the user string.
//! Auth declines are *dispositions* (`Auth::Reject`), not errors — the
//! error model stays key-source-only.
//!
//! ## The parse-only dispatch + the fail-closed decline core
//!
//! (ADR-006 point 3 as amended, ADR-003; the routing decision and the
//! exec-parse skeleton probe live in [`crate::dispatch`].) Every
//! `channel_open_*` hook gates per the feature graph: `session`
//! channels open only where a session-serving feature exists (`session`
//! ∨ `git` — the parse path lives inside session channels);
//! `direct-tcpip` / `forwarded-tcpip` / `direct-streamlocal` gate on
//! the `tunnel` feature; **`x11` always declines** — never
//! feature-gated (ADR-003 §5). Unfeatured and never-feature rows
//! decline **at the open** with a clean wire reason
//! ([`crate::dispatch::unfeatured_open_decline`]) — never a
//! half-open channel. The `direct-tcpip` / `direct-streamlocal`
//! featured rows are the route-1 bridge (`w3-tunnel-direct`,
//! [`crate::tunnel_map`]): the pre-admit decline classes (the unset
//! fabric handle under the feature — the unset-declines posture; the
//! unreachable-in-practice `no-identity`; an impossible wire target),
//! the cap admission at the hook, then the spawned bridge — accept
//! 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).
//! 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
//! hook at this pin — russh rejects those opens inline
//! (administratively prohibited), which is the decline the never-
//! feature contract needs, but it is an inherited posture: a pin bump
//! that changed it would surface here as an accepted-then-dropped
//! channel (third-party-upstream note; file upstream first).
//!
//! The session rows are **parse-only**: `exec_request` parses first
//! (the full grammar, [`crate::dispatch::parse_exec`] — a git route to
//! the git row, a declined-with-reason grammar class to its honest
//! decline, anything else to the session pipe row); `pty_request` /
//! `shell_request` route to the session rows. Nothing is served before
//! a request exists — no request, no serving, so a request-less
//! session channel declines as unfeatured by construction.
//! **Decline first, alloc never**: every decline here precedes any
//! channel/resource allocation (a leaked channel alloc is a
//! capability leak — the POC-3 stage-A rule); the per-length
//! `max_exec_command_len` cap is enforced before the parse itself.
//!
//! The store-and-take channel handoff (`w3-git-mapping`)
//!
//! A confirmed `session` open **stores** its [`Channel`] in
//! per-connection handler state ([`Self::take_stored_channel`]'s
//! supplier: `channel_open_session` accepts, then inserts — the
//! POC-3 §A.4 accepted-but-held shape); the git row's `exec_request`
//! **takes** it (`channels.remove(&id)` → `into_stream()` in the
//! spawned serving task) and the session's packet loop keeps owning
//! the store: `channel_close` removes whatever it held (a channel
//! that served or declined without an exec frees there), and every
//! mutation happens on the loop's task — single-writer, no lock. A
//! git exec on a channel whose stored handle is already gone
//! (re-served, or never held — unvanilla) declines serving-path.
//!
//! ## The session rows' real serving paths (`w3-tty-session`)
//!
//! The pty/shell/pipe rows replaced their wave-3 skeleton stubs with
//! the alktty translation bridge ([`crate::tty_map`]): the pty row
//! stores the negotiated `TerminalParamsWire` ([`Self::ptys`]'s
//! supplier — taken by the later shell/exec request on the same
//! channel, `ssh -t host "cmd"`'s pty-then-exec); the shell and pipe
//! rows take the stored channel the same way the git row does and
//! spawn `serve_tty_row` over the configured seam — locally-built
//! negotiation, unset-fields-decline (OQ-08: `shell_argv` unset →
//! the shell row declines, `exec_wrapper_argv` unset → the pipe row
//! declines, `session_seam` unset → both rows and the pty row
//! decline `seam-unconfigured`; every one of them the serving-path
//! failure+close shape from a spawned task). A routed request
//! answers `channel_success` inline (the request routed; the exit
//! chunk reports the outcome later, status-before-close from the
//! spawned task).
//!
//! ## The control rows' per-channel surface (`w3-tty-session`)
//!
//! `window_change_request` and `signal` arrive on the packet loop
//! **after** the serving task owns the wire — the spawned bridge is
//! reached through [`Self::bridges`]' per-channel surface
//! ([`crate::tty_map::BridgeSurface`]: the bounded control mpsc's
//! `try_send` — never awaited on the packet loop — plus the
//! per-channel cancel). A resize for a channel whose pty is stored
//! but not yet serving updates the stored params (the final size
//! wins); signal names are accepted verbatim (the backend owns the
//! semantics). Unfeatured builds emit the request-level decline
//! event with no reply — the control rows are fire-and-forget, no
//! SSH reply exists to send. [`Self::channel_close`] releases the
//! surface (firing the cancel: the alktty kill-on-drop contract is
//! the cancelled session's cleanup) along with the channel and pty
//! stores. The **connection teardown** signal ([`crate::tty_map::
//! Teardown`]) fires from this handler's `Drop` so a serving task
//! can never outlive the russh session that spawned it.
//!
//! ## The spawn-and-return structural constraint (ADR-006 decision 4)
//!
//! russh at this pin runs handler callbacks **inline inside the
//! session's packet loop** — a blocking handler suspends all packet
//! routing and deadlocks against the peer's follow-up bytes (POC-3
//! proved by construction with receive-pack). Every serving path is
//! therefore spawned through [`crate::serve::spawn_serving_path`] and
//! the handler returns fast: the serving rows (the git row's alkgit
//! dispatch, the session rows' config-driven declines and
//! [`crate::tty_map`]'s bridge) all clone `session.handle()`, spawn
//! through the clone, and are gone from the hook's stack — no hook
//! awaits a serving path inline. The decline keeps its wire shape
//! (failure then close), riding the spawn. Exit/status reporting from
//! spawned tasks rides the same cloned handle
//! ([`crate::serve::report_exit_status_then_close`] — the mechanics;
//! the tty rows' derivation rules live in [`crate::tty_map`]), pinned
//! end-to-end by the mechanics rows in `tests/spawn_return.rs` and the
//! mapping rows in `tests/git_mapping.rs` / `tests/tty_mapping.rs`.
//!
//! The open-level declines (unfeatured/never-feature rows, the
//! direct rows' pre-admit classes) stay inline by class: they answer
//! 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. 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
//! does not until alkfs lands); git service names carry only a name,
//! never a repository id, so git can never dispatch as a subsystem
//! (ADR-006 decision 2); everything else is unimplemented. `env_request`
//! 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` + 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)
//!
//! Two decline classes, chosen for vanilla-client observability (POC-1
//! §5: channel-scoped decline, auth intact, no hangs):
//!
//! - **Open-level declines** — channel-open failure codes (`x11`
//! never-feature, unfeatured feature rows, the tunnel rows'
//! identity-less guard and beyond-cap refusals → administratively
//! 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 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
//! SSH request-reply vocabulary has no failure text, so a request
//! decline's "reason" is the routing decision itself (documented
//! above, in [`crate::dispatch`]'s grammar, and per feature build in
//! `mapping.md`'s decline matrix). A *capability* decline answers
//! failure and leaves the channel usable (the POC-1 §5
//! session-survives-declines posture) — including the git grammar's
//! declinables (`unknown-service` / `service-only` /
//! `invalid-repo-id`, the parsed reply shapes, `w3-git-mapping`);
//! a *serving-path* decline (a routable row that owns the request's
//! whole outcome: the seam-unconfigured git row, the session rows'
//! unset-field and unset-seam declines — `w3-tty-session`'s OQ-08
//! shapes — and the identity-less guard) answers failure and closes
//! the channel — the vanilla-clean shape for a row whose serving
//! path cannot exist. The serving-path decline rides the spawned
//! serving task (ADR-006 decision 4, §The spawn-and-return
//! structural constraint above); the wire shape is unchanged. The
//! `channel-not-held` guard is the inline request-level shape (the
//! channel that could not be held stays usable). The real serve's
//! own outcomes are not declines: the substrate maps them (denial
//! band before any protocol byte; exit-status then close — the
//! `exit_code` mapping decision recorded in [`crate::git_map`];
//! the tty bridge's negotiation-error and exit-chunk decisions in
//! [`crate::tty_map`]).
//!
//! ## The event vocabulary on every decline (`w3-decline-tracing`)
//!
//! Every decline site here emits its event through
//! [`crate::observe`] — the wave-2 gate's booked finding: the
//! deciding context (row, feature build, channel id, decline class,
//! reason) rides the tracing event, not just the routing decision
//! documented per build. The vocabulary and its safety posture (no
//! secrets, no username, no fingerprint — structurally) live in that
//! module; the serving-path lifecycle (started / ended / failure)
//! hangs off the spawned stub body and is what the wave-3 mappings'
//! real serving paths inherit.
//!
//! ## Bounded resources at the handler surface (`w2-bounded-resources`)
//!
//! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy —
//! the "concurrent channels per session — ours" row.) The in-crate
//! concurrent-channel cap ([`crate::channel_cap::ChannelCap`]) is
//! enforced at every `channel_open_*` hook: an accept path beyond
//! `Limits::max_concurrent_channels` is refused fail-closed at the
//! open ([`crate::dispatch::cap_exceeded_open_decline`] — never
//! queued, never half-open), and the count releases so the cap
//! recovers: through the `channel_close` hook for established
//! channels, and — `w3-tunnel-direct` — inside the route-1 bridge's
//! 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 `-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
//! checks precede the admission — decline-first-alloc-never applies
//! to the budget too), the cap admission before any accept; the
//! admitted count is held across the fabric open and released exactly
//! once on either path. The `max_exec_command_len` cap is
//! enforced at `exec_request` *before* the parse and any allocation
//! (`w3-git-mapping`): an over-length wire command declines
//! request-level, unfeatured-or-not. The per-identity policy layer
//! arrives with the alkcall bridge.
//!
//! ## Test surface
//!
//! The handler type is deliberately constructible outside this module
//! (re-exported `#[doc(hidden)]` from the crate root) so the wave-1
//! contract tests drive these hooks directly against the in-memory
//! doubles — the wire-facing `VerifiedPublicKey`/`PublicKeyOffer` hook
//! arguments are `#[non_exhaustive]` (only russh constructs them), so
//! the bridge core is its own method ([`SessionHandler::resolve_bridge`])
//! the tests call with ordinary key material; the wire-level rows
//! exercise the hook wiring itself, end to end.
use std::collections::HashMap;
use std::sync::Arc;
use alkcall::core::auth::Identity;
use russh::keys::Algorithm;
#[cfg(any(feature = "git", feature = "session"))]
use russh::server::Handle;
use russh::server::{Auth, ChannelOpenHandle, Msg, PublicKeyOffer, Session, VerifiedPublicKey};
use russh::{Channel, ChannelId, Pty, Sig};
use crate::channel_cap::ChannelCap;
use crate::config::ServeConfig;
#[cfg(feature = "session")]
use crate::config::SessionSeam;
use crate::dispatch::{self, ExecRoute};
use crate::error::AdapterError;
use crate::fingerprint::fingerprint;
#[cfg(feature = "git")]
use crate::git_map;
use crate::observe;
#[cfg(any(feature = "git", feature = "session", feature = "tunnel"))]
use crate::serve;
#[cfg(feature = "session")]
use crate::tty_map;
#[cfg(feature = "tunnel")]
use crate::tunnel_map;
#[cfg(feature = "session")]
use alktty::negotiation::NegotiateRequest;
/// The per-connection handler (one per mounted stream): russh constructs
/// nothing here — [`crate::SshAdapter::handle`] builds one per accepted
/// stream and drives it through `russh::server::run_stream`.
///
/// Carries the serve-config the auth bridge and dispatch consume, the
/// in-crate concurrent-channel cap (`ChannelCap` — see the private
/// `channel_cap` module; enforced at every `channel_open_*` accept
/// path, bounded resources, AGENTS.md convention 9), and the resolved
/// identity once a publickey auth has been accepted (see
/// [`Self::resolved_identity`] — the wave-3 seam reads it).
pub struct SessionHandler {
serve: Arc<ServeConfig>,
/// 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). 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
/// the close releases them. Written only by the packet loop's
/// hooks (`channel_open_session` inserts; the git row's and the
/// session rows' serving dispatches remove for their taken
/// channel; `channel_close` removes) — single-writer, no lock.
channels: HashMap<ChannelId, Channel<Msg>>,
/// The session rows' negotiated pty params (`w3-tty-session`):
/// `pty_request` stores, the later `shell_request`/`exec_request`
/// on the same channel consumes (`ssh -t host "cmd"`'s
/// pty-then-exec), the close releases. Packet-loop only.
#[cfg(feature = "session")]
ptys: HashMap<ChannelId, alktty::negotiation::TerminalParamsWire>,
/// The per-channel control surfaces (`w3-tty-session`): installed
/// when a session row's serving task spawns, forwarded to by
/// `window_change_request`/`signal`, fired-and-removed by
/// `channel_close`. Packet-loop only.
#[cfg(feature = "session")]
bridges: HashMap<ChannelId, tty_map::BridgeSurface>,
/// The connection-teardown signal (`w3-tty-session`): fired by
/// this handler's [`Drop`](Self::drop) so a serving task never
/// outlives the russh session that spawned it (the alktty
/// kill-on-drop contract is the cancelled session's cleanup).
#[cfg(feature = "session")]
teardown: tty_map::Teardown,
}
impl Drop for SessionHandler {
/// Fires the teardown signal (`w3-tty-session`): every spawned
/// **session-row** serving task of this connection watches it and
/// cancels its alktty session (the kill-on-drop cleanup) instead
/// of outliving the connection. The git serving task does not
/// watch this signal — it ends through the channel duplex (stream
/// EOF / send failure) once the russh session dies, bounded by the
/// alkgit seam's own `session_wall_clock` (the alkgit limits the
/// assembly configured). 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);
}
}
}
}
impl SessionHandler {
/// Builds the handler over the serving configuration — the
/// identity provider inside is the assembly-supplied
/// `alkcall::core::auth::IdentityProvider`; the bridge only
/// resolves through it.
pub fn new(serve: Arc<ServeConfig>) -> Self {
Self {
serve,
channel_cap: Arc::new(ChannelCap::default()),
resolved_identity: None,
channels: HashMap::new(),
#[cfg(feature = "session")]
ptys: HashMap::new(),
#[cfg(feature = "session")]
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(),
}
}
/// Takes a stored channel handle (the store half of the
/// store-and-take handoff): the git row's and the session rows'
/// dispatches remove the handle the open stored, handing the
/// stream into the spawned serving task. `None` when nothing is
/// held — a re-served or never-held channel (unvanilla; declined
/// serving-path).
#[cfg(any(feature = "git", feature = "session"))]
fn take_stored_channel(&mut self, channel: ChannelId) -> Option<Channel<Msg>> {
self.channels.remove(&channel)
}
/// The identity this connection currently rides — `Some` only after
/// a fingerprint resolution accepted a publickey auth (the bridge's
/// fail-closed rejecting paths never leave a weakly-authorized
/// state behind). The wave-3 seam propagation consumes this.
pub fn resolved_identity(&self) -> Option<&Identity> {
self.resolved_identity.as_ref()
}
/// The accept-path admission under the in-crate concurrent-channel
/// cap (bounded resources, `w2-bounded-resources`): room under
/// `Limits::max_concurrent_channels` admits and counts the channel;
/// beyond the cap the open is refused fail-closed with a clean
/// wire reason ([`crate::dispatch::cap_exceeded_open_decline`]) —
/// never queued, never half-open. Called exactly at accept sites —
/// the packet loop owns every admit call (the single admit writer,
/// [`ChannelCap`]'s recorded reasoning); a pure-decline path
/// (unfeatured, never-feature, the tunnel rows' pre-admit classes)
/// never touches the count. Since `w3-tunnel-direct`, the tunnel
/// rows admit **at the hook and hold the count across the fabric
/// open**: a fabric failure releases inside the spawned bridge
/// task (the admitted-but-rejected shape — no `channel_close` can
/// ever fire); a success releases when the established channel
/// closes ([`Self::channel_close`]). The release sites are disjoint
/// per admission by the wire shape.
fn admit_channel_open(&self) -> bool {
self.channel_cap
.admit(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
/// (`russh::server::VerifiedPublicKey`) is `#[non_exhaustive]` —
/// only russh constructs it — but this method takes the two facts
/// the hook receives (the verified key and the signature's
/// algorithm) as ordinary arguments.
///
/// Ed25519-only gate first (ADR-002 decision 4 — `SkEd25519` and
/// every other scheme share the non-ed25519 decline, which is
/// explicit-over-inherited so it survives a pin bump), then the
/// single fingerprint path → the injected provider's
/// `resolve_from_fingerprint`. No username parameter exists — the
/// user string is never an identity input (OQ-03). `None` is the
/// fail-closed reject disposition; `Some` carries the identity the
/// hook stores in handler state.
pub fn resolve_bridge(
&self,
public_key: &russh::keys::PublicKey,
signature_algorithm: &Algorithm,
) -> Option<Identity> {
if *signature_algorithm != Algorithm::Ed25519
|| public_key.algorithm() != Algorithm::Ed25519
{
observe::decline_auth("auth_publickey", "non-ed25519");
return None;
}
let identity = self
.serve
.identity_provider
.resolve_from_fingerprint(&fingerprint(public_key));
if identity.is_none() {
observe::decline_auth("auth_publickey", "fingerprint-unresolved");
}
identity
}
/// The `session` channel-open gate (ADR-003 feature graph):
/// session channels open only where a session-serving feature
/// exists — `session` ∨ `git`, because git's parse path lives
/// inside session channels.
fn serves_session_channels(&self) -> bool {
cfg!(feature = "session") || cfg!(feature = "git")
}
/// The session-row gate (`pty`/`shell`/pipe-exec): only the
/// `session` feature serves these rows.
#[cfg(feature = "session")]
fn serves_session_rows(&self) -> bool {
cfg!(feature = "session")
}
/// The git-row gate (git-parseable exec): only the `git` feature
/// serves this row.
#[cfg(feature = "git")]
fn serves_git_row(&self) -> bool {
cfg!(feature = "git")
}
/// The tunnel-row gate (`direct-tcpip` / `forwarded-tcpip` /
/// `direct-streamlocal` opens and the listen flow): only the
/// `tunnel` feature serves these rows.
fn serves_tunnels(&self) -> bool {
cfg!(feature = "tunnel")
}
}
impl russh::server::Handler for SessionHandler {
type Error = AdapterError;
/// ADR-004 decision 1 (as amended for ADR-002 decision 4): the
/// verified key bridges fingerprint →
/// `IdentityProvider::resolve_from_fingerprint` →
/// accept-with-identity / reject fail-closed; non-ed25519 keys
/// decline explicitly before any resolution. The resolved identity
/// lands in the per-connection handler state.
async fn auth_publickey(
&mut self,
_user: &str,
key: VerifiedPublicKey<'_>,
) -> Result<Auth, Self::Error> {
match self.resolve_bridge(key.public_key, key.algorithm) {
Some(identity) => {
self.resolved_identity = Some(identity);
Ok(Auth::Accept)
}
None => Ok(Auth::reject()),
}
}
/// ADR-004 decision 2: accept-and-delegate, explicit — the probe
/// hook consumes no auth attempt (POC-1 verified), so keeping it at
/// `Auth::Accept` keeps the `max_auth_attempts` counting honest;
/// the real disposition is [`Self::auth_publickey`]'s.
async fn auth_publickey_offered(
&mut self,
_user: &str,
_offer: PublicKeyOffer<'_>,
) -> Result<Auth, Self::Error> {
Ok(Auth::Accept)
}
/// ADR-004 decision 3: explicit family-contract decline — never the
/// inherited default (a pin bump could silently change it).
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_none", "method-unimplemented");
Ok(Auth::reject())
}
/// ADR-004 decision 3: explicit family-contract decline — password
/// auth does not exist on this door; ADR-005's key custody is the
/// whole story.
async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_password", "method-unimplemented");
Ok(Auth::reject())
}
/// ADR-004 decision 3 + OQ-04 (resolved): explicit decline — the
/// CA-trust story does not exist in the family identity model;
/// future support is new-ADR territory.
async fn auth_openssh_certificate(
&mut self,
_user: &str,
_certificate: &russh::keys::Certificate,
) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_openssh_certificate", "cert-unsupported");
Ok(Auth::reject())
}
/// ADR-004 decision 3: explicit family-contract decline.
async fn auth_keyboard_interactive(
&mut self,
_user: &str,
_submethods: &str,
_response: Option<russh::server::Response<'_>>,
) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_keyboard_interactive", "method-unimplemented");
Ok(Auth::reject())
}
/// A counted channel closed — the in-crate concurrent-channel
/// cap's decrement (`w2-bounded-resources`): every fired
/// `channel_close` corresponds to exactly one previously-admitted
/// open (russh only routes `CHANNEL_CLOSE` for established
/// channels — a declined open never established one — and consumes
/// the close at most once), so the count recovers through this
/// single release point, saturating in [`ChannelCap::release`] on
/// top. The spawned serving paths never touch the count directly:
/// their closes ride the cloned handle, the client's close reply
/// cycles back through the packet loop, and this hook fires — the
/// single-writer reasoning in [`ChannelCap`].
async fn channel_close(
&mut self,
channel: ChannelId,
_session: &mut Session,
) -> Result<(), Self::Error> {
self.channels.remove(&channel);
#[cfg(feature = "session")]
{
self.ptys.remove(&channel);
if let Some(surface) = self.bridges.remove(&channel) {
tty_map::fire_cancel(&surface.cancel);
}
}
self.channel_cap.release();
Ok(())
}
/// The session-channel open gate (ADR-003 feature graph,
/// `session` ∨ `git`), then the in-crate concurrent-channel cap
/// ([`Self::admit_channel_open`] — bounded resources,
/// `w2-bounded-resources`): unfeatured builds decline at the open
/// — never a half-open channel — and a featured open beyond the
/// cap is refused fail-closed with a clean wire reason
/// ([`crate::dispatch::cap_exceeded_open_decline`]), never queued.
/// A confirmed open accepts the channel and counts it; the count
/// releases when the channel closes ([`Self::channel_close`]). The
/// parse-only dispatch serves the accepted channel through its
/// requests (nothing serves request-less — ADR-006 as amended).
async fn channel_open_session(
&mut self,
channel: Channel<Msg>,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
if !self.serves_session_channels() {
observe::decline_open("session", "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.admit_channel_open() {
reply.accept().await;
self.channels.insert(channel.id(), channel);
} else {
observe::decline_open("session", "cap-exceeded", channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
}
Ok(())
}
/// The x11 channel open: **always declines** — a never-feature
/// (ADR-003 §5), declined in every build, never feature-gated. The
/// cap never applies here: a count is consumed only at an accept
/// path, and this hook never has one.
async fn channel_open_x11(
&mut self,
channel: Channel<Msg>,
_originator_address: &str,
_originator_port: u32,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_open("x11", "never-feature", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
Ok(())
}
/// The `direct-tcpip` open gate (`tunnel`) and its route-1 bridge
/// dispatch (`w3-tunnel-direct`): unfeatured declines at the open;
/// featured rows build the bridge target from the SSH open
/// arguments (`SubstrateAddr::HostPort` + `Substrate::Tcp` — an
/// out-of-range wire port is an impossible target, declined
/// pre-admission) and route to [`Self::dispatch_direct_open`]:
/// pre-admit decline classes first (the unset fabric handle
/// declining under the feature — the unset-declines posture — and
/// the unreachable-in-practice `no-identity` guard), then the cap
/// admission **at the hook, held across the fabric open**, then
/// the spawned bridge per spawn-and-return —
/// `TunnelSession::open_direct` → accept only after the fabric
/// open succeeds (`ConnectFailed` on failure, the count released
/// in the task), `take_halves()` pumped against the taken channel.
async fn channel_open_direct_tcpip(
&mut self,
channel: Channel<Msg>,
host_to_connect: &str,
port_to_connect: u32,
_originator_address: &str,
_originator_port: u32,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "direct-tcpip";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
#[cfg(feature = "tunnel")]
{
let target = u16::try_from(port_to_connect).ok().map(|port| {
alktunnels::params::SubstrateAddr::HostPort {
host: host_to_connect.to_owned(),
port,
}
});
direct_open_dispatch(
self,
ROW,
channel,
reply,
target,
alktunnels::params::Substrate::Tcp,
)
.await;
}
#[cfg(not(feature = "tunnel"))]
{
let _ = (host_to_connect, port_to_connect);
}
Ok(())
}
/// 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>,
_host_to_connect: &str,
_port_to_connect: u32,
_originator_address: &str,
_originator_port: u32,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "forwarded-tcpip";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
observe::decline_open(ROW, "unvanilla-inbound", channel.id().number());
reply.reject(dispatch::connect_failed_open_decline()).await;
Ok(())
}
/// The `direct-streamlocal` open gate (`tunnel`) and its route-1
/// bridge dispatch: the `direct-tcpip` shape with the OQ-11
/// **runtime-Unix gate** — the row compiles wherever `tunnel`
/// compiles (universal compile, no per-platform feature fork), but
/// off-Unix it declines at the open **before any fabric call**
/// (never a partial bridge — the gate precedes the unset-handle
/// check, the identity guard, and the cap admission), and on-Unix
/// it rides the same bridge with `SubstrateAddr::Path` +
/// `Substrate::Unix`.
async fn channel_open_direct_streamlocal(
&mut self,
channel: Channel<Msg>,
_socket_path: &str,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "direct-streamlocal";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
#[cfg(feature = "tunnel")]
{
#[cfg(unix)]
{
let target = Some(alktunnels::params::SubstrateAddr::Path {
path: _socket_path.to_owned(),
});
direct_open_dispatch(
self,
ROW,
channel,
reply,
target,
alktunnels::params::Substrate::Unix,
)
.await;
}
#[cfg(not(unix))]
{
observe::decline_open(ROW, "unix-unavailable", channel.id().number());
reply.reject(dispatch::connect_failed_open_decline()).await;
}
}
#[cfg(not(feature = "tunnel"))]
{
let _ = _socket_path;
}
Ok(())
}
/// The exec-grammar dispatch (ADR-006 point 3 as amended; the
/// full grammar, `w3-git-mapping`): the per-length cap checks the
/// wire bytes first (pre-parse, pre-allocation — decline first,
/// alloc never), then [`crate::dispatch::parse_exec`] routes:
/// a git route to the git row (feature-gated — a session-only
/// build declines it as its gate), a grammar declinable to its
/// honest request-level decline, anything else to the session
/// pipe row. Invalid UTF-8 parses to the pipe row (parse-then-
/// fail).
async fn exec_request(
&mut self,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
if data.len() > self.serve.limits.max_exec_command_len {
observe::decline_request("exec_request", "exec-oversize", channel.number(), None);
decline_unfeatured_request(channel, session).await;
return Ok(());
}
let command = std::str::from_utf8(data).unwrap_or("");
match dispatch::parse_exec(command) {
ExecRoute::Git { service, repo } => {
#[cfg(feature = "git")]
if self.serves_git_row() {
git_row_dispatch(self, channel, session, service, repo);
return Ok(());
}
#[cfg(not(feature = "git"))]
let _ = (&service, &repo);
observe::decline_request(
"exec_request",
"unfeatured",
channel.number(),
Some("git"),
);
decline_unfeatured_request(channel, session).await;
}
ExecRoute::Declined(decline) => {
observe::decline_request(
"exec_request",
decline.reason(),
channel.number(),
Some("git"),
);
decline_unfeatured_request(channel, session).await;
}
ExecRoute::Pipe => {
#[cfg(feature = "session")]
if self.serves_session_rows() {
pipe_row_dispatch(self, channel, session, command.to_owned());
return Ok(());
}
#[cfg(not(feature = "session"))]
let _ = &command;
observe::decline_request(
"exec_request",
"unfeatured",
channel.number(),
Some("pipe"),
);
decline_unfeatured_request(channel, session).await;
}
}
Ok(())
}
/// The `pty_request` row (`session`): the args →
/// `TerminalParamsWire` (ttymodes → the reserved `{}` JSON,
/// OQ-44), stored per channel for the later shell/exec request on
/// the same channel — after the seam check (a pty whose session
/// can never serve is a declined allocation,
/// `seam-unconfigured`). Unfeatured builds decline the
/// capability. The successful row answers `channel_success`
/// (POC-3 §D.1's shape — serving starts at the shell/exec
/// request).
async fn pty_request(
&mut self,
channel: ChannelId,
term: &str,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
_modes: &[(Pty, u32)],
session: &mut Session,
) -> Result<(), Self::Error> {
#[cfg(feature = "session")]
match self.serve.session_seam.clone() {
Some(_) => {
self.ptys.insert(
channel,
tty_map::terminal_params(term, col_width, row_height, pix_width, pix_height),
);
let _ = session.channel_success(channel);
}
None => {
spawn_seam_stub(channel, session, "pty_request", None, "seam-unconfigured");
}
}
#[cfg(not(feature = "session"))]
{
let _ = (term, col_width, row_height, pix_width, pix_height);
observe::decline_request("pty_request", "unfeatured", channel.number(), None);
decline_unfeatured_request(channel, session).await;
}
Ok(())
}
/// The `shell_request` row (`session`): the serve-config shell
/// argv, verbatim, as `NegotiateRequest.cmd`; the stored pty
/// params consumed (`None` → pipe-mode shell — decided: a pty-less
/// shell request is served, `ssh -T host` is legitimate vanilla);
/// unset-declines first (OQ-08: `shell_argv` unset →
/// `shell-unconfigured`; the seam unset → `seam-unconfigured`
/// — both the spawned serving-path shape). A routed request
/// answers `channel_success` inline; the exit chunk reports the
/// outcome, status-before-close from the spawned task.
async fn shell_request(
&mut self,
channel: ChannelId,
session: &mut Session,
) -> Result<(), Self::Error> {
#[cfg(feature = "session")]
match (
self.serve.shell_argv.clone(),
self.serve.session_seam.clone(),
) {
(None, _) => {
spawn_seam_stub(
channel,
session,
"shell_request",
None,
"shell-unconfigured",
);
}
(_, None) => {
spawn_seam_stub(channel, session, "shell_request", None, "seam-unconfigured");
}
(Some(shell_argv), Some(seam)) => {
let tty = self.ptys.remove(&channel);
let Some(taken) = self.take_stored_channel(channel) else {
observe::decline_request(
"shell_request",
"channel-not-held",
channel.number(),
None,
);
let _ = session.channel_failure(channel);
return Ok(());
};
let request = tty_map::negotiate_request(
seam.backend.clone(),
tty,
shell_argv,
self.serve.session_cwd.clone(),
self.serve.session_env.clone(),
);
spawn_tty_row(
self,
channel,
session,
"shell_request",
None,
taken,
request,
seam,
);
}
}
#[cfg(not(feature = "session"))]
{
observe::decline_request("shell_request", "unfeatured", channel.number(), None);
decline_unfeatured_request(channel, session).await;
}
Ok(())
}
/// `window_change_request` (`session`): the resize control row —
/// forwarded into the serving bridge's bounded control surface
/// (`try_send`, never awaited on the packet loop); for a channel
/// whose pty is stored but not yet serving, the stored params
/// resize instead (the final size wins — vanilla sshd's own
/// ordering); with neither, the event is dropped. Fire-and-forget
/// on the wire — no SSH reply exists, so a decline sends none
/// (the event is the record). Signal names: see [`Self::signal`].
async fn window_change_request(
&mut self,
channel: ChannelId,
col_width: u32,
row_height: u32,
pix_width: u32,
pix_height: u32,
_session: &mut Session,
) -> Result<(), Self::Error> {
#[cfg(feature = "session")]
{
let cols = tty_map::clamp_u16(col_width);
let rows = tty_map::clamp_u16(row_height);
let pix_w = tty_map::clamp_u16(pix_width);
let pix_h = tty_map::clamp_u16(pix_height);
if let Some(surface) = self.bridges.get(&channel) {
let message = alktty::ControlMessage::Resize {
cols,
rows,
pixel_width: pix_w,
pixel_height: pix_h,
};
let _ = surface.forward_control(&message);
} else if let Some(params) = self.ptys.get_mut(&channel) {
params.cols = cols;
params.rows = rows;
params.pixel_width = pix_w;
params.pixel_height = pix_h;
params.modes = serde_json::json!({});
}
}
#[cfg(not(feature = "session"))]
{
let _ = (col_width, row_height, pix_width, pix_height);
observe::decline_request(
"window_change_request",
"unfeatured",
channel.number(),
None,
);
}
Ok(())
}
/// The `signal` row (`session`): accepted verbatim — no
/// alkssh-side name filter (the backend owns the semantics;
/// alktty's control handle applies its own default kill for
/// names it rejects, and the pipe-mode pid-only vs PTY
/// process-group scope is backend semantics) — so there is no
/// decline shape: the request is fire-and-forget on the SSH
/// wire, and the bridge's bounded surface is the only path. An
/// unfeatured build emits the decline event with no reply.
async fn signal(
&mut self,
channel: ChannelId,
signal: Sig,
_session: &mut Session,
) -> Result<(), Self::Error> {
#[cfg(feature = "session")]
{
if let Some(surface) = self.bridges.get(&channel) {
let message = alktty::ControlMessage::Signal {
name: tty_map::sig_control_name(&signal),
};
let _ = surface.forward_control(&message);
}
}
#[cfg(not(feature = "session"))]
{
let _ = signal;
observe::decline_request("signal", "unfeatured", channel.number(), None);
}
Ok(())
}
/// Every subsystem name declines (unimplemented on this door):
/// `subsystem("sftp")` is the OQ-13 placeholder — the feature
/// exists, the route does not until alkfs lands; git service
/// names carry only a name, never a repository id, so git can
/// never dispatch as a subsystem (ADR-006 decision 2, POC-3
/// §A.3's forced decline); everything else is unimplemented. Wire
/// shape: `channel_failure`, the channel left usable (the
/// POC-1 §5 session-survives-declines posture).
async fn subsystem_request(
&mut self,
channel: ChannelId,
_name: &str,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("subsystem_request", "unimplemented", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
/// `env_request` declines unconditionally (OQ-09: no allowlist
/// ships — wire env is not family config; the assembly supplies
/// the session env through [`crate::ServeConfig::session_env`]).
async fn env_request(
&mut self,
channel: ChannelId,
_variable_name: &str,
_variable_value: &str,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("env_request", "declined-by-design", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
/// The `x11-req` request on a session channel: always declines
/// (never-feature, ADR-003 §5).
async fn x11_request(
&mut self,
channel: ChannelId,
_single_connection: bool,
_x11_auth_protocol: &str,
_x11_auth_cookie: &str,
_x11_screen_number: u32,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("x11_request", "never-feature", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
/// Agent forwarding on a session session-channel: always declines
/// (never-feature, ADR-003 §5 — agent forwarding is the
/// anti-pattern of the alkvault custody model). `false` is the
/// decline; russh replies the channel failure.
async fn agent_request(
&mut self,
channel: ChannelId,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_request("agent_request", "never-feature", channel.number(), None);
Ok(false)
}
/// 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,
_session: &mut Session,
) -> Result<bool, Self::Error> {
#[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)
}
}
/// 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,
_session: &mut Session,
) -> Result<bool, Self::Error> {
#[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 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,
_session: &mut Session,
) -> Result<bool, Self::Error> {
#[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)
}
}
/// 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,
_session: &mut Session,
) -> Result<bool, Self::Error> {
#[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 — the register is the collision pre-check
/// (`w4-router-collision`): a mint on an already-held token refuses
/// fail-closed (`token-collision`, the minted listener torn down, the
/// cap released — a collided mint can never steal a live route; the
/// decline rides `Ok(false)`, never the error taxonomy, since a
/// global-request hook's `Err` would kill the whole session) — 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);
}
};
let token = minted.token;
let route = tunnel_map::ListenRoute {
echo,
substrate,
handle: Arc::new(session.handle()),
cap: Arc::clone(&handler.channel_cap),
cap_value: handler.serve.limits.max_concurrent_channels,
};
if let Err(_collision) = router.register(token, route) {
handler.listener_cap.release();
observe::decline_global_request(row, "token-collision");
serve::spawn_serving_path(async move {
minted.listener.close().await;
});
return Ok(false);
}
if let (Some(port), Some(bound_port)) = (port, tunnel_map::bound_port(&bound)) {
*port = bound_port;
}
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 entry leaves the session map and the route
/// leaves the shared router before any family teardown spawns (a
/// racing arrival that has not yet cloned its route is already the
/// correlation-miss class — no post-cancel half-serve; one that
/// cloned just before completes independently — the recorded
/// caveat), 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
/// event is emitted by the caller (the hook carries the row/reason
/// context); this is the wire shape only.
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
let _ = session.channel_failure(channel);
}
/// The spawned serving-path decline body (`w3-git-mapping`'s shape,
/// reused by `w3-tty-session`'s config-driven declines): fail the
/// request and close the channel, through the cloned handle the task
/// was handed — the vanilla-clean shape for a routed row that owns
/// the request's whole outcome. The helper name keeps the wave-3
/// history (the decline IS the path it spawns). Decline-first-alloc-
/// never holds structurally: nothing is allocated before the failure
/// lands — the spawn's handle clone is the only resource ever taken.
///
/// The serving-path lifecycle events ride this body
/// ([`crate::observe`]): started (the task's first action), the
/// serving-path decline (class `serving`, the caller's vocabulary
/// reason), and — after the wire reporting has completed — ended, or
/// failed when a report send could not complete (the one warn in the
/// vocabulary).
#[cfg(any(feature = "git", feature = "session"))]
async fn seam_stub_decline(
handle: Handle,
channel: ChannelId,
row: &'static str,
exec_route: Option<&'static str>,
reason: &'static str,
) {
observe::serving_path_started(row, channel.number(), exec_route);
observe::decline_serving(row, reason, channel.number(), exec_route);
let error = match handle.channel_failure(channel).await {
Ok(()) => handle.close(channel).await.err(),
Err(error) => Some(error),
};
match error {
Some(error) => observe::serving_path_failed(row, channel.number(), &error, exec_route),
None => observe::serving_path_ended(row, channel.number(), exec_route),
}
}
/// Spawns a serving-path decline as the row's serving path (ADR-006
/// decision 4): the handle is cloned here, the decline runs on its own
/// task, and the hook returns — no serving path awaits inline. The
/// row never serves, so the decline *is* the path: failure + close,
/// event-shaped ([`crate::observe`]) with the caller's reason.
#[cfg(any(feature = "git", feature = "session"))]
fn spawn_seam_stub(
channel: ChannelId,
session: &mut Session,
row: &'static str,
exec_route: Option<&'static str>,
reason: &'static str,
) {
let handle = session.handle();
serve::spawn_serving_path(seam_stub_decline(handle, channel, row, exec_route, reason));
}
/// The git row's routing stage (feature-gated, `w3-git-mapping`):
/// validation of the normalized id ([`crate::git_map::repo_id_validates`] —
/// reply-shaped `invalid-repo-id`), the seam-unconfigured decline
/// ([`Self::serves_git_row`] alone configures nothing — the
/// unset-declines contract), then the store-and-take handoff and the
/// spawned alkgit dispatch ([`crate::git_map::serve_git_row`], the
/// resolved identity riding along). A routed exec answers
/// `channel_success` inline (the request-reply vocabulary: the request
/// routed; the serving outcome reports through exit-status later —
/// the POC-3 §A.5 shape), never awaited serving.
#[cfg(feature = "git")]
fn git_row_dispatch(
handler: &mut SessionHandler,
channel: ChannelId,
session: &mut Session,
service: dispatch::GitServiceRoute,
repo: String,
) {
if !git_map::repo_id_validates(&repo) {
observe::decline_request(
"exec_request",
"invalid-repo-id",
channel.number(),
Some("git"),
);
let _ = session.channel_failure(channel);
return;
}
let Some(seam) = handler.serve.git_seam.clone() else {
spawn_seam_stub(
channel,
session,
"exec_request",
Some("git"),
"seam-unconfigured",
);
return;
};
let Some(taken) = handler.take_stored_channel(channel) else {
observe::decline_request(
"exec_request",
"channel-not-held",
channel.number(),
Some("git"),
);
let _ = session.channel_failure(channel);
return;
};
#[cfg(feature = "session")]
{
let _ = handler.ptys.remove(&channel);
}
let identity = handler.resolved_identity.clone();
let handle = session.handle();
serve::spawn_serving_path(git_map::serve_git_row(
handle, taken, service, repo, identity, seam,
));
let _ = session.channel_success(channel);
}
/// The session pipe row's dispatch (`w3-tty-session`): unset-declines
/// first (OQ-08 — `exec_wrapper_argv` unset → `wrapper-unconfigured`;
/// the seam unset → `seam-unconfigured`; both the spawned serving-path
/// shape, never a fallback), then the store-and-take handoff
/// (`channel-not-held` → the inline unvanilla decline, the git row's
/// shape), the wire command appended to the wrapper argv (`wrapper ++
/// [cmd]` — never naively split), the stored pty params consumed if a
/// pty preceded, and the spawned bridge. A routed exec answers
/// `channel_success` inline (the request routed; the exit chunk
/// reports the serving outcome later).
#[cfg(feature = "session")]
fn pipe_row_dispatch(
handler: &mut SessionHandler,
channel: ChannelId,
session: &mut Session,
command: String,
) {
let Some(mut cmd) = handler.serve.exec_wrapper_argv.clone() else {
spawn_seam_stub(
channel,
session,
"exec_request",
Some("pipe"),
"wrapper-unconfigured",
);
return;
};
let Some(seam) = handler.serve.session_seam.clone() else {
spawn_seam_stub(
channel,
session,
"exec_request",
Some("pipe"),
"seam-unconfigured",
);
return;
};
let tty = handler.ptys.remove(&channel);
let Some(taken) = handler.take_stored_channel(channel) else {
observe::decline_request(
"exec_request",
"channel-not-held",
channel.number(),
Some("pipe"),
);
let _ = session.channel_failure(channel);
return;
};
cmd.push(command);
let request = tty_map::negotiate_request(
seam.backend.clone(),
tty,
cmd,
handler.serve.session_cwd.clone(),
handler.serve.session_env.clone(),
);
spawn_tty_row(
handler,
channel,
session,
"exec_request",
Some("pipe"),
taken,
request,
seam,
);
}
/// Installs the per-channel bridge surface, clones the session
/// handle, and spawns the spawned serving path — the session rows'
/// spawn step (ADR-006 decision 4: the handler returns fast; the
/// routed request answers `channel_success` inline).
#[cfg(feature = "session")]
#[allow(clippy::too_many_arguments)]
fn spawn_tty_row(
handler: &mut SessionHandler,
channel: ChannelId,
session: &mut Session,
row: &'static str,
exec_route: Option<&'static str>,
taken: Channel<Msg>,
request: NegotiateRequest,
seam: SessionSeam,
) {
let (surface, chunk_rx, cancel_rx) = tty_map::build_surface();
let stdin_tx = surface.sender.clone();
handler.bridges.insert(channel, surface);
let identity = handler.resolved_identity.clone();
let handle = session.handle();
serve::spawn_serving_path(tty_map::serve_tty_row(
row,
exec_route,
handle,
taken,
request,
identity,
seam,
stdin_tx,
chunk_rx,
cancel_rx,
handler.teardown.subscribe(),
));
let _ = session.channel_success(channel);
}
/// The direct tunnel rows' dispatch (`w3-tunnel-direct`): the
/// pre-admit decline classes first — **decline first, alloc never**
/// applies to the cap budget too, so every consumer is checked before
/// the admission is taken. Order (the recorded admission mechanics:
/// the admission is taken at the hook — the packet-loop's single
/// admit-writer site, no race window against the cap check — and held
/// across the fabric open):
///
/// 1. **The unset fabric handle decline** (`seam-unconfigured`, the
/// unset-declines posture — the feature alone configures nothing):
/// `ConnectFailed` ([`crate::dispatch::connect_failed_open_decline`])
/// — the bridge cannot be attempted at all.
/// 2. **The `no-identity` guard** — unreachable on the wire (russh
/// authorizes before channels exist) and fail-closed: never
/// anonymous serving (the git/tty rows' shape, discharged here for
/// the tunnel row — the parked `w2-auth-bridge` item; the
/// `set_identity` mechanics the rides records live in
/// [`crate::tunnel_map`]).
/// 3. **The impossible-target decline** (`invalid-target` — an
/// out-of-range wire port): `ConnectFailed`.
///
/// Then the cap admission ([`Self::admit_channel_open`]; beyond it →
/// `cap-exceeded`), and the spawn: the admitted count rides into
/// [`crate::tunnel_map::spawn_direct_row`], which releases it exactly
/// once on the failure path (no `channel_close` can ever fire for a
/// rejected open) and never on the success path (whose close fires
/// [`Self::channel_close`] when the established channel closes).
#[cfg(feature = "tunnel")]
async fn direct_open_dispatch(
handler: &SessionHandler,
row: &'static str,
channel: Channel<Msg>,
reply: ChannelOpenHandle,
target: Option<alktunnels::params::SubstrateAddr>,
substrate: alktunnels::params::Substrate,
) {
let channel_number = channel.id().number();
let Some(client) = handler.serve.channel_client.clone() else {
observe::decline_open(row, "seam-unconfigured", channel_number);
reply.reject(dispatch::connect_failed_open_decline()).await;
return;
};
if handler.resolved_identity().is_none() {
observe::decline_open(row, "no-identity", channel_number);
reply
.reject(russh::ChannelOpenFailure::AdministrativelyProhibited)
.await;
return;
}
let Some(target) = target else {
observe::decline_open(row, "invalid-target", channel_number);
reply.reject(dispatch::connect_failed_open_decline()).await;
return;
};
if !handler.admit_channel_open() {
observe::decline_open(row, "cap-exceeded", channel_number);
reply.reject(dispatch::cap_exceeded_open_decline()).await;
return;
}
tunnel_map::spawn_direct_row(
row,
channel,
reply,
target,
substrate,
client,
Arc::clone(&handler.channel_cap),
);
}