diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index 6f7d0d2..b52fdf1 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -361,11 +361,43 @@ the datagram stage must ride a channel. Sub-questions: sentinel-reply shape is fine and no virtualized relay address is ever invented. -Hunch: wrapper-aware client halves ride the same-channel -length-prefixed-datagram shape; vanilla clients get UDP ASSOCIATE only -via the optional local backend. But this is the crate's largest -unknown — a POC candidate (OQ-SK-07 #2), decided by an ADR before the -first consumer (the datagram framing is wire-stable once published). +**Phase model vs alktty-style demux (2026-09-12 discussion).** An +alternative shape surfaced: alktty's logical demux (input/output/error/ +control sub-streams inside one channel, type byte per chunk, up to the +u8 = 255 stream-type limit) applied to the SOCKS5 channel — a `data` +stream and a `udp-relay` stream type would carry the two phases +structurally. Analysis against RFC 1928's actual structure: **RFC 1928 +is one command per connection, and control and data never interleave in +either direction.** CONNECT's control conversation ends at the reply, +after which the channel is a pure byte stream (alktunnels' 0 B +pass-through conclusion); ASSOCIATE's control stream goes silent after +the reply — fast-socks5's own `wait_on_tcp` (`src/server.rs:1195`) +treats any post-reply control byte as protocol garbage +(`UnexpectedUdpControlGarbage`). So the per-chunk type byte demuxes a +problem SOCKS5 doesn't have; a **phase model** (raw RFC conversation +until the reply, then a mode switch synchronized by the reply itself — +pass-through for CONNECT, `[len: u16 BE]` datagrams for ASSOCIATE) +subsumes it with no type byte and no sentinel address: wrapper-aware +clients know "post-reply = datagram mode," the same way they'd know +"relay stream = datagram mode" under the demux. What the demux genuinely +buys that the phase model does not: an **additive in-band vocabulary** — +new stream types extend the wire additively (no format break), whereas +adding a post-reply phase later is a wire break. alktunnels accepted the +opposite ("no in-band control path, ever — the escape hatch is a new +ALPN, a wire change is not"); the demux reintroduces that vocabulary at +2 B/chunk (type byte + length prefix) on every datagram. The one-way-door +cost is real either way: demux bakes the vocabulary in before it has a +user; phase model keeps 0 B overhead on CONNECT and 2 B on ASSOCIATE +datagrams. Hunch: phase model (simpler, RFC-shaped); the extensibility +argument is the honest case for the demux if Phase 1 wants the +vocabulary. Decide via ADR with the first consumer in sight. + +Hunch (datagram stage, unchanged): wrapper-aware client halves ride the +same-channel length-prefixed-datagram shape; vanilla clients get UDP +ASSOCIATE only via the optional local backend. But this is the crate's +largest unknown — a POC candidate (OQ-SK-07 #2), decided by an ADR +before the first consumer (the datagram framing is wire-stable once +published). ### OQ-SK-04: fast-socks5 wasm posture (blocks the wasm-clean invariant) @@ -396,6 +428,30 @@ Option A upstream if confirmed. This OQ gates the wasm verification command in AGENTS.md (the expected-failure note is already written there). +**Fork vs reimplement vs wrap — collapses onto this OQ (2026-09-12 +discussion).** The wrap surface was re-checked and **no structural fork +is needed** for the channels path: CONNECT rides the interception points +as-is — `read_command` → own dial → `reply_success` → `transfer` (the +`examples/router.rs` shape), with the producer's `BiStream` as `T` on +both sides of `transfer`. UDP is ours either way: `run_udp_proxy_custom` +takes a custom `transfer` closure for the relay half, `reply_success` +accepts any `SocketAddr` (the sentinel reply), and +`new_udp_header`/`parse_udp_request` are public, pure, and reusable. +The one hardwired piece — `run_udp_proxy`'s peer relay socket +(`udp_bind_random_port`, socket2-based, called inside the default +handler before `run_udp_proxy_custom` even runs) — is in the *default* +handler, not the protocol: the wrapper's custom closure never needs it. +So the fork-vs-reimplement decision is not about the channels surface +at all; it is driven by the wasm question above: **wrap (with the +upstream `net` feature-gate ask) if the gating lands cleanly; +reimplement if the gating turns out structural.** The reimplementation +is genuinely small — RFC 1928 over `tokio::io` generics is the +alktunnels-codec scale of code — and fast-socks5 would remain the +differential-test oracle for RFC edge cases (reply-code mapping, +domain addressing, fragmentation) either way. A full fork (divergent +copy we publish) is the least attractive branch: it duplicates the +maintenance without buying anything the two options above don't. + ### OQ-SK-05: Target policy and egress scoping SOCKS5 is arbitrary-egress by nature — the open gate plus the target @@ -536,11 +592,13 @@ Candidate reading for the research specialist (to be expanded): dialer hunch); decide in Phase 1 against the spec - [ ] OQ-SK-02 (auth mapping) — posture drafted (identity seam primary, in-band auth for the local backend); decide in Phase 1 -- [ ] OQ-SK-03 (UDP ASSOCIATE) — the shape space written; resolve via - research + POC #2, ADR before the first consumer +- [ ] OQ-SK-03 (UDP ASSOCIATE) — the shape space written (including + the phase-model-vs-alktty-demux analysis); resolve via research + + POC #2, ADR before the first consumer - [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4); - likely an upstream feature-gating ask; file early per AGENTS.md - convention 17 + the fork-vs-reimplement-vs-wrap decision collapses onto this + OQ; likely an upstream feature-gating ask; file early per + AGENTS.md convention 17 - [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; scope-gate convention pinned in Phase 1 - [ ] OQ-SK-06 (noq client) — research pass (trait-shape comparison,