Files
alkssh/src/dispatch.rs
T
glm-5.3-flash 196289f5ad feat(handler): enforce the in-crate concurrent-channel cap at the open surface (w2-bounded-resources)
- new private channel_cap module: per-session active-channel count;
  admit (check+increment at accept sites) / has_room / saturating
  release, single-writer (inline packet-loop hooks), unit-tested
- handler wires the cap at every channel_open_* hook: the session
  accept path admits-and-counts at the accept point; the tunnel rows
  cap-gate before the seam stub (a counted-but-unopened stub would
  leak the cap); x11 never consumes a count; the count releases
  through the new channel_close hook
- dispatch::cap_exceeded_open_decline -> AdministrativelyProhibited
  (the assembly's policy ceiling; OpenSSH MaxSessions parity)
- bounded-resources contract rows: cap reached -> clean decline,
  close -> recovery open succeeds (wire-order deterministic),
  zero cap refuses without poisoning, exhausted cap answers the
  tunnel row with the cap reason; max_auth_attempts verified
  behaviorally (within-ceiling failures keep the advertisement, the
  past-ceiling attempt is the NoMoreAuthMethodsAvailable teardown);
  window/packet/buffer/inactivity knobs recorded as covered by the
  config-mapping tests
- max_exec_command_len stays carried, not enforced (wave-3 grammar)

Verification: cargo test (default), --features session / git /
tunnel / --all-features, clippy --all-targets (-D warnings, also
--all-features), fmt --check, cargo doc --no-deps - all green
2026-10-28 11:40:29 +00:00

182 lines
7.4 KiB
Rust

//! The parse-only dispatch core (`w2-dispatch-decline-core`) — the
//! routing decision and the wire-decline reason shaping the
//! per-connection handler hooks speak ([`crate::handler`]).
//!
//! **Parse-only** (ADR-006 point 3 as amended): an `exec_request`
//! parses *first* — a git-parseable command routes to the git row,
//! anything else routes to the session pipe row; `pty_request` /
//! `shell_request` route to the session rows. Parsing happens before
//! dispatch and before any resource allocation — **decline first,
//! alloc never** (a leaked channel alloc is a capability leak, the
//! POC-3 stage-A rule). There is no request-less serving mode: a
//! session channel that never receives a request is never served (the
//! SOCKS-detect retired with `dynamic-forward`, ADR-003 §1a), so a
//! request-less session channel declines as unfeatured by
//! construction.
//!
//! ## The skeleton exec parse probe
//!
//! [`classify_exec`] is deliberately **skeleton-level** (service
//! prefix + path shape): it recognizes the two serving git services
//! (`git-upload-pack`, `git-receive-pack`) followed by a space and a
//! path-shaped argument (leading `'` — the vanilla-git quoted shape —
//! or `/` — unquoted absolute). Everything else routes to the session
//! pipe row, which in every build that lacks the `session` feature
//! declines: the POC-3 stage-A contract (unknown repo → collapsed
//! generic denial before any protocol byte; non-git command →
//! parse-then-fail). The full grammar (quoted/unquoted paths,
//! per-length limits from [`crate::Limits::max_exec_command_len`],
//! service-only → decline-with-reason, `git-send-pack` /
//! `git-upload-archive` → unknown-service decline) is wave-3's git
//! task, which replaces this probe wholesale — the [`ExecRoute`] rows
//! and the routing decisions around them are what it must keep.
use russh::ChannelOpenFailure;
/// Which dispatch row an `exec_request` command routes to (the
/// parse-only decision, ADR-006 point 3 as amended).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExecRoute {
/// A git-parseable command — routes to the git row (the `git`
/// feature gate is the row's other half; the parse is the session
/// build's gate — a session-only build declines here).
Git,
/// Everything else — routes to the session pipe row (the
/// `session` feature gate is the row's other half; unfeatured
/// builds decline here).
Pipe,
}
/// The skeleton exec parse probe — see the module docs for the exact
/// shape it recognizes and what wave 3 replaces it with.
pub(crate) fn classify_exec(command: &str) -> ExecRoute {
if is_serving_git_command(command) {
ExecRoute::Git
} else {
ExecRoute::Pipe
}
}
/// The skeleton probe's git recognition: exact serving-service prefix,
/// single separating space, path-shaped argument.
fn is_serving_git_command(command: &str) -> bool {
for service in ["git-upload-pack ", "git-receive-pack "] {
if let Some(argument) = command.strip_prefix(service)
&& is_path_shaped(argument)
{
return true;
}
}
false
}
/// The path shape the skeleton probe accepts: a path-shaped argument
/// starts with `'` (quoted — the vanilla-git wire shape) or `/`
/// (unquoted absolute).
fn is_path_shaped(argument: &str) -> bool {
argument.starts_with('\'') || argument.starts_with('/')
}
/// The wire decline reason for a **featureless** row — a known channel
/// type this build serves nothing of (unfeatured or never-feature):
/// `administratively prohibited`, the vanilla-observable policy
/// refusal (POC-1 §5 — channel-scoped, auth intact, no hangs). A
/// never-feature (`x11`, auth-agent) and an unfeatured feature row
/// share this class honestly: without the feature there is no
/// capability to configure, so nothing distinguishable exists.
pub(crate) fn unfeatured_open_decline() -> ChannelOpenFailure {
ChannelOpenFailure::AdministrativelyProhibited
}
/// The wire decline reason for the **tunnel seam stub** — the row is
/// feature-gated `tunnel` but the route-1 bridge (wave 3) does not
/// exist yet, so the fabric open never happened: `connect failed`, the
/// explicit reject shape ADR-006 decision 1 pins (accept only after
/// the fabric open succeeds; failure maps to an explicit SSH
/// `ConnectFailed`). Wave 3 replaces the stub with the real
/// accept-after-open flow.
pub(crate) fn tunnel_seam_stub_decline() -> ChannelOpenFailure {
ChannelOpenFailure::ConnectFailed
}
/// The wire decline reason for an open beyond the **in-crate
/// concurrent-channel cap** (`overview.md` §Bounded-resources policy,
/// `w2-bounded-resources`): `administratively prohibited` — the cap is
/// the assembly's own configured policy ceiling, refused fail-closed
/// at the open (never queued, never half-open). OpenSSH's own
/// `MaxSessions` ceiling answers the same code, so vanilla clients
/// see the vanilla-shaped policy refusal (POC-1 §5: channel-scoped,
/// auth intact, no hangs).
pub(crate) fn cap_exceeded_open_decline() -> ChannelOpenFailure {
ChannelOpenFailure::AdministrativelyProhibited
}
#[cfg(test)]
mod tests {
use super::{ExecRoute, classify_exec};
fn route(command: &str) -> bool {
classify_exec(command) == ExecRoute::Git
}
/// - *Contract stamp:* ADR-006 decision 2 (the exec-only git parse
/// — the vanilla wire shape `git-upload-pack '/owner/demo'`).
#[test]
fn vanilla_quoted_upload_pack_parse_is_git() {
assert!(route("git-upload-pack '/owner/demo'"));
}
/// - *Contract stamp:* ADR-006 decision 2 (the receive-pack half
/// of the exec-only git parse).
#[test]
fn receive_pack_parse_is_git() {
assert!(route("git-receive-pack '/owner/demo'"));
}
/// - *Contract stamp:* POC-3 §2 (the unquoted-absolute row of the
/// grammar table: `git-upload-pack /r`).
#[test]
fn unquoted_absolute_pack_parse_is_git() {
assert!(route("git-upload-pack /r"));
}
/// - *Contract stamp:* POC-3 §2 (service-only → declined — SSH
/// exec must carry the repo; the skeleton routes it to the pipe
/// row, which declines in every git-less build; wave 3 gives the
/// decline its own reason).
#[test]
fn service_only_parse_is_not_git() {
assert!(!route("git-upload-pack"));
assert!(!route("git-receive-pack "));
}
/// - *Contract stamp:* POC-3 §2 (unknown service → declined
/// honestly; the skeleton routes it off the git row).
#[test]
fn unknown_services_parse_is_not_git() {
assert!(!route("git-send-pack '/r'"));
assert!(!route("git-upload-archive '/r'"));
}
/// - *Contract stamp:* mapping.md §Session-channel dispatch
/// (non-git → parse-then-fail, the pipe row).
#[test]
fn non_git_commands_parse_to_the_pipe_row() {
assert!(!route("uptime"));
assert!(!route(""));
assert!(!route("git-upload-packrel"));
assert!(!route("git-upload-pack rel"));
assert!(!route("GIT-UPLOAD-PACK '/r'"));
assert!(!route("git-upload-pack$ '/r'"));
assert!(!route("git-upload-pack '/r'"));
}
/// The probe is total: it returns a route for any input, so the
/// dispatch never allocates on a parse failure.
#[test]
fn probe_is_total_over_arbitrary_input() {
assert!(!route("\u{0}\u{1}"));
assert!(route("git-upload-pack '/\u{e9}'"));
}
}