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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: w2-dispatch-decline-core
|
||||
name: Wave 2 — parse-only dispatch + the fail-closed decline core
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [w2-loopback-harness]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -93,8 +93,122 @@ declining stubs this task shapes the routes for.
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- `src/dispatch.rs` (new): the parse-only dispatch core — the
|
||||
`ExecRoute` decision (`Git` / `Pipe`), the skeleton exec-parse
|
||||
probe (`classify_exec`: exact `git-upload-pack`/`git-receive-pack`
|
||||
service prefix + space + path-shaped argument — leading `'` or
|
||||
`/`; everything else → the pipe row), and the two wire-decline
|
||||
reason shapers (`unfeatured_open_decline` →
|
||||
`AdministrativelyProhibited`, `tunnel_seam_stub_decline` →
|
||||
`ConnectFailed`). Seven probe unit rows pin the POC-3 stage-A
|
||||
grammar facts the skeleton carries. The full grammar (quoted/
|
||||
unquoted paths, per-length limits, service-only
|
||||
decline-with-reason unknown-service rows) is wave-3's to replace
|
||||
it with.
|
||||
- `src/handler.rs`: every `channel_open_*` hook gates per the feature
|
||||
graph (session open: `session` ∨ `git`; the three tunnel opens:
|
||||
`tunnel`; x11: always declines). Request hooks: `exec_request`
|
||||
parses first then routes on the feature graph; `pty`/`shell` route
|
||||
under `session`; `subsystem_request` declines every name (sftp =
|
||||
the OQ-13 placeholder, `git-*` = no repository id in a name-only
|
||||
request — ADR-006 decision 2); `env_request` declines
|
||||
unconditionally (OQ-09); `x11_request`/`agent_request` decline
|
||||
(ADR-003 §5); the four listen requests decline (the flow is wave
|
||||
3; never advertise a listen that cannot serve). Decline-first-
|
||||
alloc-never holds structurally: no hook allocates before its
|
||||
decline; a confirmed session open accepts and drops the channel
|
||||
handle (nothing is held this wave — the store-and-take handoff is
|
||||
wave-3 mapping material, and the routing decisions around it are
|
||||
already pinned by the seam stubs).
|
||||
- **Wire decline reasons (the wave-1 error-model reservation, now
|
||||
spent):** open-level declines carry channel-open failure codes —
|
||||
unfeatured and never-feature rows answer `AdministrativelyProhibited`
|
||||
(1) is the honest class: without the feature there is no capability
|
||||
to distinguish; the tunnel seam stub answers `ConnectFailed` (2),
|
||||
the explicit reject shape ADR-006 decision 1 pins for failed fabric
|
||||
opens (accept only after the fabric open succeeds; the skeleton
|
||||
never opens the fabric). Request-level declines carry
|
||||
`channel_failure` — the SSH request vocabulary has no failure text,
|
||||
so the "reason" is the routing decision, documented per build.
|
||||
Within request declines, two shapes: a *capability* decline answers
|
||||
failure and leaves the channel usable (POC-1 §5 posture, proven
|
||||
live by the channel-alive rows); a *serving-path* decline (the seam
|
||||
stubs) answers failure then closes the channel (vanilla clients
|
||||
never hang on a row that will spawn next wave).
|
||||
- **russh 0.65 facts coded around:** only `agent_request` and the
|
||||
global requests auto-reply from the handler's `bool`; every other
|
||||
request hook must call `session.channel_failure` itself. The bare
|
||||
`auth-agent@openssh.com` (and `forwarded-streamlocal@openssh.com`)
|
||||
channel *types* have no handler hook — russh rejects those opens
|
||||
inline (administratively prohibited); that is the decline the
|
||||
never-feature contract needs but it is an inherited posture flagged
|
||||
in the handler docs (pin-bump change would appear as
|
||||
accepted-then-dropped — upstream note). `forwarded-tcpip` has the
|
||||
same gate/stub code shape as its tcp sibling but no client-side
|
||||
open helper (the server opens it toward the client), so its wire
|
||||
rows are wave-3 acceptor material.
|
||||
- **Harness:** the helper vocabulary extended (tunnel/x11 opens; pty/
|
||||
env/x11/agent-forward requests; `tcpip_forward`), discharging the
|
||||
loopback-harness task's recorded expectation that the request
|
||||
helpers' live rows arrive here. One vehicle row
|
||||
(`authenticated_session_serves_and_declines_with_an_observed_reason`)
|
||||
recorded the *skeleton* decline-only behavior; the dispatch core
|
||||
makes session opens serve under their features, so that row is now
|
||||
cfg-pinned to the featureless builds it speaks about (its
|
||||
`ChannelOpenFailure`-reason observation remains the none-build's
|
||||
proof).
|
||||
- **Request-less session channels** (ADR-006 as amended): parse-only
|
||||
means nothing serves before a request arrives (the SOCKS-detect is
|
||||
retired); a request-less channel is served by nothing and dies at
|
||||
the wall clock. The wire-observable rows are the unfeatured opens
|
||||
(none/tunnel builds decline session channels at the open); the
|
||||
no-detect posture is structural (no serving path exists outside
|
||||
the request hooks).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Landed the parse-only dispatch + the fail-closed decline core
|
||||
(`w2-dispatch-decline-core`) in three files plus the harness:
|
||||
|
||||
- **`src/dispatch.rs`** — the `ExecRoute` decision, the skeleton
|
||||
exec-parse probe (service prefix + path shape; wave-3 grammar
|
||||
replaces), the wire-reason shapers (`AdministrativelyProhibited`
|
||||
for unfeatured/never-feature opens, `ConnectFailed` for the tunnel
|
||||
seam stub), 7 probe unit rows.
|
||||
- **`src/handler.rs`** — the `channel_open_*` gates (session ∨ git;
|
||||
tunnel; x11/auth-agent never), the parse-only `exec_request`
|
||||
dispatch, the session-row routes, the request-level declines
|
||||
(subsystem all-names with the sftp/git named classes, env, x11,
|
||||
agent), the four listen-request declines, and the named seam
|
||||
stubs (`git_row_stub`, `pipe_row_stub`, `pty_row_stub`,
|
||||
`shell_row_stub`, `tunnel_row_stub`) — the wave-3 replacement
|
||||
points, each documented with its family seam. Module docs now
|
||||
carry the decline-core contract (gates, decline classes, wire
|
||||
reasons, decline-first-alloc-never, the parse-only posture, the
|
||||
inherited-auth-agent-open upstream note).
|
||||
- **`tests/dispatch_core.rs`** — the skeleton decline-matrix rows per
|
||||
feature build, 26 rows total, each with a `Contract stamp:`:
|
||||
shared rows in every build (x11 open, `tcpip-forward` decline,
|
||||
x11/agent requests where sessions open); none-build
|
||||
(everything declines at open); git-only (sessions open, git
|
||||
exec → seam-stub failure+close, plain exec/unknown services →
|
||||
pipe-row decline with channel alive, pty/shell/subsystem/env/capability
|
||||
declines, tunnel unfeatured); session-only (git-parseable exec
|
||||
declines as gate, stub rows failure+close, capability declines,
|
||||
tunnel unfeatured); tunnel-only (session opens decline, featured
|
||||
tunnel rows → `ConnectFailed` stubs); all-features (routes hold
|
||||
with the full graph — git row, pipe row, pty/shell stubs,
|
||||
`ConnectFailed` stubs, capability declines).
|
||||
- **`tests/common/harness.rs`** — the declined-vocabulary helpers
|
||||
(tunnel/x11 opens, pty/env/x11/agent requests, listen requests),
|
||||
module doc updated to record the discharge of the
|
||||
`w2-loopback-harness` note.
|
||||
|
||||
Verification (all green): `cargo test` (91 rows across 7 binaries) ·
|
||||
`cargo test --features git/session/tunnel/sftp --test dispatch_core`
|
||||
(each build's rows) · `cargo test --all-features` ·
|
||||
`cargo clippy --all-targets -- -D warnings` ·
|
||||
`cargo clippy --all-features --all-targets -- -D warnings` ·
|
||||
`cargo fmt --check` · `cargo doc --no-deps` (no warnings). One
|
||||
pre-existing harness row pinned to the skeleton's decline-only open
|
||||
behavior was cfg-gated to the featureless builds it describes.
|
||||
@@ -56,6 +56,16 @@
|
||||
//! russh-verbatim shape ([`AuthResult`], [`Error`],
|
||||
//! [`ChannelMsg`]) the rows match on directly.
|
||||
//!
|
||||
//! The helper set grows with the rows it serves: `w2-loopback-harness`
|
||||
//! landed the auth/session/exec/shell/subsystem vocabulary, and
|
||||
//! `w2-dispatch-decline-core` extended it with the decline-matrix
|
||||
//! vocabulary (the tunnel/x11 opens, the pty/env/x11/agent-forward
|
||||
//! requests, the listen requests) — its recorded expectation that the
|
||||
//! "session-request and exit-status/close helpers are exercised by
|
||||
//! construction this wave ... their live rows arrive with
|
||||
//! `w2-dispatch-decline-core`" is discharged by exactly that
|
||||
//! extension, not by a harness rewrite.
|
||||
//!
|
||||
//! Like the doubles: test functions are *rows* carrying the greppable
|
||||
//! **`Contract stamp:`** doc markers (see the common module's doc).
|
||||
|
||||
@@ -264,6 +274,40 @@ pub async fn open_session_channel(
|
||||
client.channel_open_session().await
|
||||
}
|
||||
|
||||
/// Opens a `direct-tcpip` channel — the tunnel open helper (`ssh -L`
|
||||
/// / `ssh -D` traffic). The open-failure shape is the `Err` verbatim,
|
||||
/// the decline-matrix rows' reason surface.
|
||||
pub async fn open_direct_tcpip(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
host_to_connect: &str,
|
||||
port_to_connect: u32,
|
||||
) -> Result<Channel<Msg>, Error> {
|
||||
client
|
||||
.channel_open_direct_tcpip(host_to_connect, port_to_connect, "harness-originator", 0)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Opens a `direct-streamlocal` channel — the unix-socket forward
|
||||
/// helper. The open-failure shape is the `Err` verbatim.
|
||||
pub async fn open_direct_streamlocal(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
socket_path: &str,
|
||||
) -> Result<Channel<Msg>, Error> {
|
||||
client.channel_open_direct_streamlocal(socket_path).await
|
||||
}
|
||||
|
||||
/// Opens an x11 channel — the never-feature open helper (ADR-003 §5).
|
||||
/// The open-failure shape is the `Err` verbatim.
|
||||
pub async fn open_x11_channel(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
originator_address: &str,
|
||||
originator_port: u32,
|
||||
) -> Result<Channel<Msg>, Error> {
|
||||
client
|
||||
.channel_open_x11(originator_address, originator_port)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends an exec request over a channel (`want_reply` set — the
|
||||
/// success/failure shape rides [`ChannelMsg::Success`]/[`ChannelMsg::Failure`]
|
||||
/// through [`next_message`]).
|
||||
@@ -281,6 +325,43 @@ pub async fn request_subsystem(channel: &Channel<Msg>, name: &str) -> Result<(),
|
||||
channel.request_subsystem(true, name).await
|
||||
}
|
||||
|
||||
/// Sends a pty request over a channel (`want_reply` set — the pty row
|
||||
/// rides the standard success/failure observation).
|
||||
pub async fn request_pty(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
channel.request_pty(true, "xterm", 80, 24, 0, 0, &[]).await
|
||||
}
|
||||
|
||||
/// Sends an env request over a channel (`want_reply` set — the OQ-09
|
||||
/// unconditional-decline rows' observation).
|
||||
pub async fn request_env(channel: &Channel<Msg>, name: &str, value: &str) -> Result<(), Error> {
|
||||
channel.set_env(true, name, value).await
|
||||
}
|
||||
|
||||
/// Sends an x11-req request over a channel (`want_reply` set — the
|
||||
/// never-feature request rows).
|
||||
pub async fn request_x11(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
channel
|
||||
.request_x11(true, false, "MIT-MAGIC-COOKIE-1", "harness-cookie", 0)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends an agent-forwarding request over a channel (`want_reply` set
|
||||
/// — the never-feature request rows; ADR-003 §5).
|
||||
pub async fn request_agent_forward(channel: &Channel<Msg>) -> Result<(), Error> {
|
||||
channel.agent_forward(true).await
|
||||
}
|
||||
|
||||
/// Requests a `tcpip-forward` listen (`-R` ingress) — the
|
||||
/// success/failure shape is the result verbatim
|
||||
/// ([`Error::RequestDenied`] on a declined listen).
|
||||
pub async fn tcpip_forward(
|
||||
client: &Handle<HarnessClientHandler>,
|
||||
address: &str,
|
||||
port: u32,
|
||||
) -> Result<u32, Error> {
|
||||
client.tcpip_forward(address, port).await
|
||||
}
|
||||
|
||||
/// Observes the next channel message — `None` after the channel has
|
||||
/// closed (russh's EOF-of-conversation shape).
|
||||
pub async fn next_message(channel: &mut Channel<Msg>) -> Option<ChannelMsg> {
|
||||
|
||||
@@ -0,0 +1,730 @@
|
||||
//! The wave-2 dispatch/decline-core contract tests
|
||||
//! (`w2-dispatch-decline-core`) — the skeleton rows of the decline
|
||||
//! matrix (`mapping.md` §Decline matrix), driven per feature build
|
||||
//! through the loopback harness. This task tests the rows it lands;
|
||||
//! the full per-build matrix suite is wave-5's.
|
||||
//!
|
||||
//! Feature builds are compile-time, so the rows are feature-gated to
|
||||
//! the build whose matrix row they pin: the shared rows
|
||||
//! (never-feature `x11`, the `tcpip-forward` listen decline) run in
|
||||
//! every build; the capability rows run in the builds that open their
|
||||
//! subject (a session channel needs `session` or `git`); the
|
||||
//! per-build matrix rows are pinned to exactly one feature combination
|
||||
//! (`not(...)` guards) so `--all-features` runs neither a
|
||||
//! git-only row nor a session-only row by accident. Every feature
|
||||
//! build is exercised by its own verification run
|
||||
//! (`cargo test [--features …]`), not by cfg tricks.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::harness::{
|
||||
LoopbackSession, adapter, authenticate_publickey, await_close, drive, next_message,
|
||||
open_session_channel, open_x11_channel, tcpip_forward,
|
||||
};
|
||||
use common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity};
|
||||
use russh::client::Msg;
|
||||
use russh::{Channel, ChannelOpenFailure, Error};
|
||||
|
||||
use alkssh::Limits;
|
||||
|
||||
/// Mounts an authenticated loopback session over the doubles — the
|
||||
/// dispatch rows' driver: fixture `alpha` resolves through
|
||||
/// `MemIdentityProvider`, host keys `bravo`, the permissive host-key
|
||||
/// posture (the explicit test choice).
|
||||
async fn authenticated() -> LoopbackSession {
|
||||
let provider = Arc::new(MemIdentityProvider::new());
|
||||
provider.register(
|
||||
fixture("alpha").fingerprint,
|
||||
identity("dispatch-core", &["ssh"]),
|
||||
);
|
||||
let adapter = adapter(provider, &[fixture("bravo")]).await;
|
||||
|
||||
let policy = Arc::new(MemHostKeyPolicy::with_default(true));
|
||||
let mut session = drive(adapter, Arc::clone(&policy), Limits::default())
|
||||
.await
|
||||
.expect("the loopback mounts");
|
||||
|
||||
let result =
|
||||
authenticate_publickey(&mut session.client, "dispatch-core", fixture("alpha")).await;
|
||||
assert!(
|
||||
result.success(),
|
||||
"the fixture identity authenticates before every dispatch row"
|
||||
);
|
||||
session
|
||||
}
|
||||
|
||||
/// Matches an open failure's SSH reason — the wire decline reason the
|
||||
/// open-gate rows assert.
|
||||
fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) {
|
||||
assert!(
|
||||
matches!(
|
||||
&err,
|
||||
Error::ChannelOpenFailure(reason) if *reason == expected
|
||||
),
|
||||
"{context}: expected open decline with {expected:?}, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The request-level decline observation: the channel answers Failure.
|
||||
/// A capability decline's channel stays open afterwards — the POC-1 §5
|
||||
/// session-survives-declines posture the channel-alive rows lean on.
|
||||
async fn expect_failure(channel: &mut Channel<Msg>, context: &str) {
|
||||
match next_message(channel).await {
|
||||
Some(russh::ChannelMsg::Failure) => {}
|
||||
other => panic!("{context}: expected a channel failure, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The serving-path (family-seam stub) decline observation: Failure
|
||||
/// then Close — the vanilla-clean shape for a row whose serving path
|
||||
/// would own the channel.
|
||||
async fn expect_failure_then_close(channel: &mut Channel<Msg>, context: &str) {
|
||||
expect_failure(channel, context).await;
|
||||
assert!(
|
||||
await_close(channel).await,
|
||||
"{context}: the seam stub closes the channel after the failure"
|
||||
);
|
||||
}
|
||||
|
||||
/// Proves a declined channel is still usable — a follow-up request
|
||||
/// reaches the dispatch again instead of the channel being dead.
|
||||
async fn expect_channel_alive(channel: &mut Channel<Msg>) {
|
||||
common::harness::request_subsystem(channel, "sftp")
|
||||
.await
|
||||
.expect("the still-open channel carries another request");
|
||||
expect_failure(channel, "the surviving channel still dispatches").await;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared rows — run in every feature build.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature — declined
|
||||
/// in every build, never feature-gated).
|
||||
#[tokio::test]
|
||||
async fn x11_channel_opens_decline_in_every_build() {
|
||||
let session = authenticated().await;
|
||||
let err = open_x11_channel(&session.client, "127.0.0.1", 0)
|
||||
.await
|
||||
.expect_err("x11 opens decline");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"the never-feature x11 open",
|
||||
);
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Global-request rows (the `-R`
|
||||
/// listen flow is a `tunnel` row declined when the flow cannot
|
||||
/// serve — this wave: every build).
|
||||
#[tokio::test]
|
||||
async fn tcpip_forward_listen_requests_decline_in_every_build() {
|
||||
let session = authenticated().await;
|
||||
let err = tcpip_forward(&session.client, "127.0.0.1", 8080)
|
||||
.await
|
||||
.expect_err("the listen request declines");
|
||||
assert!(
|
||||
matches!(err, Error::RequestDenied),
|
||||
"expected the declined listen's RequestDenied, got {err:?}"
|
||||
);
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row,
|
||||
/// request side: `x11` / `auth-agent` requests decline; ADR-003 §5).
|
||||
#[cfg(any(feature = "git", feature = "session"))]
|
||||
#[tokio::test]
|
||||
async fn x11_and_agent_requests_decline_wherever_sessions_open() {
|
||||
use common::harness::{request_agent_forward, request_x11};
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens where a session-serving feature exists");
|
||||
|
||||
request_x11(&channel)
|
||||
.await
|
||||
.expect("the x11-req rides the wire");
|
||||
expect_failure(&mut channel, "the never-feature x11 request declines").await;
|
||||
|
||||
request_agent_forward(&channel)
|
||||
.await
|
||||
.expect("the agent-forward request rides the wire");
|
||||
expect_failure(&mut channel, "the never-feature agent request declines").await;
|
||||
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default build (no features): everything declines (mapping.md
|
||||
// §Decline matrix — "none (default): declines everything").
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(any(feature = "session", feature = "git", feature = "tunnel")))]
|
||||
mod default_build {
|
||||
use super::*;
|
||||
use common::harness::{open_direct_streamlocal, open_direct_tcpip};
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (none-build
|
||||
/// row: every channel type declines at open — session channels
|
||||
/// as unfeatured, tunnel rows as unfeatured — plus the
|
||||
/// never-feature x11).
|
||||
#[tokio::test]
|
||||
async fn every_channel_open_declines_in_a_default_build() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let err = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect_err("session channels decline unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"the unfeatured session open",
|
||||
);
|
||||
|
||||
let err = open_direct_tcpip(&session.client, "internal.example", 5432)
|
||||
.await
|
||||
.expect_err("direct-tcpip declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"the unfeatured direct-tcpip open",
|
||||
);
|
||||
|
||||
let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock")
|
||||
.await
|
||||
.expect_err("direct-streamlocal declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"the unfeatured direct-streamlocal open",
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// git-only build: routes git-parseable exec to the git row (stub at
|
||||
// the seam); declines pty/shell, plain exec, all other opens
|
||||
// (mapping.md §Decline matrix — git row).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(all(feature = "git", not(feature = "session"), not(feature = "tunnel")))]
|
||||
mod git_build {
|
||||
use super::*;
|
||||
use common::harness::{
|
||||
open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_shell,
|
||||
request_subsystem,
|
||||
};
|
||||
|
||||
/// - *Contract stamp:* the task's channel-open gate (session
|
||||
/// channels open where a session-serving feature exists — `git`
|
||||
/// qualifies, the parse path lives inside session channels).
|
||||
#[tokio::test]
|
||||
async fn session_channels_open_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("session channels open under the git feature");
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-006 point 3 (parse-only: a
|
||||
/// git-parseable exec routes to the git row — the seam stub
|
||||
/// declines at the family-seam boundary with the serving-path
|
||||
/// shape: failure then close).
|
||||
#[tokio::test]
|
||||
async fn git_parseable_exec_routes_to_the_git_row_stub() {
|
||||
let session = authenticated().await;
|
||||
for command in [
|
||||
"git-upload-pack '/owner/demo'",
|
||||
"git-receive-pack '/owner/demo'",
|
||||
] {
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, command)
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure_then_close(
|
||||
&mut channel,
|
||||
&format!("the git row serves its seam stub for {command}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row:
|
||||
/// plain exec declines; the parse-then-fail shape of POC-3
|
||||
/// stage A — the channel survives the decline).
|
||||
#[tokio::test]
|
||||
async fn plain_exec_declines_and_keeps_the_channel_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, "uptime")
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure(&mut channel, "the unfeatured pipe row declines").await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* POC-3 §2 (unknown git services route off
|
||||
/// the git row — the skeleton probe's honest surface: they take
|
||||
/// the pipe-row route, not the git row's close-shape).
|
||||
#[tokio::test]
|
||||
async fn unknown_git_services_route_to_the_pipe_row() {
|
||||
let session = authenticated().await;
|
||||
for command in ["git-send-pack '/r'", "git-upload-archive '/r'"] {
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, command)
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure(
|
||||
&mut channel,
|
||||
&format!("an unknown service does not reach the git row: {command}"),
|
||||
)
|
||||
.await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
}
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (git-build row:
|
||||
/// pty/shell and all session requests decline).
|
||||
#[tokio::test]
|
||||
async fn pty_shell_and_capability_requests_decline_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_shell(&channel)
|
||||
.await
|
||||
.expect("the shell request rides the wire");
|
||||
expect_failure(&mut channel, "the shell row declines unfeatured").await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (subsystem:
|
||||
/// `sftp` is the OQ-13 placeholder decline; git service names
|
||||
/// carry no repository id — ADR-006 decision 2).
|
||||
#[tokio::test]
|
||||
async fn subsystems_decline_with_their_named_reasons_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
for name in ["sftp", "git-upload-pack"] {
|
||||
request_subsystem(&channel, name)
|
||||
.await
|
||||
.expect("the subsystem request rides the wire");
|
||||
expect_failure(
|
||||
&mut channel,
|
||||
&format!("the subsystem {name} declines with reason"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows:
|
||||
/// `env_request` declines unconditionally — no allowlist ships).
|
||||
#[tokio::test]
|
||||
async fn env_requests_decline_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_env(&channel, "GIT_DIR", "/srv/git")
|
||||
.await
|
||||
.expect("the env request rides the wire");
|
||||
expect_failure(&mut channel, "the env request declines").await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (git-build
|
||||
/// row: all other channel types decline — tunnel unfeatured).
|
||||
#[tokio::test]
|
||||
async fn tunnel_opens_decline_unfeatured_in_a_git_build() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let err = open_direct_tcpip(&session.client, "internal.example", 5432)
|
||||
.await
|
||||
.expect_err("direct-tcpip declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"direct-tcpip",
|
||||
);
|
||||
|
||||
let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock")
|
||||
.await
|
||||
.expect_err("direct-streamlocal declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"direct-streamlocal",
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// session-only build: declines git-parseable exec (the git parse is
|
||||
// its gate) and serves its rows only to the seam stubs; declines
|
||||
// tunnel opens (mapping.md §Decline matrix — session row).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(all(feature = "session", not(feature = "git"), not(feature = "tunnel")))]
|
||||
mod session_build {
|
||||
use super::*;
|
||||
use common::harness::{
|
||||
open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_pty,
|
||||
request_shell, request_subsystem,
|
||||
};
|
||||
|
||||
/// - *Contract stamp:* the task's channel-open gate (session
|
||||
/// channels open where a session-serving feature exists).
|
||||
#[tokio::test]
|
||||
async fn session_channels_open_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("session channels open under the session feature");
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (session row:
|
||||
/// git-parseable exec declines — the `git` parse is its gate;
|
||||
/// the channel survives the decline, POC-1 §5).
|
||||
#[tokio::test]
|
||||
async fn git_parseable_exec_declines_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, "git-upload-pack '/owner/demo'")
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure(
|
||||
&mut channel,
|
||||
"the git parse gates the row off in a session build",
|
||||
)
|
||||
.await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* ADR-006 point 3 (parse-only: non-git exec
|
||||
/// routes to the session pipe row — the seam stub declines at
|
||||
/// the family-seam boundary: failure then close).
|
||||
#[tokio::test]
|
||||
async fn plain_exec_routes_to_the_pipe_row_stub() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, "uptime")
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the pipe row serves its seam stub").await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Channel-type rows
|
||||
/// (`session` → `pty_request` + `shell_request` is a `session`
|
||||
/// row; the stub decline is the seam shape).
|
||||
#[tokio::test]
|
||||
async fn pty_and_shell_route_to_their_row_stubs() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_pty(&channel)
|
||||
.await
|
||||
.expect("the pty request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the pty row serves its seam stub").await;
|
||||
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_shell(&channel)
|
||||
.await
|
||||
.expect("the shell request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the shell row serves its seam stub").await;
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (subsystem:
|
||||
/// the OQ-13 placeholder and git service names decline-with-
|
||||
/// reason in every session-serving build).
|
||||
#[tokio::test]
|
||||
async fn subsystems_decline_with_their_named_reasons_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
for name in ["sftp", "git-upload-pack"] {
|
||||
request_subsystem(&channel, name)
|
||||
.await
|
||||
.expect("the subsystem request rides the wire");
|
||||
expect_failure(
|
||||
&mut channel,
|
||||
&format!("the subsystem {name} declines with reason"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows:
|
||||
/// `env_request` declines unconditionally).
|
||||
#[tokio::test]
|
||||
async fn env_requests_decline_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_env(&channel, "GIT_DIR", "/srv/git")
|
||||
.await
|
||||
.expect("the env request rides the wire");
|
||||
expect_failure(&mut channel, "the env request declines").await;
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Decline matrix (session row:
|
||||
/// other channel types decline — tunnel unfeatured).
|
||||
#[tokio::test]
|
||||
async fn tunnel_opens_decline_unfeatured_in_a_session_build() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let err = open_direct_tcpip(&session.client, "internal.example", 5432)
|
||||
.await
|
||||
.expect_err("direct-tcpip declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"direct-tcpip",
|
||||
);
|
||||
|
||||
let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock")
|
||||
.await
|
||||
.expect_err("direct-streamlocal declines unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"direct-streamlocal",
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tunnel-only build: session channels decline (auto-reject); the
|
||||
// tunnel rows route to the tunnel seam stub even with the feature on
|
||||
// (the route-1 bridge is wave 3) — mapping.md §Decline matrix (tunnel
|
||||
// row, as landed this wave).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(all(feature = "tunnel", not(feature = "git"), not(feature = "session")))]
|
||||
mod tunnel_build {
|
||||
use super::*;
|
||||
use common::harness::{open_direct_streamlocal, open_direct_tcpip};
|
||||
|
||||
/// - *Contract stamp:* ADR-006 point 3 as amended (parse-only
|
||||
/// dispatch: a request-less/deserving-less session channel
|
||||
/// declines as unfeatured — a tunnel-only build has no
|
||||
/// session-serving feature, so the open declines).
|
||||
#[tokio::test]
|
||||
async fn session_channels_decline_in_a_tunnel_build() {
|
||||
let session = authenticated().await;
|
||||
let err = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect_err("session channels decline unfeatured");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::AdministrativelyProhibited,
|
||||
"the unfeatured session open in a tunnel build",
|
||||
);
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* the task's channel-open gate (featured
|
||||
/// tunnel rows route to the seam stub; the stub's decline is
|
||||
/// the explicit `ConnectFailed` reject ADR-006 decision 1 pins
|
||||
/// for failed fabric opens).
|
||||
#[tokio::test]
|
||||
async fn tunnel_rows_route_to_the_seam_stub_in_a_featured_build() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let err = open_direct_tcpip(&session.client, "internal.example", 5432)
|
||||
.await
|
||||
.expect_err("the featured direct-tcpip row is a declining seam stub");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::ConnectFailed,
|
||||
"the direct-tcpip seam stub",
|
||||
);
|
||||
|
||||
let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock")
|
||||
.await
|
||||
.expect_err("the featured direct-streamlocal row is a declining seam stub");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::ConnectFailed,
|
||||
"the direct-streamlocal seam stub",
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// All features: the routing decisions hold with the full graph on —
|
||||
// every serving path is still a seam stub wave-3 material, and the
|
||||
// decline classes hold.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(all(feature = "git", feature = "session", feature = "tunnel"))]
|
||||
mod all_features {
|
||||
use super::*;
|
||||
use common::harness::{
|
||||
open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_pty,
|
||||
request_shell, request_subsystem,
|
||||
};
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Session-channel dispatch (the
|
||||
/// git-parseable exec routes to the git row in the full build).
|
||||
#[tokio::test]
|
||||
async fn git_exec_routes_to_the_git_row_stub_with_all_features() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, "git-upload-pack '/owner/demo'")
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the git row serves its seam stub").await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Session-channel dispatch
|
||||
/// (non-git exec routes to the session pipe row in the full
|
||||
/// build).
|
||||
#[tokio::test]
|
||||
async fn plain_exec_routes_to_the_pipe_row_stub_with_all_features() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_exec(&channel, "uptime")
|
||||
.await
|
||||
.expect("the exec request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the pipe row serves its seam stub").await;
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Channel-type rows (the
|
||||
/// pty/shell rows route within the session channel).
|
||||
#[tokio::test]
|
||||
async fn pty_and_shell_route_to_their_stubs_with_all_features() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_pty(&channel)
|
||||
.await
|
||||
.expect("the pty request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the pty row serves its seam stub").await;
|
||||
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
request_shell(&channel)
|
||||
.await
|
||||
.expect("the shell request rides the wire");
|
||||
expect_failure_then_close(&mut channel, "the shell row serves its seam stub").await;
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* the task's channel-open gate (the tunnel
|
||||
/// rows are seam stubs even with the feature on — the route-1
|
||||
/// bridge is wave 3; `ConnectFailed` is the stub's class).
|
||||
#[tokio::test]
|
||||
async fn tunnel_rows_route_to_the_seam_stub_with_all_features() {
|
||||
let session = authenticated().await;
|
||||
|
||||
let err = open_direct_tcpip(&session.client, "internal.example", 5432)
|
||||
.await
|
||||
.expect_err("the full-featured direct-tcpip row is a declining seam stub");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::ConnectFailed,
|
||||
"the direct-tcpip seam stub",
|
||||
);
|
||||
|
||||
let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock")
|
||||
.await
|
||||
.expect_err("the full-featured direct-streamlocal row is a declining seam stub");
|
||||
open_failure(
|
||||
err,
|
||||
ChannelOpenFailure::ConnectFailed,
|
||||
"the direct-streamlocal seam stub",
|
||||
);
|
||||
|
||||
session.unmount().await;
|
||||
}
|
||||
|
||||
/// - *Contract stamp:* mapping.md §Request-level rows (the
|
||||
/// capability declines — sftp placeholder, git subsystem
|
||||
/// names, env — hold with everything on).
|
||||
#[tokio::test]
|
||||
async fn capability_declines_hold_with_all_features() {
|
||||
let session = authenticated().await;
|
||||
let mut channel = open_session_channel(&session.client)
|
||||
.await
|
||||
.expect("the session channel opens");
|
||||
|
||||
request_subsystem(&channel, "sftp")
|
||||
.await
|
||||
.expect("the subsystem request rides the wire");
|
||||
expect_failure(&mut channel, "the sftp placeholder declines").await;
|
||||
|
||||
request_subsystem(&channel, "git-upload-pack")
|
||||
.await
|
||||
.expect("the subsystem request rides the wire");
|
||||
expect_failure(&mut channel, "the git subsystem declines").await;
|
||||
|
||||
request_env(&channel, "GIT_DIR", "/srv/git")
|
||||
.await
|
||||
.expect("the env request rides the wire");
|
||||
expect_failure(&mut channel, "the env request declines").await;
|
||||
|
||||
expect_channel_alive(&mut channel).await;
|
||||
session.unmount().await;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ use std::time::Duration;
|
||||
|
||||
use alkcall::core::auth::IdentityProvider;
|
||||
use common::harness::{
|
||||
LOOPBACK_HOST, LOOPBACK_PORT, authenticate_publickey, client_config, drive,
|
||||
open_session_channel, remaining_methods, serve_config,
|
||||
LOOPBACK_HOST, LOOPBACK_PORT, authenticate_publickey, client_config, drive, remaining_methods,
|
||||
serve_config,
|
||||
};
|
||||
use common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity};
|
||||
use russh::{Error, MethodSet};
|
||||
@@ -90,11 +90,15 @@ async fn in_process_pair_connects_and_authenticates_end_to_end() {
|
||||
|
||||
/// - *Contract stamp:* ADR-001 (the authenticated session is live and
|
||||
/// serving over the mount) + `mapping.md` §Decline matrix (the
|
||||
/// skeleton's fail-closed decline is observable through the harness
|
||||
/// as a ChannelOpenFailure reason — the *specific* reason is
|
||||
/// `w2-dispatch-decline-core`'s row, not this vehicle's).
|
||||
/// fail-closed decline is observable through the harness as a
|
||||
/// ChannelOpenFailure reason — the *specific* reason is
|
||||
/// `w2-dispatch-decline-core`'s row, which landed it; in builds with
|
||||
/// a session-serving feature this row never sees a decline, so it
|
||||
/// is pinned to the featureless builds it speaks about).
|
||||
#[cfg(not(any(feature = "session", feature = "git")))]
|
||||
#[tokio::test]
|
||||
async fn authenticated_session_serves_and_declines_with_an_observed_reason() {
|
||||
use common::harness::open_session_channel;
|
||||
let provider = registered_provider(&[fixture("alpha").fingerprint]);
|
||||
let adapter = SshAdapter::new(serve_config(
|
||||
provider as Arc<dyn IdentityProvider>,
|
||||
|
||||
Reference in new issue
Block a user