feat(dispatch): land the parse-only dispatch + the fail-closed decline core (w2-dispatch-decline-core)
- src/dispatch.rs: the ExecRoute decision, the skeleton exec-parse probe (service prefix + path shape; wave 3 replaces with the full grammar), the wire-reason shapers (AdministrativelyProhibited for unfeatured/never-feature opens, ConnectFailed for the tunnel seam stub), 7 probe unit rows - src/handler.rs: channel_open_* gates per the feature graph (session ∨ git; tunnel; x11/auth-agent never), parse-only exec_request dispatch before any allocation, session-row routes, request-level declines (subsystem all-names with the sftp OQ-13 and git named classes, env per OQ-09, x11/agent, the four listen requests), the named wave-3 seam stubs; decline-first-alloc-never structural; module docs carry the decline-core contract incl. the inherited auth-agent-open upstream note - tests/dispatch_core.rs: 26 Contract-stamped decline-matrix rows per feature build driven through the loopback harness (none / git-only / session-only / tunnel-only / all-features + shared never-feature rows) - tests/common/harness.rs: extended helper vocabulary (tunnel/x11 opens, pty/env/x11/agent requests, listen requests) — discharges the w2-loopback-harness live-rows note - tests/loopback_harness.rs: the skeleton decline-only session-open row cfg-pinned to the featureless builds it describes Verification: cargo test (91 rows, default build); per-feature dispatch_core rows (git 11 / session 10 / tunnel 4 / all 8 / sftp 3); cargo test --all-features; cargo clippy --all-targets -- -D warnings (and --all-features); cargo fmt --check; cargo doc --no-deps
This commit is contained in:
1 parent
3b77193e85
commit
68d3723f6a
7 files changed
+1543
-12
No files matched your search
+169
@@ -0,0 +1,169 @@
|
||||
//! 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
|
||||
}
|
||||
|
||||
#[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}'"));
|
||||
}
|
||||
}
|
||||
+436
-4
@@ -1,7 +1,7 @@
|
||||
//! The per-connection server handler — the ADR-004 dispositional auth
|
||||
//! bridge, with the channel/request dispatch surface still standing at
|
||||
//! its inherited fail-closed russh defaults (`w2-dispatch-decline-core`
|
||||
//! fills those; `w2-auth-bridge` filled the auth hooks).
|
||||
//! bridge, plus the parse-only dispatch and the fail-closed decline
|
||||
//! core (`w2-dispatch-decline-core`; `w2-auth-bridge` filled the auth
|
||||
//! hooks).
|
||||
//!
|
||||
//! ## The auth bridge (ADR-004, decisions 1–5)
|
||||
//!
|
||||
@@ -33,6 +33,76 @@
|
||||
//! 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 `tunnel`-featured rows decline at the
|
||||
//! family-seam stub ([`crate::dispatch::tunnel_seam_stub_decline`])
|
||||
//! until wave 3's route-1 bridge replaces it. 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
|
||||
//! (a git-parseable command routes to the git row, anything else to
|
||||
//! the session pipe row — [`crate::dispatch::classify_exec`]);
|
||||
//! `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 channel handle
|
||||
//! a confirmed `session` open produces is *not* consumed this wave —
|
||||
//! the store-and-take handoff (`channels.remove(&id)` → `into_stream`
|
||||
//! in the spawned serving task) is wave-3 mapping material; the
|
||||
//! routing decisions around it are already this wave's, because the
|
||||
//! routing stubs decline at the seam.
|
||||
//!
|
||||
//! 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` / the cancel forms) decline — they are
|
||||
//! `tunnel` rows whose serving flow is wave-3 material, so featured
|
||||
//! builds decline them too this wave.
|
||||
//!
|
||||
//! ## 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 → administratively
|
||||
//! prohibited; the tunnel seam stub → 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 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); a
|
||||
//! *serving-path* decline (the wave-3 seam stubs) answers failure
|
||||
//! and closes the channel — the vanilla-clean shape for a row that
|
||||
//! will spawn its serving path next wave.
|
||||
//!
|
||||
//! ## Test surface
|
||||
//!
|
||||
//! The handler type is deliberately constructible outside this module
|
||||
@@ -48,9 +118,11 @@ use std::sync::Arc;
|
||||
|
||||
use alkcall::core::auth::Identity;
|
||||
use russh::keys::Algorithm;
|
||||
use russh::server::{Auth, PublicKeyOffer, VerifiedPublicKey};
|
||||
use russh::server::{Auth, ChannelOpenHandle, Msg, PublicKeyOffer, Session, VerifiedPublicKey};
|
||||
use russh::{Channel, ChannelId, Pty};
|
||||
|
||||
use crate::config::ServeConfig;
|
||||
use crate::dispatch;
|
||||
use crate::error::AdapterError;
|
||||
use crate::fingerprint::fingerprint;
|
||||
|
||||
@@ -116,6 +188,33 @@ impl SessionHandler {
|
||||
.identity_provider
|
||||
.resolve_from_fingerprint(&fingerprint(public_key))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn serves_session_rows(&self) -> bool {
|
||||
cfg!(feature = "session")
|
||||
}
|
||||
|
||||
/// The git-row gate (git-parseable exec): only the `git` feature
|
||||
/// serves this row.
|
||||
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 {
|
||||
@@ -186,4 +285,337 @@ impl russh::server::Handler for SessionHandler {
|
||||
) -> Result<Auth, Self::Error> {
|
||||
Ok(Auth::reject())
|
||||
}
|
||||
|
||||
/// The session-channel open gate (ADR-003 feature graph,
|
||||
/// `session` ∨ `git`): unfeatured builds decline at the open —
|
||||
/// never a half-open channel. A confirmed open accepts the
|
||||
/// channel; the parse-only dispatch serves it 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() {
|
||||
reply.accept().await;
|
||||
} else {
|
||||
reply.reject(dispatch::unfeatured_open_decline()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The x11 channel open: **always declines** — a never-feature
|
||||
/// (ADR-003 §5), declined in every build, never feature-gated.
|
||||
async fn channel_open_x11(
|
||||
&mut self,
|
||||
_channel: Channel<Msg>,
|
||||
_originator_address: &str,
|
||||
_originator_port: u32,
|
||||
reply: ChannelOpenHandle,
|
||||
_session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
reply.reject(dispatch::unfeatured_open_decline()).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `direct-tcpip` open gate (`tunnel`): unfeatured declines
|
||||
/// at the open; featured routes to the tunnel seam stub — the
|
||||
/// route-1 bridge is wave-3 material, and the stub's decline is
|
||||
/// the explicit `ConnectFailed` reject shape ADR-006 decision 1
|
||||
/// pins (accept only after the fabric open succeeds; the skeleton
|
||||
/// never opens the fabric).
|
||||
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> {
|
||||
if self.serves_tunnels() {
|
||||
tunnel_row_stub(reply).await;
|
||||
} else {
|
||||
reply.reject(dispatch::unfeatured_open_decline()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `forwarded-tcpip` open gate (`tunnel` — the `-R` data
|
||||
/// leg): the same gate/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.
|
||||
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> {
|
||||
if self.serves_tunnels() {
|
||||
tunnel_row_stub(reply).await;
|
||||
} else {
|
||||
reply.reject(dispatch::unfeatured_open_decline()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `direct-streamlocal` open gate (`tunnel`): the same
|
||||
/// gate/stub split as the tcp rows. The runtime-Unix gate (OQ-11)
|
||||
/// gates *serving* on Unix; this wave the stub declines
|
||||
/// everywhere, so the runtime gate has nothing to ride yet.
|
||||
async fn channel_open_direct_streamlocal(
|
||||
&mut self,
|
||||
_channel: Channel<Msg>,
|
||||
_socket_path: &str,
|
||||
reply: ChannelOpenHandle,
|
||||
_session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.serves_tunnels() {
|
||||
tunnel_row_stub(reply).await;
|
||||
} else {
|
||||
reply.reject(dispatch::unfeatured_open_decline()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The parse-only exec dispatch (ADR-006 point 3 as amended):
|
||||
/// parse first ([`crate::dispatch::classify_exec`]) — before any
|
||||
/// allocation, decline first, alloc never — then route on the
|
||||
/// *feature graph* the route carries: a git-parseable command to
|
||||
/// the git row (a session-only build declines here — the git
|
||||
/// parse is its gate), 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> {
|
||||
let command = std::str::from_utf8(data).unwrap_or("");
|
||||
match dispatch::classify_exec(command) {
|
||||
dispatch::ExecRoute::Git => {
|
||||
if self.serves_git_row() {
|
||||
git_row_stub(channel, session).await;
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
}
|
||||
dispatch::ExecRoute::Pipe => {
|
||||
if self.serves_session_rows() {
|
||||
pipe_row_stub(channel, session).await;
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `pty_request` row route (`session`): featured routes to
|
||||
/// the session-row seam stub; unfeatured declines the capability.
|
||||
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> {
|
||||
if self.serves_session_rows() {
|
||||
pty_row_stub(channel, session).await;
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `shell_request` row route (`session`): the same shape as
|
||||
/// `pty_request`. The OQ-08 unset-shell check is the serving
|
||||
/// path's consumption (wave 3): this wave the stub declines the
|
||||
/// row regardless, so the unset-declines posture already holds.
|
||||
async fn shell_request(
|
||||
&mut self,
|
||||
channel: ChannelId,
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.serves_session_rows() {
|
||||
shell_row_stub(channel, session).await;
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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.
|
||||
async fn tcpip_forward(
|
||||
&mut self,
|
||||
_address: &str,
|
||||
_port: &mut u32,
|
||||
_session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Nothing is ever registered this wave — the cancel form
|
||||
/// declines accordingly.
|
||||
async fn cancel_tcpip_forward(
|
||||
&mut self,
|
||||
_address: &str,
|
||||
_port: u32,
|
||||
_session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
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.
|
||||
async fn streamlocal_forward(
|
||||
&mut self,
|
||||
_socket_path: &str,
|
||||
_session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Nothing is ever registered this wave — the cancel form
|
||||
/// declines accordingly.
|
||||
async fn cancel_streamlocal_forward(
|
||||
&mut self,
|
||||
_socket_path: &str,
|
||||
_session: &mut Session,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
|
||||
let _ = session.channel_failure(channel);
|
||||
}
|
||||
|
||||
/// The family-seam decline shape for a **routed-but-not-yet-served**
|
||||
/// row: fail the request and close the channel — the vanilla-clean
|
||||
/// shape for a serving path that would have owned the channel (wave 3
|
||||
/// replaces these stubs with the spawned serving paths; a declined
|
||||
/// seam never leaks a half-served channel, since the decline IS the
|
||||
/// row this wave). Decline-first-alloc-never holds structurally:
|
||||
/// nothing is allocated before the failure lands.
|
||||
async fn seam_stub_decline(channel: ChannelId, session: &mut Session) {
|
||||
let _ = session.channel_failure(channel);
|
||||
let _ = session.close(channel);
|
||||
}
|
||||
|
||||
/// The git row's seam stub — wave 3 lands the alkgit seam here
|
||||
/// (`parse command → Service + repo → serve_duplex_resolved`), spawned
|
||||
/// per ADR-006 decision 4; the skeleton declines at the seam.
|
||||
async fn git_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
}
|
||||
|
||||
/// The session pipe row's seam stub — wave 3 lands the alktty pipe
|
||||
/// seam here (`wrapper ++ [cmd]`, the OQ-08 argv append), spawned per
|
||||
/// ADR-006 decision 4; the skeleton declines at the seam.
|
||||
async fn pipe_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
}
|
||||
|
||||
/// The session pty row's seam stub — wave 3 lands the alktty
|
||||
/// `NegotiateRequest.tty` construction here
|
||||
/// (`drive_session_pre_negotiated`), spawned per ADR-006 decision 4;
|
||||
/// the skeleton declines at the seam.
|
||||
async fn pty_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
}
|
||||
|
||||
/// The session shell row's seam stub — wave 3 lands the alktty
|
||||
/// `NegotiateRequest.cmd` = serve-config shell argv here (OQ-08, no
|
||||
/// crate default), spawned per ADR-006 decision 4; the skeleton
|
||||
/// declines at the seam.
|
||||
async fn shell_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
}
|
||||
|
||||
/// The tunnel row's seam stub: the explicit `ConnectFailed` reject
|
||||
/// ([`crate::dispatch::tunnel_seam_stub_decline`]) — wave 3 lands the
|
||||
/// route-1 bridge here (`TunnelSession::open_direct` → accept only
|
||||
/// after the fabric open succeeds → `take_halves` pump).
|
||||
async fn tunnel_row_stub(reply: ChannelOpenHandle) {
|
||||
reply.reject(dispatch::tunnel_seam_stub_decline()).await;
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
mod adapter;
|
||||
mod config;
|
||||
mod dispatch;
|
||||
mod error;
|
||||
mod fingerprint;
|
||||
mod handler;
|
||||
|
||||
Reference in new issue
Block a user