//! The `w3-decline-tracing` wire rows — the event presence pin: every //! decline class in `src/` (the open-level declines, the request-level //! capability declines, the seam-stub serving declines with their //! spawned-path lifecycle, and the auth dispositions) emits its //! event through the vocabulary (`src/observe.rs`), driven over the //! real wire through the loopback harness. //! //! **Test convention (recorded):** the family's only tracing-test //! precedent is alktunnels' fmt-subscriber dev-dep, used for debug //! *output* — there is no event-capture convention to adopt, so these //! rows pin the convention: a hand-rolled minimal //! [`tracing::Subscriber`] capturing events into a shared buffer, //! installed once as the global default (the server side runs on //! spawned tasks — a thread-local default cannot see them; a global //! capture needs no extra dependency). The install is up front — the //! mount helpers call [`capture`] before any wire exchange, so no //! event is emitted below the not-yet-installed global. Rows assert //! presence by field-matching (`row` + `decline_class` + `reason` + //! build), and the vocabulary assertion checks each captured decline's exact //! field set — the shape the three wave-3 mapping branches inherit. //! The helper-level shapes (field order, the `warn` level of the //! failure arm) are pinned in `src/observe.rs`'s own unit rows. //! //! Feature builds are compile-time, so the build-specific rows are //! feature-gated to the build whose decline matrix they drive (the //! `dispatch_core.rs` convention); the shared decline rows run in //! every build. #![allow(dead_code)] mod common; use std::sync::{Arc, Mutex, OnceLock}; use std::time::{Duration, Instant}; use common::harness::{ adapter, authenticate_publickey, await_close, drive, next_message, open_x11_channel, tcpip_forward, }; use common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity}; use russh::client::Msg; use russh::{Channel, ChannelMsg}; use alkssh::Limits; // --------------------------------------------------------------------------- // The capture subscriber — the recorded test convention (see module docs). // --------------------------------------------------------------------------- #[derive(Debug, Clone)] struct Captured { level: String, fields: Vec<(String, String)>, } impl Captured { fn field(&self, name: &str) -> Option<&str> { self.fields .iter() .find(|(n, _)| n == name) .map(|(_, v)| v.as_str()) } fn has(&self, want: &[(&str, &str)]) -> bool { want.iter() .all(|(n, v)| self.field(n).is_some_and(|got| got == *v)) } } struct FieldVisitor(Vec<(String, String)>); impl tracing::field::Visit for FieldVisitor { fn record_str(&mut self, field: &tracing::field::Field, value: &str) { self.0.push((field.name().to_owned(), value.to_owned())); } fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { self.0.push((field.name().to_owned(), format!("{value:?}"))); } } #[derive(Default)] struct GlobalCapture(Mutex>); impl tracing::Subscriber for GlobalCapture { fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool { true } fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::Id { static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); tracing::Id::from_u64(NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) } fn record(&self, _span: &tracing::Id, _values: &tracing::span::Record<'_>) {} fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {} fn event(&self, event: &tracing::Event<'_>) { let mut visitor = FieldVisitor(Vec::new()); event.record(&mut visitor); self.0.lock().unwrap().push(Captured { level: event.metadata().level().to_string(), fields: visitor.0, }); } fn enter(&self, _span: &tracing::Id) {} fn exit(&self, _span: &tracing::Id) {} fn clone_span(&self, _id: &tracing::Id) -> tracing::Id { tracing::Id::from_u64(1) } } fn capture() -> &'static Arc { static CAPTURE: OnceLock> = OnceLock::new(); CAPTURE.get_or_init(|| { let capture = Arc::new(GlobalCapture::default()); tracing::subscriber::set_global_default(Arc::clone(&capture)) .expect("the observe suite installs the global capture exactly once"); capture }) } /// The shared capture snapshot — every test reads the whole buffer and /// matches fields, so parallel rows never order against each other. fn captured() -> Vec { capture().0.lock().unwrap().clone() } /// Waits until an event matching every field pair appears (the /// spawned-task surfaces land their events asynchronously), then /// returns it. async fn await_event(want: &[(&str, &str)]) -> Captured { let deadline = Instant::now() + Duration::from_secs(10); loop { if let Some(event) = captured().into_iter().find(|e| e.has(want)) { return event; } if Instant::now() > deadline { panic!("no tracing event matching {want:?} within the window"); } tokio::time::sleep(Duration::from_millis(10)).await; } } /// The expected `feature_build` value, derived from `cfg!` — the /// vocabulary assertion checks every captured decline against it (the /// same convention as `src/observe.rs`'s cfg-derived unit row). fn expected_build() -> &'static str { match ( cfg!(feature = "session"), cfg!(feature = "git"), cfg!(feature = "tunnel"), cfg!(feature = "sftp"), ) { (false, false, false, false) => "none", (true, false, false, false) => "session", (false, true, false, false) => "git", (false, false, true, false) => "tunnel", (false, false, false, true) => "sftp", (true, true, false, false) => "session+git", (true, false, true, false) => "session+tunnel", (true, false, false, true) => "session+sftp", (false, true, true, false) => "git+tunnel", (false, true, false, true) => "git+sftp", (false, false, true, true) => "tunnel+sftp", (true, true, true, false) => "session+git+tunnel", (true, true, false, true) => "session+git+sftp", (true, false, true, true) => "session+tunnel+sftp", (false, true, true, true) => "git+tunnel+sftp", (true, true, true, true) => "session+git+tunnel+sftp", } } /// The global-request rows — the no-channel surfaces (the event /// vocabulary assertion needs to know the field set these carry). fn is_global_row(row: Option<&str>) -> bool { row.is_some_and(|row| { matches!( row, "tcpip_forward" | "cancel_tcpip_forward" | "streamlocal_forward" | "cancel_streamlocal_forward" ) }) } /// The vocabulary assertion: a captured decline carries the exact /// field set (nothing more — the safety posture rides on this: no /// dynamic extra fields exist), with the build/class/reason/row the /// caller matched on. fn assert_decline(event: &Captured, want: &[(&str, &str)]) { assert!( event.has(want), "decline event missing the matched fields {want:?}: {event:?}" ); assert_eq!( event.field("feature_build"), Some(expected_build()), "every event is self-describing per build" ); let class = event .field("decline_class") .expect("a decline event carries its class"); let row = event.field("row"); let mut expected: Vec = ["feature_build", "row", "decline_class", "reason", "message"] .iter() .map(|n| n.to_string()) .collect(); let channel_carrying = class != "auth" && !is_global_row(row); if channel_carrying { assert!( event.field("channel_id").is_some(), "a channel decline carries the wire channel id: {event:?}" ); expected.push("channel_id".to_owned()); } else { assert!( event.field("channel_id").is_none(), "an auth/global decline has no channel id: {event:?}" ); } if row == Some("exec_request") { expected.push("exec_route".to_owned()); assert!( event.field("exec_route").is_some(), "an exec-row decline carries the parse route: {event:?}" ); } let mut names: Vec = event.fields.iter().map(|(n, _)| n.clone()).collect(); names.sort(); expected.sort(); assert_eq!( names, expected, "a decline carries exactly the vocabulary, nothing more (no dynamic fields)" ); } // --------------------------------------------------------------------------- // Shared rows — run in every feature build. // --------------------------------------------------------------------------- /// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature — the open /// declines in every build; the event pins the deciding context). #[tokio::test] async fn x11_open_decline_emits_the_never_feature_event() { let session = authenticated("x11-open-row").await; open_x11_channel(&session.client, "127.0.0.1", 0) .await .expect_err("x11 opens decline"); let event = await_event(&[ ("row", "x11"), ("decline_class", "open"), ("reason", "never-feature"), ]) .await; assert_decline(&event, &[("row", "x11"), ("reason", "never-feature")]); session.unmount().await; } /// - *Contract stamp:* mapping.md §Global-request rows (the `-R` /// listen form declines this build's flow — the event carries no /// channel id: global requests have none). The reason is the /// build's gate: `unfeatured` where the feature is off, /// `seam-unconfigured` where the tunnel feature is on but no /// fabric leg/listen seam is configured (the unset-declines /// posture — a bare `authenticated()` session configures neither). #[cfg(not(feature = "tunnel"))] #[tokio::test] async fn tcpip_forward_decline_emits_the_global_request_event() { let session = authenticated("forward-row").await; tcpip_forward(&session.client, "127.0.0.1", 8080) .await .expect_err("the listen request declines"); assert_decline( &await_event(&[ ("row", "tcpip_forward"), ("decline_class", "request"), ("reason", "unfeatured"), ]) .await, &[("row", "tcpip_forward"), ("reason", "unfeatured")], ); session.unmount().await; } /// - *Contract stamp:* mapping.md §Global-request rows + the /// decline-matrix's config-driven notes (the `w3-tunnel-listen` /// migrated stub row): a featured build whose fabric leg/listen /// seam is unconfigured declines `seam-unconfigured` — the feature /// alone configures nothing. #[cfg(feature = "tunnel")] #[tokio::test] async fn tcpip_forward_decline_emits_the_seam_unconfigured_event() { let session = authenticated("forward-row").await; tcpip_forward(&session.client, "127.0.0.1", 8080) .await .expect_err("the unconfigured listen declines"); assert_decline( &await_event(&[ ("row", "tcpip_forward"), ("decline_class", "request"), ("reason", "seam-unconfigured"), ]) .await, &[("row", "tcpip_forward"), ("reason", "seam-unconfigured")], ); session.unmount().await; } /// - *Contract stamp:* ADR-004 (the bridge rejects fail-closed — /// `None` resolution → reject disposition; the event pins the /// decline without exposing who). #[tokio::test] async fn unresolved_fingerprint_auth_decline_emits_the_auth_event() { let mut session = mounted().await; let result = authenticate_publickey(&mut session.client, "unregistered", fixture("charlie")).await; assert!(!result.success(), "the unregistered fixture rejects"); let event = await_event(&[ ("row", "auth_publickey"), ("decline_class", "auth"), ("reason", "fingerprint-unresolved"), ]) .await; assert_decline( &event, &[ ("row", "auth_publickey"), ("reason", "fingerprint-unresolved"), ], ); session.unmount().await; } /// - *Contract stamp:* ADR-004 decision 3 (auth_none: explicit /// family-contract decline). #[tokio::test] async fn auth_none_decline_emits_the_auth_event() { let mut session = mounted().await; let result = session .client .authenticate_none("observe-none") .await .expect("the auth exchange completes"); assert!( !result.success(), "auth_none rejects (the explicit disposition)" ); let event = await_event(&[ ("row", "auth_none"), ("decline_class", "auth"), ("reason", "method-unimplemented"), ]) .await; assert_decline( &event, &[("row", "auth_none"), ("reason", "method-unimplemented")], ); session.unmount().await; } /// - *Contract stamp:* auth.md §Dispositions (log exposure is /// deployment policy — the crate's events are safe-by-default: no /// username, secret, or fingerprint ever enters an event field or /// message; `w3-decline-tracing`'s safety posture, pinned by /// scanning every captured event across the whole binary's rows). #[tokio::test] async fn no_captured_event_ever_carries_a_username_or_fingerprint() { let session = authenticated("safety-row").await; open_x11_channel(&session.client, "127.0.0.1", 0) .await .expect_err("x11 declines"); session.unmount().await; await_event(&[("row", "x11")]).await; for event in captured() { let message = event.field("message").unwrap_or(""); for username in [ "x11-open-row", "forward-row", "fp-row", "none-row", "safety-row", "unregistered", "observe-none", ] { assert!( !message.contains(username), "no event message names the username, got {message:?}" ); } for (name, value) in &event.fields { let lowered = name.to_lowercase(); assert!( !lowered.contains("user"), "no field ever names a username-like input: {name}" ); assert!( !lowered.contains("fingerprint"), "the fingerprint is not an event field (auth.md: safe-by-default), got {name}" ); assert!( !value.contains("SHA256:"), "no event value carries a fingerprint, got {value:?}" ); for username in ["x11-open-row", "fp-row", "unregistered"] { assert!( !value.contains(username), "no event value names the username, got {value:?}" ); } } } } // --------------------------------------------------------------------------- // Feature-build-specific rows (the dispatch_core.rs convention). // --------------------------------------------------------------------------- #[cfg(not(any(feature = "session", feature = "git", feature = "tunnel")))] mod default_build { use super::*; use common::harness::open_session_channel; /// - *Contract stamp:* mapping.md §Decline matrix (the none-build /// row: everything declines — the event answers the booked /// finding: the decline's deciding context is observable). #[tokio::test] async fn unfeatured_session_open_emits_the_open_event() { let session = authenticated("none-build-row").await; open_session_channel(&session.client) .await .expect_err("a default build declines session channels"); let event = await_event(&[ ("row", "session"), ("decline_class", "open"), ("reason", "unfeatured"), ]) .await; assert_decline(&event, &[("row", "session"), ("reason", "unfeatured")]); session.unmount().await; } } #[cfg(all(feature = "git", not(feature = "session"), not(feature = "tunnel")))] mod git_build { use super::*; use common::harness::{ open_session_channel, request_agent_forward, request_env, request_exec, request_pty, request_subsystem, request_x11, }; /// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return — the /// serving path is spawned; the lifecycle events started / /// decline / ended ride the spawned task, in wiring order). The /// git row's decline in a seam-less build is config-driven /// (`w3-git-mapping`: the unset-declines contract — the reason /// names the seam, not the stub). #[tokio::test] async fn git_seam_stub_declines_with_the_full_serving_lifecycle() { let session = authenticated("git-stub-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open where a session-serving feature exists"); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("the exec request rides the wire"); expect_failure_then_close(&mut channel).await; let decline = await_event(&[ ("row", "exec_request"), ("decline_class", "serving"), ("reason", "seam-unconfigured"), ("exec_route", "git"), ]) .await; assert_decline(&decline, &[("row", "exec_request"), ("exec_route", "git")]); await_event(&[("message", "serving path started"), ("exec_route", "git")]).await; await_event(&[("message", "serving path ended"), ("exec_route", "git")]).await; session.unmount().await; } #[tokio::test] async fn unfeatured_pipe_exec_decline_emits_the_request_event_with_the_route() { let session = authenticated("git-pipe-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open where a session-serving feature exists"); request_exec(&channel, "uptime") .await .expect("the exec rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "exec_request"), ("decline_class", "request"), ("reason", "unfeatured"), ("exec_route", "pipe"), ]) .await; assert_decline(&event, &[("row", "exec_request"), ("exec_route", "pipe")]); session.unmount().await; } #[tokio::test] async fn subsystem_env_pty_and_never_feature_request_declines_emit_events() { let session = authenticated("git-req-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open where a session-serving feature exists"); request_subsystem(&channel, "sftp") .await .expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "subsystem_request"), ("decline_class", "request"), ("reason", "unimplemented"), ]) .await; assert_decline(&event, &[("row", "subsystem_request")]); request_pty(&channel).await.expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "pty_request"), ("decline_class", "request"), ("reason", "unfeatured"), ]) .await; assert_decline(&event, &[("row", "pty_request")]); request_env(&channel, "SOME_VAR", "value") .await .expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "env_request"), ("decline_class", "request"), ("reason", "declined-by-design"), ]) .await; assert_decline(&event, &[("row", "env_request")]); request_x11(&channel).await.expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "x11_request"), ("decline_class", "request"), ("reason", "never-feature"), ]) .await; assert_decline(&event, &[("row", "x11_request")]); request_agent_forward(&channel) .await .expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "agent_request"), ("decline_class", "request"), ("reason", "never-feature"), ]) .await; assert_decline(&event, &[("row", "agent_request")]); session.unmount().await; } /// - *Contract stamp:* overview.md §Bounded-resources policy (the /// cap is refused fail-closed at the open; the event carries the /// cap reason). #[tokio::test] async fn cap_exceeded_session_open_emits_the_cap_event() { let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture("alpha").fingerprint, identity("observe-cap", &["ssh"]), ); let limits = Limits { max_concurrent_channels: 0, ..Limits::default() }; let adapter = alkssh::SshAdapter::new(common::harness::serve_config( provider, &[fixture("bravo")], limits, )) .await .expect("adapter constructs"); let mut session = drive( adapter, Arc::new(MemHostKeyPolicy::with_default(true)), limits, ) .await .expect("the loopback mounts"); let result = authenticate_publickey(&mut session.client, "observe-cap", fixture("alpha")).await; assert!(result.success(), "the fixture identity authenticates"); open_session_channel(&session.client) .await .expect_err("the zero-cap build refuses session opens"); let event = await_event(&[ ("row", "session"), ("decline_class", "open"), ("reason", "cap-exceeded"), ]) .await; assert_decline(&event, &[("row", "session"), ("reason", "cap-exceeded")]); session.unmount().await; } } #[cfg(all(feature = "session", not(feature = "git"), not(feature = "tunnel")))] mod session_build { use super::*; use common::harness::{open_session_channel, request_exec, request_pty}; /// - *Contract stamp:* ADR-006 decision 4 (the pty serving decline /// is spawned; the lifecycle rides the task) + OQ-08 (the /// unset-seam decline: feature on, seam unset — the config- /// driven class the real paths inherit, /// `w3-tty-session`). #[tokio::test] async fn pty_declines_seam_unconfigured_with_the_full_serving_lifecycle() { let session = authenticated("session-pty-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open"); request_pty(&channel).await.expect("rides the wire"); expect_failure_then_close(&mut channel).await; let decline = await_event(&[ ("row", "pty_request"), ("decline_class", "serving"), ("reason", "seam-unconfigured"), ]) .await; assert_decline(&decline, &[("row", "pty_request")]); await_event(&[("message", "serving path started"), ("row", "pty_request")]).await; await_event(&[("message", "serving path ended"), ("row", "pty_request")]).await; session.unmount().await; } #[tokio::test] async fn unfeatured_git_exec_decline_emits_the_request_event_with_the_route() { let session = authenticated("session-git-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open"); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("rides the wire"); expect_failure(&mut channel).await; let event = await_event(&[ ("row", "exec_request"), ("decline_class", "request"), ("reason", "unfeatured"), ("exec_route", "git"), ]) .await; assert_decline(&event, &[("row", "exec_request"), ("exec_route", "git")]); session.unmount().await; } /// - *Contract stamp:* OQ-08 + the event vocabulary (`w3-tty- /// session`: the unset-field pipe decline — /// `wrapper-unconfigured`, the serving-path class from a /// spawned task). #[tokio::test] async fn pipe_declines_wrapper_unconfigured_with_the_full_serving_lifecycle() { let session = authenticated("session-pipe-row").await; let mut channel = open_session_channel(&session.client) .await .expect("session channels open"); request_exec(&channel, "uptime") .await .expect("rides the wire"); expect_failure_then_close(&mut channel).await; let decline = await_event(&[ ("row", "exec_request"), ("decline_class", "serving"), ("reason", "wrapper-unconfigured"), ("exec_route", "pipe"), ]) .await; assert_decline(&decline, &[("row", "exec_request"), ("exec_route", "pipe")]); await_event(&[("message", "serving path ended"), ("exec_route", "pipe")]).await; session.unmount().await; } } #[cfg(all(feature = "tunnel", not(feature = "session"), not(feature = "git")))] mod tunnel_build { use super::*; use common::harness::open_session_channel; /// - *Contract stamp:* the unset-declines posture (`w3-seam-config`, /// carried through `w3-tunnel-direct`: the route-1 bridge's /// pre-admit classes) — the direct row's absent-handle decline /// emits the `seam-unconfigured` open-class event (the fabric /// double's real decline events ride tests/tunnel_mapping.rs). #[tokio::test] async fn tunnel_direct_row_absent_handle_decline_emits_the_event() { let session = authenticated("tunnel-stub-row").await; common::harness::open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("the tunnel row declines on the absent handle"); let event = await_event(&[ ("row", "direct-tcpip"), ("decline_class", "open"), ("reason", "seam-unconfigured"), ]) .await; assert_decline( &event, &[("row", "direct-tcpip"), ("reason", "seam-unconfigured")], ); session.unmount().await; } #[tokio::test] async fn unfeatured_session_open_in_a_tunnel_build_emits_the_event() { let session = authenticated("tunnel-none-row").await; open_session_channel(&session.client) .await .expect_err("a tunnel-only build declines session channels"); let event = await_event(&[ ("row", "session"), ("decline_class", "open"), ("reason", "unfeatured"), ]) .await; assert_decline(&event, &[("row", "session"), ("reason", "unfeatured")]); session.unmount().await; } } // --------------------------------------------------------------------------- // Local drivers (dispatch_core.rs's shapes, copied per-binary). // --------------------------------------------------------------------------- async fn authenticated(username: &str) -> common::harness::LoopbackSession { capture(); let provider = Arc::new(MemIdentityProvider::new()); provider.register(fixture("alpha").fingerprint, identity(username, &["ssh"])); let adapter = adapter(provider, &[fixture("bravo")]).await; let mut session = drive( adapter, Arc::new(MemHostKeyPolicy::with_default(true)), Limits::default(), ) .await .expect("the loopback mounts"); let result = authenticate_publickey(&mut session.client, username, fixture("alpha")).await; assert!( result.success(), "the fixture identity authenticates before every observe row" ); session } /// A mounted (kex-complete, **unauthenticated**) loopback session — /// the auth-decline rows' driver: an auth disposition is observed /// before any acceptance exists, so these rows must not pre-derive /// themselves through [`authenticated`] (a second auth exchange on an /// authenticated session is not a supported russh surface — mounted /// fresh is the honest shape, matching `auth_bridge.rs`'s rows). async fn mounted() -> common::harness::LoopbackSession { capture(); let provider = Arc::new(MemIdentityProvider::new()); let adapter = adapter(provider, &[fixture("bravo")]).await; drive( adapter, Arc::new(MemHostKeyPolicy::with_default(true)), Limits::default(), ) .await .expect("the loopback mounts") } async fn expect_failure(channel: &mut Channel) { match next_message(channel).await { Some(ChannelMsg::Failure) => {} other => panic!("expected a channel failure, got {other:?}"), } } async fn expect_failure_then_close(channel: &mut Channel) { expect_failure(channel).await; assert!( await_close(channel).await, "the seam stub closes the channel" ); // Yield so the spawned task's post-close events (ended) are likely // to have landed before the unmount — await_event's polling covers // the rest. tokio::time::sleep(Duration::from_millis(50)).await; }