Files
alkssh/docs/research/mapping-table.md
T
glm-5.3-flash c7ab19ed8a docs(architecture): retire the dynamic-forward feature — the -D untangle
Review untangle (three layers: local door / wire protocol / fabric):
- vanilla ssh -D binds its SOCKS5 proxy client-side (the local door —
  alksocks' local-door shape) and opens plain direct-tcpip per
  connection: the server needs nothing SOCKS-shaped (tunnel row +
  route-1 bridge cover the leg; POC-3 correction 2 already said this)
- the family-native SOCKS5-in-channel server row served no consumer
  of ours: family apps compose alksocks' local door locally over the
  alkssh client's direct-tcpip DialFn — alkssh doesn't depend on
  alksocks on this path
- amendments: ADR-003 §1a (feature removed), ADR-006 point 3 (SOCKS-
  detect retired; session dispatch is parse-only; request-less
  session channels decline), OQ-02 resolution updated with the
  simplification, mapping-table.md rows annotated (research doc keeps
  POC-3 stage B as the re-add feasibility record)
2026-10-26 15:32:08 +00:00

185 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Mapping table — SSH channel types × family protocol seams
Status: Draft (phase 0). 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).
**POC-3 validated the load-bearing rows against the published
crates** (`poc-3-mapping-bakeoff.md`): git exec, SOCKS-in-channel,
direct-tcpip (route 1), and session/pty all work end to end with
vanilla clients and zero seam fixups. Three row corrections from that
run are marked **[POC-3 corrected]** below.
## 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). **[POC-3 corrected]** the data-plane mapping is a *translation bridge*, not a byte pump: alktty's plane is chunk-framed both directions (stdin/stdout/stderr + CTRL_IN/CTRL_OUT chunks), so alkssh encodes SSH data→stdin chunks and decodes stdout→data, stderr→extended_data, exit chunk→exit-status/exit-signal. New OQ: the exit chunk carries no signal identity (negative code only) |
| `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). **[POC-3 corrected]** git over SSH is exec-only: a `subsystem` request carries the service *name* but no repo id, so even a git-service subsystem must decline — vanilla git always executes `git-upload-pack '/repo'`. POC-3: vanilla clone/push/second-push through the published seam + substrate composition, exit-status in both directions |
| `session` → SOCKS5-in-channel | (n/a — retired) | ~~`dynamic-forward`~~ **retired (ADR-003 §1a)** | was alksocks: `serve_socks5_over` | **Post-architecture retirement:** no consumer is ever the family-native client shape this row served. Vanilla `ssh -D` does SOCKS client-side and opens plain `direct-tcpip` (POC-3 correction 2 → the `tunnel` row); the family `-D` behavior is a *client-local* alksocks door over the alkssh client's `direct-tcpip` opens (assembly composition). POC-3 stage B remains the evidence that SOCKS-in-channel is feasible if a family client ever wants a remote SOCKS door (back via explicit ADR). **[ADR-003 amended]** |
| `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`; 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). (Post-architecture: the former SOCKS-detect dispatch
arm is retired with the `dynamic-forward` feature — ADR-003 §1a /
ADR-006 amendment; request-less session channels decline.) Grammar/
precedence per ADR-006.
## 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 (POC-3: proven both directions via the cloned `Session::handle()` from the spawned serving task) |
| `exit_signal` | Only reachable in signal-termination cases; mapped from the same exit-code path (OQ: exact encoding). **[POC-3]** the alktty exit chunk is `{"type":"exit","code":N}` — a negative code implies signal termination but carries no *name*, so the faithful `exit-signal` mapping needs alktty to widen the chunk or an assembly-policy default |
## 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`