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)
This commit is contained in:
1 parent
e0805cd438
commit
ecdf836eeb
7 files changed
+830
-1
No files matched your search
@@ -0,0 +1,178 @@
|
||||
# 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`
|
||||
Reference in new issue
Block a user