docs: SSH/SOCKS5 survey + fold findings into OQ ledger

Research specialist survey (docs/research/ssh-socks5-survey.md):
SSH forwarding model (direct-tcpip/forwarded-tcpip payloads, open-
failure reason codes, tcpip-forward registration lifecycle), SOCKS5
(ATYP, CONNECT, UDP ASSOCIATE), error-vocabulary comparison table,
endpoint-at-open vs per-datagram analysis, and an explicit anti-
prior-art list (SSH window updates, packet-size negotiation, dual
channel numbering, SOCKS5 FRAG/auth/BND semantics, udpgw
KEEPALIVE/pooling — all mechanisms channels already owns or the
substrate change obsoletes).

Key corrections to phase-0 findings:
- OQ-TN-02 category error fixed: SSH has NO UDP forwarding at all;
  the earlier '-D UDP associate over SSH' line was wrong (SSH -D
  carries only the SOCKS5 TCP control connection). Real UDP-over-
  stream prior art: tun2proxy udpgw + SOCKS5's own UDP relay.
- OpenSSH -D is protocol-level indistinguishable from -L (one
  direct-tcpip channel per SOCKS CONNECT) — direct confirmation of
  the -D = 'tunnel a socks5 connection' resolution.

OQ ledger updates:
- OQ-TN-01: minimal params shape supported ({resource, substrate});
  direct-streamlocal is the extensibility template; ATYP not needed
  in base params
- OQ-TN-02: RESOLVED as split by path — endpoint-at-open for base UDP
  resources; per-datagram addressing only inside the -D payload;
  channel ID replaces CONN_ID (no second demux); KEEPALIVE drops
- OQ-TN-07: option A (single alk/tunnel ALPN) strengthened
- OQ-TN-09: reason-code vocabulary from SSH's four codes + detail
  string; udpgw ERR bit flagged as the counterexample to avoid
