Files
alkssh/docs/research/mapping-table.md
T
glm-5.3-flash ecdf836eeb docs(research): phase 0 research set — vision, russh, auth bridge, mapping table, POC plan
The phase 0 document set (documented only; no POCs run yet):

- docs/research/vision.md — guiding principles (russh owns the wire,
  door-not-backend, fail-closed everything, one seam per mapping,
  family identity not SSH identity); non-goals (no sshd, no sftp until
  alkfs, no password auth); the five scope questions phase 0/1 must
  answer
- docs/research/russh.md — 0.64.1-era inventory: server/client
  Handler method tables, Config knobs, keys module (ssh-key re-export,
  fingerprint API), the RC-pinned dependency tree (ssh-key, rsa,
  pkcs1), MSRV/edition/license posture; alknet-main referenced as a
  rough example only, explicitly not decision input
- docs/research/auth-bridge.md — the alkcall mapping: no
  resolve_from_signature exists; the actual surface is
  IdentityProvider::resolve_from_fingerprint fed by russh-verified key
  fingerprints in auth_publickey; auth-method disposition matrix
  (only publickey implemented, all defaults overridden); client side
  mirrors alkhttp ADR-014 (key sources, not static keys; vault
  client-side only); the vault-to-russh-key bridge named as the
  missing design piece
- docs/research/mapping-table.md — the spine: channel-type rows with
  feature candidates and the verified sibling serve/consume entries
  (alktty drive_session, alkgit serve_duplex_resolved, alksocks
  serve_socks5_over, alktunnels consumer wrappers + the no-serve-over-
  stream absence note); request-level rows; parse-then-route dispatch
  inside the session channel; the alk/ssh ALPN topology question
  (ADR-004 reserved / ADR-034 stream_types [0,1] / ADR-042 hub never
  runs it); alktunnels route choice flagged for ADR
- docs/research/pocs.md — POC-1 (auth bridge + fail-closed declines
  against vanilla OpenSSH), POC-2 (vault-to-russh key bridge:
  PrivateKey extraction vs vault-backed Signer), POC-3 (mapping
  bake-off stages A-D: git exec, socks -D, tunnel -L, pty); all
  standalone mode; findings docs are the deliverables

Verification: cargo doc --no-deps, cargo fmt --check, cargo test
(docs-only change; lib untouched)
2026-10-25 10:13:17 +00:00

12 KiB

Mapping table — SSH channel types → family protocol seams

Status: Draft (phase 0 research). The crate's spine. Every row maps a russh server-side entry point onto exactly one family protocol seam plus exactly one cargo feature. Names below are feature-name candidates, not decisions — the feature graph is phase 1 ADR territory. All downstream surfaces verified against published sibling source (alktty 0.8.1, alktunnels, alksocks, alkgit 0.1.0, alkcall 0.10).

The shape that makes this work

Every family protocol crate already exposes a generic duplex-stream or Connection-serving entry — no crate needs to know SSH exists:

Crate Serve-over-a-stream entry Signature shape
alktty drive_session[_pre_negotiated] (impl AsyncWrite, impl AsyncRead, backends, identity)
alkgit serve_duplex_resolved (repo_id, identity, service, T: AsyncRead+AsyncWrite, limits, backend traits)
alksocks serve_socks5_over (T: AsyncRead+AsyncWrite+Unpin, dial, egress, accept, can_bind)
alktunnels — no serve-over-stream entry; see absence note consumer wrappers / open-op registrations instead

