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)
185 lines
13 KiB
Markdown
185 lines
13 KiB
Markdown
# 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` |