- OQ-TN-10: reverse-flow POC gains the tcpip-forward template
- Checklist: SSH/SOCKS5 survey item checked off
This commit is contained in:
2026-09-06 09:20:08 +00:00
parent 648feb194d
commit 1a3eb8418d
2 changed files with 688 additions and 37 deletions
+86 -37
View File
@@ -307,6 +307,16 @@ and discovery residue). Direction of travel:
field layout (Phase 1 spec, ADR before first consumer — wire-stable
once published).
**Survey input 2026-09-06** (`ssh-socks5-survey.md`): SSH's
`direct-tcpip` payload reduces to "target + informational originator" —
the originator pair has no analogue here (ACL rides the channels
open-op machinery), supporting the two-field reframe. OpenSSH's
`direct-streamlocal` extension is the extensibility template: new
substrate = same open-op shape, degenerate address slots, new type
string → new `substrate` value, not a format change. SOCKS5 ATYP is
not needed in base params (dynamic-path addressing only). Minimal
shape: `{ "resource": <id>, "substrate": "tcp" | "udp" | <extensible> }`.
### OQ-TN-02: Datagram substrates (UDP) — boundary preservation
Does a UDP tunnel preserve datagram boundaries end-to-end, or does the
@@ -316,11 +326,14 @@ re-chunked arbitrarily)?
- Channels is a chunk stream with bounded buffers; the zero-length chunk
is the EOF sentinel — datagram boundaries are *not* preserved by the
substrate (alknet ADR-071/093; the POC only exercised TCP).
- SSH's `-D` UDP associate tunnels UDP as a stream with per-datagram
framing re-added by the tunnel protocol (e.g. SOCKS5 UDP over TCP).
Note (2026-09-05): russh does not support UDP channels at all — no
`direct-udp`-style prior art exists there; the tun2proxy gateway
(§Prior art) is the strongest framing precedent.
- **Corrected 2026-09-06** (`ssh-socks5-survey.md`): an earlier line
here claimed "SSH's `-D` UDP associate tunnels UDP as a stream" — a
category error. SSH has *no UDP forwarding at all* (RFC 4254 defines
only TCP/X11/session channels; russh is grep-confirmed UDP-free;
only SSH3 — a different HTTP/3 protocol — has `direct-udp`). SSH
`-D` carries only the SOCKS5 *TCP* control connection. The real
UDP-over-stream prior art is tun2proxy udpgw (§Prior art) and
SOCKS5's own UDP relay.
- iroh and quinn-proxy-poc have native datagram transports; tun2proxy has
a full UDP-over-TCP model worth reading.
- Boundary preservation is a wire-format decision (per-datagram length
@@ -331,20 +344,30 @@ re-chunked arbitrarily)?
"association" carries many remote endpoints — does one tunnel channel
carry one endpoint or many, and how are per-endpoint replies routed?
**Status:** open — survey mostly resolved by tun2proxy prior art
(§Prior art): per-datagram length framing over the stream is
production-proven (`LEN | FLAGS | CONN_ID | [addr] | DATA`), one
stream carries many UDP flows via a protocol-level `CONN_ID`, and
flow lifecycle (idle timeout + keepalive) is packet-level. Remaining:
whether alktunnels fixes the UDP endpoint at open (per-channel, TCP-
like) or carries per-datagram addresses (udpgw-like), and whether a
u16 conn-id vocabulary is right for channels (vs the channel ID
itself doing the demux and one channel per UDP flow). Note (2026-09-05,
hub model §Prior art): if UDP resources are produced like any other
resource, endpoint-at-open aligns naturally with resource naming
(OQ-TN-01); per-datagram addressing matches the `-D`/dynamic-target
composition path instead. A targeted POC (OQ-TN-10 #1) is likely still
+EV for the chosen shape.
**Status: resolved as a split by path, 2026-09-06** (survey
`ssh-socks5-survey.md` + tun2proxy prior art):
- **Base open-op UDP resources: endpoint-at-open.** The resource
identifies the endpoint; one channel = one UDP flow (or one pinned
association). Aligns with resource naming (OQ-TN-01); per-datagram
addressing would reintroduce the "general addressing in params" the
reframe removed. Boundary preservation inside the channel stays
per-datagram length framing (tun2proxy-proven) — endpoint-at-open is
about addressing, not about dropping the LEN prefix.
- **Dynamic/`-D` UDP (SOCKS5-style): per-datagram addressing inside
the tunnel payload** (SOCKS5 UDP header or udpgw format), composed
at the assembly layer, never in base params.
- **The channel ID replaces udpgw's CONN_ID** — a conn-id inside the
channel would be a second demux layer (AGENTS.md convention 10).
One channel per UDP flow; no protocol-level flow key.
- **KEEPALIVE drops** — udpgw's heartbeats exist for NAT-traversed
long-lived TCP; channels transport liveness is the alkcall layer's
concern. Flow-level idle expiry remains producer-side bookkeeping.
- A UDP gateway resource (multi-endpoint, udpgw-shaped) remains
possible *inside* a channel as self-describing framing — invisible
to base params, keeping OQ-TN-07 option A viable. A targeted POC
(OQ-TN-10 #1) remains +EV for the channels-layer fit
(chunk-size vs datagram-size, MTU vs bounded buffers, idle expiry).
### OQ-TN-03: Direction semantics (`-L` / `-R` / dynamic)
@@ -486,14 +509,17 @@ consumer — ALPN strings are wire-stable once published.
consumer-side API bifurcation (two session types vs one with a
substrate enum).
**Status:** open — needs the OQ-TN-02 outcome first (if datagrams need
different framing, option B gets stronger). Note from the tun2proxy
prior art (§Prior art): udpgw runs its packet framing over a plain TCP
stream — one framing covers both the stream and datagram cases there.
If alktunnels follows the same shape (datagram framing self-describing
inside the channel), option A (single `alk/tunnel` ALPN) stays viable
even with UDP support; option B remains cleaner if the datagram
channel needs structurally different framing from the first chunk on.
**Status: option A strengthened, 2026-09-06** (survey
`ssh-socks5-survey.md`): SSH uses one channel mechanism for all
forwarding types (the type string is per-open metadata, not a separate
transport); SOCKS5 runs CONNECT and UDP ASSOCIATE over one control
connection with a CMD discriminator; udpgw proves datagram framing
self-describes over a stream. With OQ-TN-02 resolved as endpoint-at-
open for base UDP resources, the substrate discriminator in `params`
tells the handler which framing to expect — exactly option A's shape.
Option B (`alk/tunnel-dgram`) remains defensible only if Phase 1 wants
structurally different framing from byte zero with no params-dependent
dispatch; prior art gives no reason to prefer that.
### OQ-TN-08: Access control and ownership scope
@@ -578,15 +604,29 @@ POC issue #6). What's missing is the error/level above bytes:
(standard two-pump behavior) — is that always desired, or does the
consumer need a "close both" control?
**Status: direction set 2026-09-05** — the original half-answer is
accepted: a self-contained control frame (alktty ADR-006 shape)
**Status: direction set 2026-09-05; vocabulary informed 2026-09-06**
(survey `ssh-socks5-survey.md` §Comparison). The original half-answer
is accepted: a self-contained control frame (alktty ADR-006 shape)
carrying an establishment result/error, sent before any data chunk;
dial errors are tunnel-closing (the whole channel dies), whereas
byte-level EOFs stay per-direction. Beyond that, the frame vocabulary
should follow the alktty stream-splitting pattern (§Prior art: the
alktty stream-splitting pattern) rather than invent a parallel
mechanism. Concrete frame set lands as a Phase 1 ADR (wire-stable
before the first consumer).
byte-level EOFs stay per-direction. The alktty-style JSON frame is the
strictly richest of the four prior-art vocabularies (SSH code+desc,
SOCKS5 codes-only, udpgw 1 opaque bit — the counterexample to avoid).
Survey inputs for the Phase 1 frame ADR:
- Content: establishment ack + failure frame with a small reason-code
set mirroring SSH's four (policy-denied / dial-failed /
unknown-resource-or-substrate / resource-shortage — mapping 1:1 onto
what an open handler can produce: ACL denial, dial failure,
unregistered resource/substrate, channels limits) + a detail string
(SOCKS5's network/host/refused granularity lives in `detail`, not
more codes — codes are wire-stable, detail strings are not).
- Failure ⇒ channel closes before data flows: all four prior arts
agree; there is no "fail the open but keep the channel" prior art,
and none is needed (consumer just opens a new channel).
- The frame does NOT need: window adjustments (channels owns
backpressure), keepalives (channels owns liveness), fragmentation
(not the tunnel's job), per-datagram error replies.
### OQ-TN-10: POC scope for what remains unvalidated
@@ -603,6 +643,9 @@ Phase 0 may need (in rough priority order, per the SDD process's
endpoint-addressing shape. Derisks OQ-TN-02 (and OQ-TN-07's option B).
2. **Reverse-flow POC**`-R`-style: the accept side listens, the far
side carries. Derisks OQ-TN-03's advertisement/lifecycle shape.
Template available: SSH's `tcpip-forward` global-request
registration → per-accept `forwarded-tcpip` opens → cancel
(`ssh-socks5-survey.md` §RFC 4254 §7.1).
3. **Unix socket + stdio bridge POC** — cheap; validates "substrate
agnostic" beyond IP substrates.
4. **Two-pump helper extraction spike** — OQ-TN-06, only after 13.
@@ -648,12 +691,18 @@ Candidate reading for the research specialist (to be expanded):
- alktty: `NegotiateRequest` shape (self-contained negotiation
precedent), `TtyBackend` inversion point, `TTY_OPEN_SCOPE` access
gate.
- `ssh-socks5-survey.md` (this directory, 2026-09-06): SSH
channel-open/open-failure/forwarding model, SOCKS5 ATYP/CONNECT/
UDP ASSOCIATE, error-vocabulary comparison, endpoint-at-open vs
per-datagram analysis, anti-prior-art list (what NOT to carry over).
Feeds OQ-TN-01/02/07/09 statuses above.
## Convergence checklist (what Phase 0 must produce)
- [ ] Survey notes: SSH/SOCKS5 addressing + UDP framing
(OQ-TN-01, OQ-TN-02) — tun2proxy UDP gateway done (§Prior art);
SSH/SOCKS5 survey delegated to research specialist
- [x] Survey notes: SSH/SOCKS5 addressing + UDP framing
(OQ-TN-01, OQ-TN-02) — tun2proxy UDP gateway (§Prior art) +
`ssh-socks5-survey.md` (SSH/SOCKS5, error vocabularies,
endpoint-vs-per-datagram, anti-prior-art list)
- [x] Reframe landed (OQ-TN-01) — params = self-contained JSON open-op
object identifying a produced resource + substrate
discriminator (`tcp`/`udp`/extensible); producer owns the