russh channels convert to duplex via Channel::{into_stream, split, make_reader/make_writer} (ChannelStream/com halves). So each mapping is: accept-or-decline the channel open (ChannelOpenHandle; dropping it auto-rejects — russh's default is already fail-closed), pump/serve the channel through the seam, map the request-level extras.

Channel-type rows

SSH channel type Vanilla use Feature (candidate) Family seam Notes
session → pty_request+shell_request ssh host interactive shell session alktty producer: NegotiateRequest constructed locally → drive_session (or pre-allocated variant) pty_request args → TerminalParamsWire (term/cols/rows/pixels; SSH modes → modes JSON reserved); shell case needs a cmd convention (OQ); PTY mode merges stderr into stdout (kernel PTY)
session → exec_request (non-git command) ssh host "cmd" session alktty pipe mode: terminal: None, separate stderr stderr flows back as SSH extended_data code 1 — only valid without PTY
session → exec_request/subsystem_request git git push/clone ssh:// git alkgit: parse command → Service + repo (same grammar as preamble.rs, without \0host=) → serve_duplex_resolved(repo_id, identity, service, duplex, ...) Process-mode dialect is correct (SSH supplies service+repo out of band; server speaks first = advertisement). subsystem names also map here (git-upload-pack etc. arrive as either); unknown subsystems decline
session → SOCS5-in-channel ssh -D dynamic-forward alksocks: serve_socks5_over(channel_duplex, dial, egress, accept, can_bind) alkssh embeds Socks5Producer-shaped config from the assembly; DialFn/EgressFactory injected at assembly
direct-tcpip ssh -L tunnel alktunnels (two routes, below) Target = SubstrateAddr::HostPort/Path; Substrate = Tcp/Unix
forwarded-tcpip ssh -R (data channel) tunnel alktunnels: register_tunnel_forwarded_openable's ForwardedHandler(params, BiStream) shape — in the adapter, hand the russh channel duplex to the handler listen flow via global-request rows below
direct-streamlocal unix-socket forward tunnel same SubstrateAddr::Path treatment unix-only; decline on other platforms (compile-time feature + runtime decline)
x11 X forwarding never (no feature) — decline, always; not even feature-gated initially (OQ: revisit years later if a use case arrives)
sftp (subsystem_request("sftp")) sftp clients sftp (future) alkfs (deferred, family ADR-002) The declinable placeholder — the feature name exists in the feature graph, decline until alkfs lands
auth-agent (agent_request) ssh-agent forwarding never — decline; family holds keys in alkvault — forwarding agents to remote sessions is the anti-pattern this posture rejects

Notes: the rows deliberately split one channel type (session) across three features, because vanilla SSH multiplexes everything through a session channel: a git command (exec_request), a SOCKS proxy client, a shell, a pipe-mode command. The failure matrix this produces:

  • git-only build: declines pty/shell, declines plain exec, declines SOCKS-in-channel; serves only the git-parseable exec/subsystem path.
  • session-only build: serves pty/shell/pipe-exec; the git-parseable exec path declines (parsing to a git service is gated on git).
  • No features: every session channel declines at open; every unfeatured channel type declines at open.

Dispatch is parse-then-route by command shape: parsing to a git service and repo requires git; SOCKS-in-channel requires dynamic-forward; otherwise the alktty pipe/pty path requires session. Parsing happens before dispatch and before any resource allocation (a leaked channel alloc is a capability leak — decline first). Exact grammar/precedence is a phase 1 spec question (OQ candidate).

Global-request rows (tcpip_forward / streamlocal_forward) — the -R listen flow

Handler::tcpip_forward(address, &mut port) -> bool + per-open channel_open_forwarded_tcpip. Consumer-side alktunnels: TunnelListener, open_reverse_channel; the forwarded data channel maps to the ForwardedHandler route above. Dispositions follow the tunnel feature (decline both listen and data when tunnel absent). port = 0 (server picks) interacts with the &mut u32 out-param — spec detail deferred to phase 1.

Request-level mapping on a session channel

SSH request Family mapping
pty_request NegotiateRequest.tty = TerminalParamsWire{term, cols, rows, pixel_w, pixel_h, modes}
shell_request NegotiateRequest.cmd per the backend's shell convention (OQ — probably cmd + cwd/env from assembly policy, not from the wire)
exec_request Parse command → git/service dispatch or alktty pipe cmd; unknown commands fail with session.channel_failure (parse-then-decline, no resource alloc)
subsystem_request sftp → decline until sftp feature serves alkfs; git subsystem names → same parse as exec; anything else → decline
env_request Decline by default. Wire-supplied env is not family identity config; TtyParams.env comes from assembly. (OQ: allowlist shape, e.g. LANG, later.)
window_change_request TtyControlHandle::resize(cols, rows, pixel_w, pixel_h)
signal TtyControlHandle::signal(name) (alktty's signal_from_name accepts the HUP/INT/TERM/... names verbatim)
data / extended_data(1) channel duplex in/out; stderr only meaningful in pipe mode (PTY merges)
exit_status from TtyHandle.exit_code (TtySession.wait() semantics: i32; negative = signal-terminated) — alkssh sends the SSH exit-status request before channel close
exit_signal Only reachable in signal-termination cases; mapped from the same exit-code path (OQ: exact encoding)

Data-plane / limits contract

  • russh windows: Config::window_size, maximum_packet_size, channel_buffer_size cap per-channel memory; Limits caps rekey volume; inactivity_timeout the session wall clock — the family bounded-resources convention maps onto these knobs plus the crate's own max-concurrent-channels (per session) and the per-length parse limits on exec commands.
  • alkcall bridge leg (when the family half is remote): channels-layer reassembly cap 1 MiB/channel, chunk max 16 MiB, per-identity channel-count policy — an SSH channel bridged over alk/channels rides one reassembled byte stream per SSH channel; SSH's own multiplexing is why ADR-034 grants alk/ssh only stream_types [0, 1] ("SSH multiplexes internally, including its own control").
  • Honest advertisement: Config::methods mirrors implemented auth; an unfeatured channel type is declined at open, never half-opened. Vanilla clients see a clean failure — which is the contract.

alktunnels absence note

alktunnels has no standalone serve_*_over(stream) producer entry (unlike alktty/alkgit/alksocks). The direct-tcpip mapping therefore has two legitimate routes, and choosing between them is a phase 1 decision:

  1. Adapter-side bridge: alkssh is itself an alk/channels client (it holds a ChannelClient into the family fabric) and calls TunnelSession::open_direct(client, target, substrate) / TunnelSession::adopt(...) directly, pumping take_halves()/ stream_halves() against the russh channel. (alksocks' composed::direct_egress is the family precedent for exactly this bridging.)
  2. Registry-side bridge: within an alkcall serving endpoint, register_tunnel_direct_openable with a DialFn — but that puts alkssh-shaped work in the serving endpoint's registry, which inverts the door posture. Likely rejected on composability grounds; recorded here for the ADR to decide explicitly.

Topology: where the alk/ssh ALPN itself lives

Three alkcall ADR constraints collide productively:

  • ADR-004 reserves alk/ssh → SshAdapter (custom protocol).
  • ADR-034's per-ALPN table: alk/ssh gets stream_types [0, 1] — SSH multiplexes internally, the channels layer is byte-blind to it.
  • ADR-042/051: "The hub never runs a handler for alk/tty, alk/ssh, or alk/tunnel" — alk/ssh channels relay payload-blind between spokes; ACL fires at channel/open.

ADR-009 additionally sketches a browser route: a WebTransport session to /alk/ssh where each bidi stream is handed to SshAdapter::handle as a Connection, with a WASM SSH parser speaking SSH over it.

Reading: alk/ssh is plausibly SSH-over-channels — the raw vanilla- SSH byte stream carried between alkssh instances (edge termination at one end, mapped serving at the other), reused by the browser route. Whether the edge-terminated topology (russh at the edge, family seams in-process or via alk/channels hops) ever needs to speak alk/ssh itself, and what SshAdapter::handle(connection) does for the browser route (it would be alkssh's russh wrapper serving an inbound alkcall connection), is the phase 0 topology question. POC-3's design will attempt the simplest composition; the decision records itself in the phase 1 ADR-001 (ALPN pin, ledger rule 1).

Open questions seeded for phase 1

  • Session-channel dispatch precedence + the exec-command grammar (git parse vs pipe cmd) — parse-then-fail-closed contract.
  • Shell command convention for shell_request (assembly policy vs wire-supplied; wd/env provenance).
  • env_request policy (decline-only vs allowlist).
  • OQ: OpenSSH certificate auth (auth_openssh_certificate) — decline until a use case.
  • OQ: username disposition (ignored entirely vs a routing hint).
  • Feature-graph names + the session-vs-git-vs-dynamic-forward split inside one channel type.
  • The alk/ssh topology question above.
  • Windows: direct-streamlocal is unix-only; pty on Windows targets depends on alktty backend capability (decline rather than degrade).

References

  • russh.md §server surface (handler inventory, Config)
  • auth-bridge.md (identity arrives before any channel opens)
  • alkcall ADRs 002, 004, 009, 034 (+035 chunk header), 037, 042, 051
  • Sibling sources: alktty src/adapter.rs/src/negotiation.rs/ src/backend.rs/src/channels.rs/src/session.rs, alktunnels src/consumer.rs/src/producer.rs/src/params.rs, alksocks src/producer.rs/src/composed/mod.rs, alkgit src/duplex.rs/src/preamble.rs/src/substrate.rs/src/session.rs