From 001aac1fd568bded9a048216c2ef14221c5f1346 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Tue, 22 Sep 2026 12:33:36 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20the=20targeted=20fork=20=E2=80=94=20ADR?= =?UTF-8?q?-013,=20the=20RFC=201928=20core=20is=20owned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-013 supersedes ADR-011's maintenance posture (not its mechanics): the extracted T-generic subset is fully owned code, the vendor framing is retired. Module renamed src/vendor/ -> src/rfc1928/ (naming decided in the ADR: 'vendor' names an ownership relation now gone; 'socks5' would stutter and collide with the future producer/consumer layers). Fork point = the tree as of the extraction-review commit (5b18d4d); provenance stays permanent (module docs + the extended LICENSE notice). - ADR-013: fork posture, license mechanics (notice extended, not replaced; post-fork modifications under the crate license), hand-port replaces re-vendor, lint-allow posture dissolved - ADR-011 marked Superseded (extraction mechanics remain authoritative); ADR-002 references re-pointed - vendor.md -> rfc1928.md rewritten as the owned-core spec (the cut table re-read as the hand-port map) - spec sweep: README index (ADR-013 row, doc table, principle 8), overview (module map, deps, ADR table), producer/consumer/wire/ bast reference vocabulary, open-questions OQ-SK-04 resolution, AGENTS.md conventions 4/17 + verification note + context bullet, Cargo.toml description Verification: cargo doc --no-deps clean; no stale vendor.md/src-vendor references outside the ADR-011 historical text. --- AGENTS.md | 79 +++--- Cargo.toml | 2 +- docs/architecture/README.md | 42 ++-- docs/architecture/bast.md | 12 +- docs/architecture/consumer.md | 15 +- .../decisions/002-protocol-base.md | 23 +- .../decisions/009-feature-gates.md | 3 +- .../decisions/010-channel-capacity.md | 2 +- .../decisions/011-inline-vendor.md | 14 +- .../decisions/013-targeted-fork.md | 148 ++++++++++++ docs/architecture/open-questions.md | 26 +- docs/architecture/overview.md | 50 ++-- docs/architecture/producer.md | 9 +- docs/architecture/rfc1928.md | 225 ++++++++++++++++++ docs/architecture/vendor.md | 188 --------------- docs/architecture/wire.md | 5 +- 16 files changed, 538 insertions(+), 305 deletions(-) create mode 100644 docs/architecture/decisions/013-targeted-fork.md create mode 100644 docs/architecture/rfc1928.md delete mode 100644 docs/architecture/vendor.md diff --git a/AGENTS.md b/AGENTS.md index f90b8f3..fb96abf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,14 +90,15 @@ implementation agents. (`rt`, `sync`, `io-util`, `macros`, `time`) — **do NOT use `features = ["full"]`** — and keep socket/platform I/O feature-gated. Note: `fast-socks5` uses `tokio::net` and `socket2` unconditionally as - published; the posture is the inline vendor as an **extraction** - (OQ-SK-04, resolved 2026-09-20 — convention 17; extraction pinned - 2026-09-21, ADR-011 §Amendment): extract the `T`-generic subset - only — the kernel-net handlers are not copied, so the vendor is - wasm-clean by construction with no `net`-shaped feature; socket - I/O rides `local` alone. Run the wasm check whenever a non-backend + published; the posture is the **targeted fork** (OQ-SK-04, + resolved 2026-09-20 — convention 17; extraction pinned 2026-09-21, + ADR-011 §Amendment; fork 2026-09-22, ADR-013): the `T`-generic + subset was extracted in-tree — the kernel-net handlers are not + copied — so the RFC 1928 core (`src/rfc1928/`) is wasm-clean by + construction with no `net`-shaped feature; socket I/O rides + `local` alone. Run the wasm check whenever a non-backend module changes; it is a hard gate (no dependency-graph failure is - expected — the extraction contains no `tokio::net`). + expected — the core contains no `tokio::net`). 5. **Wire format is stable** — the SOCKS5 protocol itself is RFC 1928 (fixed); this crate's wire surface is the ALPN + the channel open-op @@ -234,24 +235,33 @@ implementation agents. One ALPN per protocol. The ALPN string is wire-stable once published — decide via ADR before the first consumer. -17. **Upstream: alkcall is ours to shape; fast-socks5 is inlined.** +17. **Upstream: alkcall is ours to shape; fast-socks5 is forked + in-tree.** alkcall is ours (the actively co-developed substrate): file asks early and land them there rather than working around them locally — the alktunnels precedent (the E-01/E-02 sweep, filed from its Phase 0 and landed in alkcall 0.5.0 within a day; the graduation sweep landed in alkcall 0.8.0 / alktunnels 0.2.0). `fast-socks5` - is NOT ours — it is third-party upstream (`/workspace/fast-socks5` - is a read-only reference checkout, v1.0.0, MIT). The posture is - the **inline vendor as an extraction** (OQ-SK-04, resolved - 2026-09-20; extraction pinned 2026-09-21, ADR-011 §Amendment): - extract the `T`-generic subset only — the kernel-net handlers and - the `socks4` module are not copied — and carry the wrapper's - needs in-tree (BIND, the `new_reply` publication; the `net` gate - and the `run_udp_proxy_custom` bind seam dissolve with the - kernel-net region). Offering the net-gate upstream as a courtesy - PR stays optional; never wait on it. Keep - `/workspace/fast-socks5` for differential testing of RFC edge - cases; note the vendor point (v1.0.0) in the vendored code. + is NOT ours and never will be — it is third-party upstream + (`/workspace/fast-socks5` is a read-only reference checkout, + v1.0.0, MIT), but the `T`-generic subset we depend on is now + **ours, as a targeted fork** (OQ-SK-04 resolved 2026-09-20 as the + inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 + §Amendment; fork posture pinned 2026-09-22, ADR-013): the subset + lives in `src/rfc1928/` as fully owned code — project conventions + apply to it unconditionally (no vendor carve-outs, no + lint-allow posture, no verbatim preservation). The extraction + cut (ADR-011's table) is the hand-port map: if a future need + surfaces (e.g. the kernel-net region for a standalone + vanilla-server example), port the piece from the reference + checkout, adapt it to the conventions, and let the differential + suite guard it. A future upstream fix that matters is a + hand-port too — nothing is absorbed silently, and nothing waits + on upstream. Keep `/workspace/fast-socks5` for differential + testing of RFC edge cases. Provenance is permanent: the module + docs and the bundled license notice (`src/rfc1928/LICENSE`) + always name the origin (v1.0.0, the extraction range, the fork + point) — extend, never replace, the notice. ## Verification Commands @@ -270,8 +280,8 @@ cargo publish --dry-run --allow-dirty # before a release The wasm check is the structural guard for the "default crate is wasm-clean" invariant (same as alktty/alktunnels) — run it whenever a -non-backend module changes. It is a hard gate: the vendor is an -extraction with no kernel-net code (ADR-011 §Amendment), so no +non-backend module changes. It is a hard gate: the RFC 1928 core is +an extraction with no kernel-net code (ADR-011 §Amendment), so no dependency-graph failure is expected. ## Architecture Context @@ -309,17 +319,20 @@ dependency-graph failure is expected. (the composed BIND substrate; OQ-SK-08 variant 2), and `TunnelListener`. - **fast-socks5** — `/workspace/fast-socks5` (v1.0.0, MIT, - third-party upstream — NOT ours). The SOCKS5 implementation to - wrap: explicit typestate server API (`Socks5ServerProtocol`), generic over `T: AsyncRead + AsyncWrite + Unpin`; - `run_tcp_proxy` / `run_udp_proxy_custom` / `transfer` are the - interception points; client side has `Socks5Stream` (generic over - the backing socket) and `Socks5Datagram` (UDP associate). The - posture is the inline vendor as an extraction (OQ-SK-04, resolved - 2026-09-20; extraction pinned 2026-09-21, ADR-011 §Amendment — - convention 17): the `T`-generic subset is extracted into this - crate with no kernel-net code and no `net` feature; BIND and the - `new_reply` publication are the two carried changes. + third-party upstream — NOT ours, never will be). The SOCKS5 + implementation the RFC 1928 core forked: explicit typestate + server API (`Socks5ServerProtocol`), generic over + `T: AsyncRead + AsyncWrite + Unpin`; `run_tcp_proxy` / + `run_udp_proxy_custom` / `transfer` are the interception points; + client side has `Socks5Stream` (generic over the backing socket) + and `Socks5Datagram` (UDP associate). The posture is the + **targeted fork** (OQ-SK-04 resolved 2026-09-20 as the + inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 + §Amendment; fork 2026-09-22, ADR-013 — convention 17): the + `T`-generic subset lives in `src/rfc1928/` as owned code, no + kernel-net code and no `net` feature; BIND and the `new_reply` + publication are the two carried changes; the reference checkout + stays for differential testing of RFC edge cases. - **alknet's SOCKS5 client** — `/workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs` (ADR-090): the client-side example need — `Socks5UdpSocket` diff --git a/Cargo.toml b/Cargo.toml index a2a8e1e..973d2e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" -description = "SOCKS5 (RFC 1928) proxy protocol over alkcall channels: vendored fast-socks5 typestate core plus producer/consumer sessions. Producer/consumer protocol crate." +description = "SOCKS5 (RFC 1928) proxy protocol over alkcall channels: targeted-forked fast-socks5 typestate core plus producer/consumer sessions. Producer/consumer protocol crate." repository = "https://git.alk.dev/alkdev/alksocks" keywords = ["socks5", "proxy", "rfc1928", "channels", "alkcall"] categories = ["network-programming", "asynchronous"] diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 5ba7489..8edda7b 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,8 +1,10 @@ --- status: draft -last_updated: 2026-09-21 (the vendor extraction amendment — ADR-011 -§Amendment, ADR-009 §Amendment; Phase 1 initial draft 2026-09-20 — -the OQ-SK ledger promoted into ADRs 001-012 and open-questions.md) +last_updated: 2026-09-22 (the targeted fork — ADR-013; the module is +`src/rfc1928/`; the vendor posture retired. The vendor extraction +amendment ADR-011 §Amendment 2026-09-21; Phase 1 initial draft +2026-09-20 — the OQ-SK ledger promoted into ADRs 001-012 and +open-questions.md) --- # alksocks — Architecture @@ -24,8 +26,10 @@ POC-complete (all numbered POCs passed or were dropped with the scope decision); the OQ-SK ledger's Phase 1 inputs are collected and promoted into ADRs 001–012. All docs are `draft` — they transition to `reviewed` when their open questions resolve and the architecture -review passes. `src/` does not exist yet; implementation follows the -decomposition phase. +review passes. The RFC 1928 core is implemented (the targeted fork, +ADR-013 — the vendor group's six tasks landed and passed the +extraction review); the remaining layers follow the decomposition +phase. ## Documents @@ -35,7 +39,7 @@ decomposition phase. | [wire.md](wire.md) | draft | The wire format: the open op (trivial params — ADR-001), the phase model, the data planes (CONNECT pass-through / ASSOCIATE frames / BIND phases), sentinels | | [producer.md](producer.md) | draft | The producer half: the session-gate establisher, command dispatch, the three seams (dial/egress/accept), scope gates, the local backend | | [consumer.md](consumer.md) | draft | The consumer half: the channels-native sessions (connect/associate/bind), the front doors, door auth (ADR-005), teardown ownership | -| [vendor.md](vendor.md) | draft | The inline vendor: the extracted fast-socks5 subset's scope (extraction, not wholesale copy), the two carried changes, the maintenance posture | +| [vendor.md](rfc1928.md) | draft | The RFC 1928 core: the targeted fork of fast-socks5's `T`-generic subset (ADR-013; the extraction mechanics in ADR-011) — the fork's scope, the deliberate modifications, the maintenance posture | | [bast.md](bast.md) | draft | The BAST document for the ASSOCIATE datagram frame (ADR-003); pass-through stages have no binary framing to describe | | [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-SK-01..09 promoted; new questions as OQ-SK2-06.. — the SK2 numbering is not contiguous from 01, working-draft numbers were consumed) | @@ -53,8 +57,9 @@ decomposition phase. | [008](decisions/008-provider-defaults.md) | Egress/Accept Provider Defaults — Local Socket and Composed (Direct Op) | Accepted | | [009](decisions/009-feature-gates.md) | Feature-Gate Layout — `local` and the Composition Boundary | Accepted (amended 2026-09-21) | | [010](decisions/010-channel-capacity.md) | Channel Capacity — Proxy-Workload Recommendation, Not a Wire Change | Accepted | -| [011](decisions/011-inline-vendor.md) | the Inline Vendor — fast-socks5's `T`-Generic Subset, In-Tree | Accepted (amended 2026-09-21) | +| [011](decisions/011-inline-vendor.md) | the Inline Vendor — fast-socks5's `T`-Generic Subset, In-Tree | Superseded by ADR-013 (extraction mechanics remain authoritative) | | [012](decisions/012-target-policy.md) | Target Policy and the Virtual-Subnet Translation Layer | Accepted | +| [013](decisions/013-targeted-fork.md) | The Targeted Fork — the Extracted Subset Is Owned, Not Vendored | Accepted | Upstream ADRs (alkcall/alktunnels/alknet) are referenced by their own numbers and are NOT ported — the alkcall originals at @@ -91,15 +96,20 @@ format, `pump_bidi`, identity seam, the channel caps). convention holds for all three commands; the proxy-workload cap posture is ADR-010 (config, not wire). 7. **Wasm-clean default crate** (ADR-009): the default crate is - protocol-only; the vendor is an extraction with zero kernel-net - code — there is no `net` feature (ADR-011 §Amendment); `local` is - opt-in and non-wasm; `composed` rides alktunnels' wasm-clean - default (verified with the wasm check at implementation). -8. **The vendor is first-class** (ADR-011): the `T`-generic subset - extracted in-tree with two carried changes (BIND, `new_reply`); - `/workspace/fast-socks5` stays the differential-testing reference - and the only place the full upstream surface (the kernel-net - region) exists; nothing waits on upstream. + protocol-only; the RFC 1928 core is an extraction with zero + kernel-net code — there is no `net` feature (ADR-011 §Amendment); + `local` is opt-in and non-wasm; `composed` rides alktunnels' + wasm-clean default (verified with the wasm check at + implementation). +8. **The RFC 1928 core is owned** (ADR-013, superseding ADR-011's + vendor posture): the extracted `T`-generic subset is a targeted + fork (`src/rfc1928/`, fork point `5b18d4d`) — project + conventions apply unconditionally, the lint-allow/verbatim + apparatus is dissolved; `/workspace/fast-socks5` stays the + differential-testing behavioral reference and the only place the + full upstream surface (the kernel-net region) exists; nothing + waits on upstream and nothing is absorbed from it (a future + upstream fix is a hand-port, guarded by the differential suite). ## Document Lifecycle diff --git a/docs/architecture/bast.md b/docs/architecture/bast.md index 0c6f49d..1cc6b0e 100644 --- a/docs/architecture/bast.md +++ b/docs/architecture/bast.md @@ -18,7 +18,7 @@ conforming to the BAST meta-schema at `https://alk.dev/bast/v1/schema` well-formedness. **alksocks does not depend on alktype.** The runtime codec is the -vendored fast-socks5 address encoding + the length-framing codec +forked RFC 1928 address encoding + the length-framing codec (shaped on alktunnels' `frame_datagram`/`DatagramReader`, ADR-003); this BAST document is the human-readable contract describing what those round-trip. If runtime validation against the BAST becomes @@ -37,8 +37,8 @@ plus raw pass-through. Per the BAST format spec, a BAST document describes **binary data layouts** — not JSON payloads. The RFC 1928 conversation itself (negotiation, command, replies, the RFC UDP datagram header at front -doors) is specified by RFC 1928 and implemented by the vendored -fast-socks5 codecs ([vendor.md](vendor.md)) — it is not re-specified +doors) is specified by RFC 1928 and implemented by the forked +RFC 1928 codecs ([rfc1928.md](rfc1928.md)) — it is not re-specified here. This document covers exactly the alksocks-specific binary framing: @@ -51,7 +51,7 @@ What this BAST deliberately does **not** cover: - **The RFC 1928 wire rules** — method negotiation, command/reply encoding, the `addr` sub-layout (ATYP + addr + port), the RFC UDP datagram header (RSV/FRAG/ATYP/addr/port, front doors only). The - RFC is normative; the vendored codecs implement it. + RFC is normative; the forked codecs implement it. - **The open-op params JSON shape** (`{}`) — UTF-8 JSON on the call plane; ADR-001. - **The channels 8-byte chunk header** — the channels layer's own @@ -99,7 +99,7 @@ arithmetic).) Maps 1:1 to the on-wire bytes: `len` as `uint16` big-endian (2 bytes), then exactly `len` bytes = `addr + data`. The `addr` -sub-layout is the RFC 1928 address encoding as vendored +sub-layout is the RFC 1928 address encoding as forked (`TargetAddr::to_be_bytes()`: ATYP `0x01` IPv4 / `0x03` domain / `0x04` IPv6, then the address, then the u16 BE port). RSV/FRAG do not ride the tunnel wire (ADR-003); FRAG ≠ 0 is refused at front @@ -129,7 +129,7 @@ upstream's, not re-specified here. - [ADR-003](decisions/003-udp-handle-swap.md) (the handle swap + the frame), [ADR-001](decisions/001-alpn-and-open-op.md) (the JSON open-op shape — outside BAST scope), - [ADR-011](decisions/011-inline-vendor.md) (the vendored codecs) + [ADR-011](decisions/011-inline-vendor.md) (the forked codecs) - [wire.md](wire.md) (the normative prose; §The Data Plane) - alktunnels `bast.md` (the BAST precedent — same meta-schema, same "binary framing only" scope rule; the length codec's original diff --git a/docs/architecture/consumer.md b/docs/architecture/consumer.md index cbc7a8f..fc9f01b 100644 --- a/docs/architecture/consumer.md +++ b/docs/architecture/consumer.md @@ -29,7 +29,7 @@ Opens `channels/socks5/sub` via `ChannelClient::open_channel` (alkcall ADR-037/043), then drives the client side of the RFC conversation in-stream over the channel's `BiStream`: -- the vendored client typestate's `use_stream` path — generic over +- the forked client typestate's `use_stream` path — generic over the backing stream (POC #1: `Socks5Stream` impls `AsyncRead + AsyncWrite` itself, so the session over a `BiStream` is the intended use, not a hack); @@ -46,7 +46,7 @@ The UDP ASSOCIATE client (POC #2's shape): **receives the sentinel `BND.ADDR` and never uses it for addressing** (the handle is the channel; ADR-003); - `send_datagram(dst, bytes)` frames once - (`[len: u16 BE][addr + data]`, the vendored address encoding) and + (`[len: u16 BE][addr + data]`, the RFC 1928 address encoding) and writes the frame; - `recv_datagram()` returns `Some((source_addr, bytes))` per datagram — its incremental reader state (mid-frame position + @@ -63,16 +63,16 @@ The UDP ASSOCIATE client (POC #2's shape): ### The BIND client -The BIND reply#2 reader (POC #5's `read_reply2` sketch): the vendored +The BIND reply#2 reader (POC #5's `read_reply2` sketch): the forked client `request()` reads reply#1; reply#2 is hand-read with the -vendored public `read_address` codec (reply#1's codec applied again; +forked public `read_address` codec (reply#1's codec applied again; no second codec). The session then rides the pass-through data plane. The BIND client is a wrapper-aware-consumer convenience — vanilla clients reach BIND through the front door instead. ### Error surface -`SocksServerError` / `SocksError` (the vendored surface) mapped +`SocksServerError` / `SocksError` (the forked surface) mapped faithfully; channel-open failures are the typed `channel:open_failed` reasons (alkcall ADR-049). A refused session never yields a phantom channel; a refused target is an RFC reply @@ -110,7 +110,7 @@ BIND door): channel identity/endpoint selection). The SOCKS password never crosses the channel as a SOCKS password; the channel identity never reaches the vanilla client. The door checks credentials - itself (no vendored `AuthMethod` impl needed — the door owns the + itself (no forked `AuthMethod` impl needed — the door owns the accept loop). Door variant selection (NoAuth vs cred-bearing; the UDP door's bind @@ -143,7 +143,8 @@ door.serve(channel_client).await?; | [004](decisions/004-dial-callback-shape.md) | Ownership rides the handle | handle payloads own channel sessions; the session-teardown posture is inherited from **alktunnels ADR-005** (the consumer session type owns teardown) | | [005](decisions/005-auth-split-by-door.md) | Door auth | RFC 1929 at the door; static credential→identity mapping | | [007](decisions/007-bind-in-base.md) | BIND client | the reply#2 reader; vanilla clients via the door | -| [011](decisions/011-inline-vendor.md) | The vendor | the client `use_stream` path in-tree | +| [011](decisions/011-inline-vendor.md) | The extraction | the client `use_stream` path in-tree | +| [013](decisions/013-targeted-fork.md) | The targeted fork | the core is owned (`src/rfc1928/`) | ## Open Questions diff --git a/docs/architecture/decisions/002-protocol-base.md b/docs/architecture/decisions/002-protocol-base.md index 4a2dc5c..56fb9e8 100644 --- a/docs/architecture/decisions/002-protocol-base.md +++ b/docs/architecture/decisions/002-protocol-base.md @@ -2,8 +2,10 @@ ## Status -Accepted (2026-09-20, Phase 1; the inline-vendor posture is -OQ-SK-04's resolution, recorded in [ADR-011](011-inline-vendor.md)) +Accepted (2026-09-20, Phase 1; the extracted-and-forked posture is +OQ-SK-04's resolution — the extraction mechanics in +[ADR-011](011-inline-vendor.md), the ownership posture in +[ADR-013](013-targeted-fork.md)) ## Context @@ -28,13 +30,14 @@ functionally complete (a full RFC 1928 CONNECT conversation passes pure codecs (`new_udp_header`/`parse_udp_request`). - **Only the explicit API is used.** The legacy `Socks5Server`/`Socks5Socket`/`Incoming` bind-based API is - deprecated upstream and is out — never vendored into the default + deprecated upstream and is out — never forked into the default path. -- **Base = v1.0.0, inline-vendored as an extraction** (see ADR-011 - for the vendor mechanics and the two carried changes). +- **Base = v1.0.0, extracted in-tree and forked** (ADR-011 for the + extraction mechanics and the two carried changes; ADR-013 for the + targeted-fork ownership posture). `/workspace/fast-socks5` remains the read-only reference for - differential testing of RFC edge cases; the vendor point is noted - in the vendored code. + differential testing of RFC edge cases; the fork point is noted + in the module docs and the bundled license notice. - **Refusals map faithfully per RFC 1928** — `ReplyError` codes (`as_u8`/`from_u8`) are the error vocabulary at the command plane; refusals are never collapsed into generic errors (AGENTS.md @@ -42,7 +45,7 @@ functionally complete (a full RFC 1928 CONNECT conversation passes ## Consequences -- The crate owns its protocol code in-tree (the vendor), so BIND and +- The crate owns its protocol code in-tree (the fork), so BIND and the `new_reply` publication ride the same tree — no upstream waits. - The typestate's interception points are the layering seams: command-read (the dial/accept dispatch), `transfer` (the raw @@ -55,8 +58,8 @@ functionally complete (a full RFC 1928 CONNECT conversation passes ## References -- OQ-SK-04 (resolved: inline vendor), OQ-SK-03 (the `run_udp_proxy_ - custom` caveat) +- OQ-SK-04 (resolved: the extracted-and-forked base), OQ-SK-03 (the + `run_udp_proxy_custom` caveat) - `docs/research/iroh-socks5-eval.md` (the base comparison), POC #1 + POC #4 (`poc-connect-wasm-findings.md`) - Sibling precedent: alktunnels ADR-002 (one-ALPN option A) \ No newline at end of file diff --git a/docs/architecture/decisions/009-feature-gates.md b/docs/architecture/decisions/009-feature-gates.md index 67d1ec0..c25fd38 100644 --- a/docs/architecture/decisions/009-feature-gates.md +++ b/docs/architecture/decisions/009-feature-gates.md @@ -95,7 +95,8 @@ implying it. ADR-011's extraction amendment dissolves it: ## References -- OQ-SK-04 (resolved: inline vendor — see ADR-011), OQ-SK-01/03/08 +- OQ-SK-04 (resolved: the extracted-and-forked base — ADR-011/ + ADR-013), OQ-SK-01/03/08 (the providers the features gate) - POC #4 `poc-connect-wasm-findings.md` (the measured gate — now the extraction recipe), diff --git a/docs/architecture/decisions/010-channel-capacity.md b/docs/architecture/decisions/010-channel-capacity.md index bbe684b..378c55d 100644 --- a/docs/architecture/decisions/010-channel-capacity.md +++ b/docs/architecture/decisions/010-channel-capacity.md @@ -21,7 +21,7 @@ plus documentation, never wire. Considered and rejected: in-channel port multiplexing (a `[port: u16][len][payload]` per-chunk prefix) — it re-opens the demux-vs-phase trade ADR-003 (the handle swap) resolved, breaks -the 0-B pass-through that lets the vendored `transfer` drop in +the 0-B pass-through that lets the forked `transfer` drop in unchanged, imposes chunk boundaries on a stream RFC 1928 treats as unstructured, and violates the one-channel-per-SOCKS5-session convention (AGENTS.md #10). diff --git a/docs/architecture/decisions/011-inline-vendor.md b/docs/architecture/decisions/011-inline-vendor.md index cd1589a..f37d71e 100644 --- a/docs/architecture/decisions/011-inline-vendor.md +++ b/docs/architecture/decisions/011-inline-vendor.md @@ -2,10 +2,16 @@ ## Status -Accepted (2026-09-20, Phase 1; OQ-SK-04's resolution — the ownership -premise corrected the same day). Amended 2026-09-21 (pre-decomposition): -the vendor scope is pinned as **extraction** — the kernel-net handlers -are not copied; see §Amendment. +Superseded by [ADR-013](013-targeted-fork.md) (2026-09-22): the +maintenance posture (the vendor framing — re-base/re-vendor +discipline, the verbatim/lint-allow apparatus) is retired; the +extracted region is a targeted fork, module `rfc1928`. **The +extraction mechanics here remain authoritative** — the cut table, +the two carried changes (BIND, `new_reply`), the §Amendment +(extraction, not wholesale copy; the net gate dissolves) describe +what was copied and what was not; only the post-copy maintenance +posture changed. Originally Accepted 2026-09-20 (Phase 1; +OQ-SK-04's resolution), Amended 2026-09-21 (pre-decomposition). ## Context diff --git a/docs/architecture/decisions/013-targeted-fork.md b/docs/architecture/decisions/013-targeted-fork.md new file mode 100644 index 0000000..a57f0c3 --- /dev/null +++ b/docs/architecture/decisions/013-targeted-fork.md @@ -0,0 +1,148 @@ +# ADR-013: The Targeted Fork — fast-socks5's `T`-Generic Subset Is Owned, Not Vendored + +## Status + +Accepted (2026-09-22, post-extraction-review; supersedes the ADR-011 +maintenance posture — ADR-011 stays for the extraction mechanics and +the cut table; the ADR-011 §Amendment is subsumed here) + +## Context + +ADR-011 pinned the inline-vendor posture as **extraction** (the +`T`-generic subset copied in-tree, kernel-net region not copied; the +net gate dissolving), and the vendor group — five implementation +tasks plus the review gate — landed it in commits `57b107b..5b18d4d`, +with the review (`tasks/implementation/ +review-vendor-extraction.md`) as the fidelity gate: the file-by-file +diff vs `/workspace/fast-socks5` v1.0.0 categorizes every hunk, and +every review criterion passed. + +The review also made the maintenance posture's cost visible. The +tree is now permanently diverged from upstream in five categories: + +1. **The extraction cut** — the kernel-net region, `socks4`, the + legacy `Config`/`Authentication` layer, `Socks5Datagram`, the + convenience dialers, the DNS helper: gone. +2. **The carried changes** — BIND's reply surface, the published + `new_reply`. +3. **The panic translations** — `ReplyError::from_u8` (the review's + F-1), `use_password_auth`, `reply_error`'s parse. +4. **The error-pruning adjustments** — anyhow/log/async-trait out, + error variants pruned, `MissingTargetAddr` added. +5. **The lint-allow posture** — module-level `#[allow]`s preserving + upstream style (`u8::max_value()`, needless lifetimes, + `match_like_matches_macro`, `io_other_error`, `wrong_self_ + convention`, the F-4 verbatim string) to keep the diff mechanical. + +Category 5 exists solely to keep categories 1–4 reconcilable by a +line-based diff. But a future re-base against an upstream release +was never going to be a clean mechanical diff again — it was always +going to be manual reconciliation across all five categories. The +vendor posture's maintenance apparatus (verbatim preservation, the +lint allows, the re-vendor/adjustment-list discipline) pays a +permanent tax to serve a re-base path that would never be used. + +The "vendor" framing also produced a real defect: the review's +F-1 (`ReplyError::from_u8`'s upstream `unreachable!()` arm kept +verbatim — wire-reachable, a misbehaving peer could panic the +consumer task) escaped precisely because the vendor posture +maintained a second, weaker instruction set ("verbatim + adjustments +list" instead of "project conventions, unconditionally"). The +fidelity-vs-conventions tension is structural to the vendor framing; +the review caught the escape, but the seam itself was the vendor +framing's. + +The fork point is now — post-review, post-F-1 — the cleanest line +the tree will ever present: the extraction provenance is in git +history (`57b107b..5b18d4d`), the review is the recorded fidelity +gate, and the five divergence categories are documented in ADR-011, +vendor.md, and the review task. "The tree as of the review commit +(`5b18d4d`) is the fork point" is a trivially defensible line. + +## Decision + +- **The extracted region is a targeted fork, fully owned by this + crate.** The "vendor" framing is retired: there is no upstream + tree to stay reconcilable with, no re-vendor, no adjustment-list + apparatus, no second instruction set. Project conventions (the + AGENTS.md set) apply unconditionally to the forked code. +- **The fork point is the tree as of commit `5b18d4d`** (the + extraction review's gate commit). The extraction provenance is + carried in git history (`57b107b..5b18d4d`), the module docs, and + the extended LICENSE notice; the review task is the recorded + fidelity gate. +- **Module naming: the forked core is `src/rfc1928/`, re-exported + as `alksocks::rfc1928`.** "vendor" names an ownership relation + (now gone); "socks5" would stutter with the crate name + (`alksocks::socks5`) and collide with the future module structure + (the producer/consumer/adapter layers are also SOCKS5-shaped). + `rfc1928` names what the module *is* — the RFC 1928 wire + vocabulary and the state machines — permanent and collision-free. +- **The lint-allow posture dissolves.** The module-level `#[allow]`s + and the "verbatim lint posture" module-doc paragraphs are removed; + the suppressed smells are fixed per project conventions: + `u8::max_value()` → `u8::MAX`, needless lifetimes elided, + `match_like_matches_macro` applied, `io::Error::new(Other)` → + `io::Error::other`, `wrong_self_convention` fixed on `AuthMethod:: + new(self, ..)`, the F-4 string re-worded (it no longer names a + cut function), and the `#[allow(dead_code)]` on + `AuthenticationMethod` swept with actual usage. Upstream's + commented-out dead code stays dropped (F-2). +- **The license mechanics**: MIT permits sublicensing, so + modifications after the fork point are this project's under the + crate license (MIT OR Apache-2.0, per Cargo.toml). The bundled + notice is **extended, not replaced**: upstream MIT text + + copyright (the notice-preservation obligation, perpetual) + a + fork-point statement + the modification note. +- **The reference checkout survives as a behavioral reference.** + `/workspace/fast-socks5` stays for differential testing of RFC + edge cases; the differential suite (tests/vendor-differential.rs) + pins bytes, not upstream text, so it survives the rename + untouched. Textual-diff discipline is retired; behavioral + differential testing is retained. +- **The cut table is re-read as the not-forked inventory** — the + map of what to hand-port if a future need surfaces (e.g. the + kernel-net region for a standalone vanilla-server example). The + hand-port path replaces the re-vendor path: port against the + reference checkout, adapt to the crate's conventions, and let the + suite guard it. +- **The honest cost**: the cheap upstream-fix path closes. A future + fast-socks5 fix (a reply-code mapping correction, a codec fix) + arrives as a hand-port against the reference checkout, not a + diff absorb. Risk is assessed low — fast-socks5 is a quiet + project, and the extracted surface is the well-trodden RFC + 1928 path. + +## Consequences + +- **Positive**: one instruction set (project conventions, + unconditionally); the lint-allow tax and the adjustment-list + apparatus dissolve; smells are fixed at the only cost that + matters — a one-time cleanup verified by the existing suite. + The F-1 seam class (conventions-vs-fidelity gaps) is closed + structurally. +- **Positive**: the module name is stable for the crate's future + (no `alksocks::socks5` stutter, no `socks5::producer` collision + pressure). +- **Negative (accepted)**: the cheap upstream-fix path closes — a + future upstream fix is a hand-port against the reference + checkout, guarded by the differential suite. Assessed low-risk + (quiet upstream, well-trodden RFC surface). +- **Neutral**: the differential suite survives the rename untouched + (it pins bytes, not upstream text). `/workspace/fast-socks5` keeps + its role as the behavioral reference; the only thing it loses is + being the target of a line-based diff. +- **Neutral**: the fork-point line (`5b18d4d`) is recorded in git + history, the module docs, and the extended LICENSE notice; the + extraction provenance (the ADR-011 trail) remains readable. + +## References + +- ADR-011 (the extraction mechanics, the cut table, the carried + changes — the mechanics remain authoritative), ADR-002 (the + protocol base), ADR-009 (feature gates, no `net` feature) +- `tasks/implementation/review-vendor-extraction.md` (the fidelity + gate + F-1), `docs/architecture/rfc1928.md` (the owned-core + spec) +- `/workspace/fast-socks5` (v1.0.0, MIT — the behavioral + reference for differential testing) \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index c1aa6b7..1e9bad7 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,8 +1,8 @@ --- status: draft -last_updated: 2026-09-21 (OQ-SK-04's resolution updated to the -extraction posture; the OQ-SK ledger OQ-SK-01..09 promoted 2026-09-20; -new questions continue as OQ-SK2-06..) +last_updated: 2026-09-22 (OQ-SK-04's resolution updated to the +targeted-fork posture — ADR-013; the OQ-SK ledger OQ-SK-01..09 +promoted 2026-09-20; new questions continue as OQ-SK2-06..) --- # alksocks — Open Questions @@ -43,7 +43,7 @@ sequence starts at the numbers in use. Format per - **Resolution**: 2026-09-20 (Phase 1; baseline settled 2026-09-13, POC #2 validated 2026-09-14). The handle swap: one channel per association; `{addr, data}` frames in-band (`[len: u16 BE]` over - the vendored address encoding); association lifetime = channel + the RFC 1928 address encoding); association lifetime = channel lifetime; RFC codec at front doors only; sentinel BND.ADDR in-band. The datagram framing (the one-way door) is now decided with empirical ground under it (open cost measured ~0.8 ms; LRU + @@ -84,14 +84,20 @@ sequence starts at the numbers in use. Format per - **Origin**: [phase-0.md](../research/phase-0.md) (OQ-SK-04) - **Status**: resolved — [ADR-011](decisions/011-inline-vendor.md) + (the extraction mechanics), [ADR-013](decisions/ + 013-targeted-fork.md) (the ownership posture) - **Priority**: high - **Resolution**: 2026-09-20 (Phase 0's final resolution, promoted; - amended 2026-09-21 by ADR-011 §Amendment). The inline vendor as - **extraction**: the `T`-generic subset in-tree with zero kernel-net - code (wasm-clean by construction, no `net` feature); two carried - changes (BIND, `new_reply` publication); v1.0.0 noted; - differential testing against `/workspace/fast-socks5`. -- **Cross-references**: ADR-002, ADR-009, ADR-011 + amended 2026-09-21 by ADR-011 §Amendment; superseded as a posture + 2026-09-22 by ADR-013). The extracted region is a **targeted + fork**: the `T`-generic subset in-tree with zero kernel-net code + (wasm-clean by construction, no `net` feature); two carried + changes (BIND, `new_reply` publication); fork point = the tree as + of the extraction-review commit (`5b18d4d`), module `src/rfc1928/`; + differential testing against `/workspace/fast-socks5` (behavioral + reference — the cheap upstream-fix path closes; a future upstream + fix is a hand-port, guarded by the suite). +- **Cross-references**: ADR-002, ADR-009, ADR-011, ADR-013 ### OQ-SK-07: POC register diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d897ad6..1735317 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,8 +1,9 @@ --- status: draft -last_updated: 2026-09-21 (the vendor extraction amendment — no `net` -feature; Phase 1 initial draft 2026-09-20 — the OQ-SK ledger promoted -into ADRs 001-012 and open-questions.md) +last_updated: 2026-09-22 (the targeted fork — ADR-013, the module is +`src/rfc1928/`; the vendor extraction amendment — no `net` feature — +2026-09-21; Phase 1 initial draft 2026-09-20 — the OQ-SK ledger +promoted into ADRs 001-012 and open-questions.md) --- # alksocks — Overview @@ -18,7 +19,7 @@ the one alktunnels explicitly deferred to composition. This document covers the crate's purpose, the resource model in brief, dependencies, the ALPN, feature gates, and the module map; component details are in the sibling documents ([wire.md](wire.md), [producer.md](producer.md), -[consumer.md](consumer.md), [vendor.md](vendor.md)). +[consumer.md](consumer.md), [rfc1928.md](rfc1928.md)). ## What @@ -31,10 +32,10 @@ front doors: `ChannelCore::register_openable_with_establisher`, alkcall ADR-047/049). The establisher is a *session gate* (validates the opener identity; the SOCKS5 target is unknown at open time); the - open handler runs the RFC 1928 state machine in-stream over the - channel's `BiStream` — the vendored fast-socks5 typestate - ([vendor.md](vendor.md)) drives negotiation, command read, and - replies; the dial/egress/accept seams (ADR-004) supply the I/O + open handler runs the RFC 1928 state machine in-stream over the + channel's `BiStream` — the forked RFC 1928 core + ([rfc1928.md](rfc1928.md)) drives negotiation, command read, and + replies; the dial/egress/accept seams (ADR-004) supply the I/O planes; `alkcall::channels::pump_bidi` (ADR-050) is the data plane for CONNECT and BIND's post-reply#2 stage. - **The consumer half** ([consumer.md](consumer.md)): a typed @@ -72,7 +73,7 @@ binding decision belongs to the caller. whole host's traffic rides the alk* egress. This crate is the SOCKS5 leg; alktunnels carries the tunnel; tun2proxy closes the loop. -- The substrate-agnostic protocol layer: the vendored typestate is +- The substrate-agnostic protocol layer: the forked RFC 1928 core is generic over `T: AsyncRead + AsyncWrite + Unpin` — a channels `BiStream` drops in directly (POC #1), a `TcpStream` drops in at the local backends. Neither substrate leaks into the protocol @@ -92,7 +93,7 @@ A SOCKS5 channel's life (full detail in [wire.md](wire.md) and phantom channel. 2. **RFC conversation (the control plane, in-stream).** The RFC 1928 handshake (method negotiation → command → reply) runs in-stream - over the `BiStream`, driven by the vendored typestate. Command + over the `BiStream`, driven by the forked typestate. Command dispatch: CONNECT/ASSOCIATE require `socks5:open`; BIND requires `socks5:bind` (the command gate, ADR-006). 3. **Data plane.** CONNECT: the dial seam (ADR-004) produces boxed @@ -139,18 +140,19 @@ alksocks (composed feature) ``` Same posture as alktty/alktunnels: the default crate is -protocol-only and wasm-clean — the vendor is an *extraction* of -fast-socks5's `T`-generic subset, containing zero kernel-net code, +protocol-only and wasm-clean — the RFC 1928 core is an *extraction* +of fast-socks5's `T`-generic subset, containing zero kernel-net code, so there is no `net` feature at all (ADR-011 §Amendment); substrate I/O rides `local` alone (ADR-009 §Amendment). -`fast-socks5` is NOT a Cargo dependency — it is extracted in-tree -(ADR-011). +`fast-socks5` is NOT a Cargo dependency — it was extracted in-tree +and forked (ADR-013); `/workspace/fast-socks5` stays as the +differential-testing behavioral reference. ## Feature Gates | Feature | Contents | Wasm | |---------|----------|------| -| *(default)* | the extracted `T`-generic subset (codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types — protocol only; zero kernel-net code, wasm-clean by construction | yes | +| *(default)* | the RFC 1928 core (`src/rfc1928/`, ADR-013 — codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types — protocol only; zero kernel-net code, wasm-clean by construction | yes | | `local` | the crate's first-party local backends: TCP dial, UDP egress socket, BIND `TcpListener` accept, the front doors (consumer + producer local shapes); brings `tokio/net` + `socket2` into the graph | no (build error on wasm, the alktty posture) | | `composed` | the composed providers over alktunnels 0.2.0: direct-op egress (lazy destination→tunnel table), bind-first listen accept — adds the alktunnels dep | yes (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface — verify with the wasm check at implementation) | @@ -159,10 +161,13 @@ substrate I/O rides `local` alone (ADR-009 §Amendment). ``` src/ lib.rs — re-exports; crate docs - vendor/ — the extracted fast-socks5 T-generic subset (ADR-011; - vendor point v1.0.0 noted in-tree): codecs, typestate, - client use_stream path; two carried changes (BIND reply - writer, new_reply publication); no kernel-net code + rfc1928/ — the RFC 1928 core: the targeted fork of + fast-socks5's T-generic subset (ADR-013; + extraction per ADR-011; fork point noted + in-tree): consts, codecs, errors, typestate, + client use_stream path; the fork's two + carried changes (BIND reply surface, + new_reply publication); no kernel-net code params.rs — the trivial params schema + SOCKS5_OPEN_SCOPE/ SOCKS5_BIND_SCOPE constants (ADR-001/006) wire.rs — the ASSOCIATE datagram frame codec, re-implemented @@ -199,16 +204,17 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | ADR | Decision | Summary | |-----|----------|---------| | [001](decisions/001-alpn-and-open-op.md) | ALPN + open op + trivial params | `alk/socks5`, `channels/socks5/sub`, `{}` — the RFC conversation is the data plane | -| [002](decisions/002-protocol-base.md) | Protocol base | fast-socks5's explicit typestate API (vendored); faithful reply-code mapping | +| [002](decisions/002-protocol-base.md) | Protocol base | fast-socks5's explicit typestate API (extracted + forked, `src/rfc1928/`); faithful reply-code mapping | | [003](decisions/003-udp-handle-swap.md) | The handle swap | one channel per association; `{addr, data}` frames in-band; RFC codec at front doors only | | [004](decisions/004-dial-callback-shape.md) | The dial-callback shape | function-not-trait at three seams (dial/egress/accept); ownership rides the handle | | [005](decisions/005-auth-split-by-door.md) | Auth: "both," split by the door | producer = identity seam; front door = RFC 1929 + credential→identity mapping (static config) | | [006](decisions/006-access-control.md) | Access-control posture | `socks5:open` gate + `socks5:bind` command gate; target policy = the dialer | | [007](decisions/007-bind-in-base.md) | BIND in the base crate | phase model extended (reply#1 → quiet wait → accept → reply#2 → pass-through); additive | | [008](decisions/008-provider-defaults.md) | Provider defaults | local-socket + composed (direct op) providers, both feature-gated; no implicit dialer | -| [009](decisions/009-feature-gates.md) | Feature gates | `local` + `composed`; no `net` feature (the vendor is wasm-clean by construction — ADR-011 §Amendment); default crate wasm-clean | +| [009](decisions/009-feature-gates.md) | Feature gates | `local` + `composed`; no `net` feature (the RFC 1928 core is wasm-clean by construction — ADR-011 §Amendment); default crate wasm-clean | | [010](decisions/010-channel-capacity.md) | Channel capacity | no wire change; assembly-layer config + the documented proxy-workload recommendation | -| [011](decisions/011-inline-vendor.md) | The inline vendor | the `T`-generic subset extracted in-tree; two carried changes (BIND, `new_reply`); v1.0.0 noted | +| [011](decisions/011-inline-vendor.md) | The extraction | the `T`-generic subset copied in-tree; two carried changes (BIND, `new_reply`); v1.0.0 noted (superseded as a posture by ADR-013) | +| [013](decisions/013-targeted-fork.md) | The targeted fork | the extracted region is owned; `src/rfc1928/`; fork point `5b18d4d`; hand-port replaces re-vendor | | [012](decisions/012-target-policy.md) | Target policy | dialer-refuses; domain targets pass through unresolved; virtual subnets = assembly policy | ## Open Questions diff --git a/docs/architecture/producer.md b/docs/architecture/producer.md index 600a63c..3d31410 100644 --- a/docs/architecture/producer.md +++ b/docs/architecture/producer.md @@ -10,7 +10,7 @@ resource: it registers the `channels/socks5/sub` open op (ADR-001) and serves RFC 1928 inside each opened channel. The layering is the POC #1/#2/#5-validated shape: **establisher = session gate** (the target is unknown at open time), **open handler = the RFC state -machine + command dispatch** (the vendored typestate driving, ADR-002), +machine + command dispatch** (the forked typestate driving, ADR-002), **I/O planes = the three seams** (dial/egress/accept, ADR-004). ## The Open Op and the Session-Gate Establisher @@ -44,7 +44,7 @@ behavior, but the wiring requirement is implicit. ## The Open Handler: RFC Conversation + Command Dispatch -The open handler runs the vendored typestate over the channel's +The open handler runs the forked typestate over the channel's `BiStream` (`T`-generic — the POC #1 fit): 1. **Negotiation**: `start(inner)` → `negotiate_auth(&methods)` — @@ -154,13 +154,14 @@ are feature-gated constructors over the same seams (ADR-008/ADR-009): | ADR | Decision | Summary | |-----|----------|---------| | [001](decisions/001-alpn-and-open-op.md) | ALPN + op + params | `channels/socks5/sub`, trivial params, session-gate establisher | -| [002](decisions/002-protocol-base.md) | Protocol base | the vendored typestate drives the conversation | +| [002](decisions/002-protocol-base.md) | Protocol base | the forked typestate drives the conversation | | [004](decisions/004-dial-callback-shape.md) | The seams | DialFn / EgressFactory / BindAcceptFactory; ownership rides the handle | | [005](decisions/005-auth-split-by-door.md) | Auth | producer = identity seam; in-band NoAuth no-op | | [006](decisions/006-access-control.md) | ACL | `socks5:open` + `socks5:bind` command gate; dialer-refuses | | [007](decisions/007-bind-in-base.md) | BIND | the phase model; the accept variants | | [008](decisions/008-provider-defaults.md) | Providers | local + composed, feature-gated | -| [011](decisions/011-inline-vendor.md) | The vendor | the typestate subset in-tree | +| [011](decisions/011-inline-vendor.md) | The extraction | the typestate subset in-tree | +| [013](decisions/013-targeted-fork.md) | The targeted fork | the core is owned (`src/rfc1928/`) | ## Open Questions diff --git a/docs/architecture/rfc1928.md b/docs/architecture/rfc1928.md new file mode 100644 index 0000000..6bdafef --- /dev/null +++ b/docs/architecture/rfc1928.md @@ -0,0 +1,225 @@ +--- +status: draft +last_updated: 2026-09-22 (the targeted fork — ADR-013 supersedes +ADR-011's maintenance posture; the module is `src/rfc1928/`, the +fork point is `5b18d4d`, the lint-allow posture dissolves; +extraction review F-1: `ReplyError::from_u8`'s panic arm mapped to +`GeneralFailure`; Phase 1 initial draft 2026-09-20) +--- + +# alksocks — The RFC 1928 Core (the targeted fork) + +The crate owns its RFC 1928 protocol code in-tree: the **targeted +fork** of fast-socks5's `T`-generic subset ([ADR-013](decisions/ +013-targeted-fork.md); the extraction mechanics in +[ADR-011](decisions/011-inline-vendor.md); OQ-SK-04's resolution). +This document is the fork's scope — what was extracted, what was +not, the deliberate modifications — and the maintenance posture. +The protocol base decision is ADR-002; the feature gating is +ADR-009. + +The fork point is the tree as of the extraction-review commit +(`5b18d4d`); the extraction provenance lives in git history +(`57b107b..5b18d4d`), the module docs, and the bundled license +notice. The review (`tasks/implementation/ +review-vendor-extraction.md`) is the recorded fidelity gate. + +## The Mechanism: Extraction, Then Fork (ADR-011 §Amendment + ADR-013) + +The code arrived by **extraction** (ADR-011 §Amendment): copy *only* +the `T`-generic inventory from fast-socks5 v1.0.0 into the tree; the +kernel-net handlers are **not copied** — they stay in +`/workspace/fast-socks5` (the read-only reference checkout). +Concretely: POC #4's cfg map (its five-file inventory of what is +kernel-net-gated) is the base of the extraction recipe — copy what +POC #4 left *ungated*, minus the two recorded exclusions (the +`socks4` module and the legacy `Config`/`Authentication` layer — +see the not-forked inventory). The extraction contains zero +kernel-net code, so it is wasm-clean by construction; there is no +in-tree `net`-shaped feature gate. + +Post-extraction (ADR-013), the region is **owned code**: project +conventions apply unconditionally, there is no upstream tree to +stay reconcilable with, and the module is `src/rfc1928/` — named +for what it is (the RFC 1928 wire vocabulary and state machines), +not for an ownership relation it no longer has. + +## What was Extracted (now the forked core) + +From fast-socks5 **v1.0.0** (MIT, third-party upstream — +`/workspace/fast-socks5` is the read-only reference checkout; the +fork point is noted in the module docs and the license notice): + +| Piece | Why it rides | Notes | +|-------|--------------|-------| +| The server typestate (`Socks5ServerProtocol`, negotiate/auth/command-read/reply, `accept_no_auth`/`finish_auth`/`accept_password_auth`) | the producer's conversation driver (ADR-002) | `T`-generic | +| The auth traits (`AuthMethod`/`AuthMethodSuccessState`) | the negotiation machinery (the crate uses `accept_no_auth` in-band; the traits stay) | `T`-generic | +| `transfer` + `wait_on_tcp` | the typestate's own two-pump seam (the crate's data plane is `pump_bidi`; `transfer` is the interception-point shape) | `T`-generic | +| The codecs: `new_udp_header`/`parse_udp_request`, `TargetAddr`/`to_be_bytes`/`read_address`, `ReplyError`/`SocksError`/`SocksServerError` | the RFC wire vocabulary everywhere (front doors, associate frames, reply#2 reader) | pure; `new_reply` published in-tree (fork change) | +| The client types: `Socks5Stream::use_stream` + the `AsyncRead/AsyncWrite` impls | the consumer half's stream session (ADR-002; POC #1's fit) — the first-party associate session in `consumer.rs` was informed by this shape | `T`-generic | +| The non-net test modules (where extractable as unit tests) | RFC edge-case coverage rides in-tree | adjusted to the crate's test layout | + +## What was NOT Forked (the cut, re-read as the hand-port map) + +These stay in `/workspace/fast-socks5` (the differential-testing +reference); the table below records the boundary — under the fork +posture it is the **hand-port map**: what to port from the +reference checkout if a future need surfaces (port it, adapt it to +the crate's conventions, let the differential suite guard it — +ADR-013). Rows 1–5 are POC #4's five-file cfg map (what POC #4 +gated is what was not extracted); the socks4 row and the legacy +`Config`/`Authentication` layer are the recipe's two additions — +the socks4 module is out of RFC 1928 scope, and the legacy auth +layer was left ungated by POC #4 (it compiles without net) but no +extraction consumer reached it. None of it is forked: + +| Upstream file | Region NOT in `src/rfc1928/` | +|---|---| +| `src/server.rs` | `run_tcp_proxy`, `run_udp_proxy`/`run_udp_proxy_custom`, `udp_bind_random_port`, the legacy `Socks5Server`/`Incoming`/`Socks5Socket` (struct + impls), `DnsResolveHelper` trait + impl, `handle_udp_*`/`transfer_udp`, the net-only test module — plus the legacy `Config` + `Authentication` trait + impls (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`, `authenticate_callback`), which POC #4 left ungated but which only this cut region consumes (rows 1–5 are POC #4's gate map; the `Config`/`Authentication` layer and the socks4 row are this recipe's two additions) | +| `src/client.rs` | `Socks5Datagram` (struct + impl), the `Socks5Stream` convenience impl (`connect`/`connect_with_password`/`connect_raw`) | +| `src/util/stream.rs` | `tcp_connect*` fns (the `read_exact!`/`ready!` macros + `ConnectError` stay) | +| `src/util/target_addr.rs` | the `resolve_dns` method (the codec stays) | +| `src/lib.rs` | the net-heavy test module (consts/errors/UDP-header codec stay) | +| `src/socks4/` | the whole module (upstream's separate default-off `socks4` cargo feature; out of RFC 1928 scope) | + +**Why none of it is needed in-tree** (ADR-011 §Amendment): no crate +consumer routes through the kernel-net region — the producer drives +the typestate directly (the ASSOCIATE path is the POC #2 +typestate-driving shape; `run_udp_proxy_custom` is unusable on +channels regardless), the consumer sessions use `use_stream` plus +the first-party associate session, the doors are pure codecs + +first-party bridges, and the local backends are first-party tokio +code (ADR-008). The "producer-side local backend" is a bridge *onto +the channels-served service* (POC #1's door shape), not a standalone +vanilla server. If a future need surfaces (e.g. a standalone +vanilla-server example), the fix is hand-port that piece from the +reference checkout, as an additive decision (ADR-013). + +## The Deliberate Modifications (the fork's diff vs upstream v1.0.0) + +The fork's diff vs upstream v1.0.0 is deliberate and bounded — +**two changes touch the forked code**; the other two of the original +four dissolve with the kernel-net region: + +1. **BIND** — absent upstream (every handler refuses + `Socks5Command::TCPBind`). The crate drives the typestate itself + for the BIND shape (ADR-007); the fork carries the shape in-tree + (`reply_success`'s `sock_addr` as the reply#1 carrier; the + reply#2 writer using the published `new_reply`). +2. **`new_reply` publication** — the reply encoder becomes public + (a `reply(code, addr)`-shaped constructor) so the BIND reply#2 + writer doesn't re-encode the 10/22-byte reply form. + +Dissolved: the `net` feature gate (no net code in-tree) and the +`run_udp_proxy_custom` bind seam (the handler isn't forked). + +Plus the provenance notes: the fork point (upstream v1.0.0, +extraction `57b107b..5b18d4d`, fork at the review commit) and the +license notice (MIT, extended — ADR-013's license mechanics). + +Plus the **extraction-time adjustments** (mechanical edits applied +at extraction, before the fork): + +- The `socks4` module is out entirely (upstream's separate + default-off `socks4` cargo feature, which POC #4 never compiled; + out of RFC 1928 scope). `SocksError`'s `ReplySocks4Error` variant + (also `#[cfg(feature = "socks4")]` upstream) goes with it. +- The legacy `Config`/`Authentication` auth layer is out + (ungated upstream, but consumed only by the cut legacy/net region + — `Socks5Server`/`Socks5Socket`/the `run_*` handlers; the + typestate's own `accept_no_auth`/`accept_password_auth` are the + crate's in-band auth surface). With it go `async-trait` (its four + upstream uses are all on this layer), the example auth impls + (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`), + and `SocksError`'s `AuthenticationRejected`-consuming legacy + plumbing that only they exercise. +- Upstream `log` calls in the extracted region are dropped or + re-pointed at the crate's `tracing`. +- Upstream `anyhow` error wrapping (`Context` / `Other(#[from] + anyhow::Error)`) is dropped — the crate's error surface is + `thiserror`-typed; the forked `SocksError`/`SocksServerError` + stay the wire-level vocabulary. +- The panic paths in the extracted region are translated (AGENTS.md + convention 2 — no panics in library code): `ReplyError::from_u8`'s + upstream `unreachable!()` arm for codes outside the RFC table + maps unassigned codes (`0x09`–`0xFF`, RFC 1928 §6) to + `GeneralFailure` instead — the value comes off the wire + (`Socks5Stream::read_request_reply` feeds the raw reply byte + there), so a misbehaving peer surfaces as a typed error, never a + panic (the extraction review's F-1 correction). + (`client.rs`'s `use_password_auth` `unreachable!()` and + `server.rs`'s `reply_error` `unwrap` were already translated by + their tasks.) +- The client `Config` derives `Default` instead of upstream's manual + impl (upstream's manual impl only spelled the cut + `connect_timeout` field), and `request_header` writes with + `write_all` where upstream wrote with `write` (a partial write of + a request header would corrupt the conversation; byte-identical on + a healthy stream — `clippy::unused_io_amount` is deny-by-default). + +Plus the **post-fork convention fixes** (ADR-013 — the lint-allow +posture dissolved, project conventions applied): the module-level +`#[allow]`s removed and the suppressed smells fixed +(`u8::max_value()` → `u8::MAX`, needless lifetimes elided, +`match_like_matches_macro` applied, `io::Error::new(Other)` → +`io::Error::other`, `wrong_self_convention` fixed on `AuthMethod:: +new`, the `ToSocketAddrs` error string re-worded — it no longer +names the cut `resolve_dns()` — and the `#[allow(dead_code)]` on +`AuthenticationMethod` swept). These are one-time, recorded for +completeness; the fork is first-class code thereafter. + +## Maintenance Posture + +- **The fork is first-class code**: it co-evolves with the wrapper + under the project conventions, unconditionally — no second + instruction set, no adjustment-list apparatus, no verbatim + preservation (ADR-013). +- **Differential testing**: `/workspace/fast-socks5` stays the + behavioral reference for RFC edge cases (reply-code mapping, + domain addressing, fragmentation). The crate's test suite covers + the edge cases the forked code must keep passing; when in doubt, + diff behavior against the reference checkout. The kernel-net + region lives there too — the reference checkout is the only place + the full upstream surface exists. +- **Upstream posture**: fast-socks5 is third-party; nothing waits + on it, nothing is absorbed from it. A future upstream fix that + matters is **hand-ported** against the reference checkout and + guarded by the differential suite (ADR-013's honest cost — + assessed low-risk: a quiet upstream, a well-trodden RFC surface). + The offering-a-PR-upstream courtesy item is retired with the + vendor posture (the net gate dissolved with the kernel-net + region; there is no in-tree need to offer). +- **Provenance is permanent**: the module docs and the bundled + license notice always name the origin (fast-socks5 v1.0.0, the + extraction range, the fork point) — orthogonal to ownership, kept + so future readers can always see where the code came from + (ADR-013). +- **No second codec, ever**: the forked codecs are the only RFC + wire vocabulary in the crate (ADR-011's rationale, carried; + the alknet anti-prior-art record is in phase-0 §Anti-prior-art). + +## Design Decisions + +| ADR | Decision | Summary | +|-----|----------|---------| +| [002](decisions/002-protocol-base.md) | Protocol base | the explicit typestate API only | +| [009](decisions/009-feature-gates.md) | Feature gates | `local`, `composed`; the default crate is wasm-clean with no `net` feature (ADR-011 §Amendment) | +| [011](decisions/011-inline-vendor.md) | The extraction | the `T`-generic subset copied in-tree; two carried changes (BIND, `new_reply`); v1.0.0 noted; the cut table | +| [013](decisions/013-targeted-fork.md) | The targeted fork | the extracted region is owned; `src/rfc1928/`; fork point `5b18d4d`; lint-allow posture dissolved; hand-port replaces re-vendor | + +## Open Questions + +Open questions are tracked in [open-questions.md](open-questions.md). +None currently blocking this document. + +## References + +- OQ-SK-04 (the resolved posture), OQ-SK-08 (BIND + `new_reply`), + OQ-SK-03 (the bind seam) +- POC #4 (`poc-connect-wasm-findings.md` — the measured gate), POC + #5 (`poc-bind-findings.md` — the BIND driving + the `new_reply` + finding), POC #2 (the typestate-driving shape for ASSOCIATE) +- `tasks/implementation/review-vendor-extraction.md` (the fidelity + gate; F-1) +- `/workspace/fast-socks5` (v1.0.0, MIT — the behavioral reference); + `docs/research/iroh-socks5-eval.md` (why not the alternative base) \ No newline at end of file diff --git a/docs/architecture/vendor.md b/docs/architecture/vendor.md deleted file mode 100644 index 308ec04..0000000 --- a/docs/architecture/vendor.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -status: draft -last_updated: 2026-09-22 (extraction review F-1: `ReplyError::from_u8`'s -panic arm mapped to `GeneralFailure`, panic-translation + Config/ -write_all adjustments recorded — see the adjustment list; the -extraction amendment ADR-011 §Amendment: extraction not wholesale -copy, the net gate dissolves, two carried changes; Phase 1 initial -draft 2026-09-20) ---- - -# alksocks — The Inline Vendor (fast-socks5 subset) - -The crate owns its RFC 1928 protocol code in-tree: the **inline -vendor** of fast-socks5's `T`-generic subset ([ADR-011](decisions/ -011-inline-vendor.md); OQ-SK-04's resolution). This document is the -vendor's scope, the carried changes, and the maintenance posture. -The protocol base decision is ADR-002; the feature gating is -ADR-009. - -## The Mechanism: Extraction, Not Wholesale Copy (ADR-011 §Amendment) - -The vendor is an **extraction**: copy *only* the `T`-generic -inventory from fast-socks5 v1.0.0 into `src/vendor/`; the kernel-net -handlers are **not copied** — they stay in `/workspace/fast-socks5` -(the read-only reference checkout). Concretely: POC #4's cfg map -(its five-file inventory of what is kernel-net-gated) is the base of -the extraction recipe — copy what POC #4 left *ungated*, minus the -two recorded exclusions (the `socks4` module and the legacy -`Config`/`Authentication` layer — see the cut table). The extraction -contains zero kernel-net code, so it is wasm-clean by construction; -there is no in-tree `net`-shaped feature gate (that part of POC #4's -change dissolves — ADR-011 §Amendment). - -## What is Extracted - -From fast-socks5 **v1.0.0** (MIT, third-party upstream — -`/workspace/fast-socks5` is the read-only reference checkout; the -vendor point is noted in the extracted code): - -| Piece | Why it rides | Notes | -|-------|--------------|-------| -| The server typestate (`Socks5ServerProtocol`, negotiate/auth/command-read/reply, `accept_no_auth`/`finish_auth`/`accept_password_auth`) | the producer's conversation driver (ADR-002) | `T`-generic | -| The auth traits (`AuthMethod`/`AuthMethodSuccessState`) | the negotiation machinery (the crate uses `accept_no_auth` in-band; the traits stay for fidelity) | `T`-generic | -| `transfer` + `wait_on_tcp` | the typestate's own two-pump seam (the crate's data plane is `pump_bidi`; `transfer` is the interception-point shape) | `T`-generic | -| The codecs: `new_udp_header`/`parse_udp_request`, `TargetAddr`/`to_be_bytes`/`read_address`, `ReplyError`/`SocksError`/`SocksServerError` | the RFC wire vocabulary everywhere (front doors, associate frames, reply#2 reader) | pure; `new_reply` published in-tree (carried change) | -| The client types: `Socks5Stream::use_stream` + the `AsyncRead/AsyncWrite` impls | the consumer half's stream session (ADR-002; POC #1's fit) — the first-party associate session in `consumer.rs` was informed by this shape | `T`-generic | -| The non-net test modules (where extractable as unit tests) | RFC edge-case coverage rides in-tree | adjusted to the crate's test layout | - -## What is NOT Extracted (the cut — POC #4's gated inventory + two recipe additions) - -These stay in `/workspace/fast-socks5` (the differential-testing -reference); the cut table below records the boundary, so a future -re-vendor knows the cut. Rows 1–5 are POC #4's five-file cfg map -(what POC #4 gated is what is not extracted); the socks4 row and the -legacy `Config`/`Authentication` layer are this recipe's two -additions — the socks4 module is out of RFC 1928 scope, and the -legacy auth layer was left ungated by POC #4 (it compiles without -net) but no extraction consumer reaches it. None of it is copied: - -| Upstream file | Region NOT in `src/vendor/` | -|---|---| -| `src/server.rs` | `run_tcp_proxy`, `run_udp_proxy`/`run_udp_proxy_custom`, `udp_bind_random_port`, the legacy `Socks5Server`/`Incoming`/`Socks5Socket` (struct + impls), `DnsResolveHelper` trait + impl, `handle_udp_*`/`transfer_udp`, the net-only test module — plus the legacy `Config` + `Authentication` trait + impls (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`, `authenticate_callback`), which POC #4 left ungated but which only this cut region consumes (rows 1–5 are POC #4's gate map; the `Config`/`Authentication` layer and the socks4 row are this recipe's two additions) | -| `src/client.rs` | `Socks5Datagram` (struct + impl), the `Socks5Stream` convenience impl (`connect`/`connect_with_password`/`connect_raw`) | -| `src/util/stream.rs` | `tcp_connect*` fns (the `read_exact!`/`ready!` macros + `ConnectError` stay) | -| `src/util/target_addr.rs` | the `resolve_dns` method (the codec stays) | -| `src/lib.rs` | the net-heavy test module (consts/errors/UDP-header codec stay) | -| `src/socks4/` | the whole module (upstream's separate default-off `socks4` cargo feature; out of RFC 1928 scope) | - -**Why none of it is needed in-tree** (ADR-011 §Amendment): no crate -consumer routes through the kernel-net region — the producer drives -the typestate directly (the ASSOCIATE path is the POC #2 -typestate-driving shape; `run_udp_proxy_custom` is unusable on -channels regardless), the consumer sessions use `use_stream` plus -the first-party associate session, the doors are pure codecs + -first-party bridges, and the local backends are first-party tokio -code (ADR-008). The "producer-side local backend" is a bridge *onto -the channels-served service* (POC #1's door shape), not a standalone -vanilla server. If a future need surfaces (e.g. a standalone -vanilla-server example), the fix is re-vendor that piece then, as an -additive decision. - -## The Carried Changes (ADR-011, re-read under extraction) - -The extraction's diff vs upstream v1.0.0 is deliberate and bounded — -**two changes touch extracted code**; the other two of the original -four dissolve with the kernel-net region: - -1. **BIND** — absent upstream (every handler refuses - `Socks5Command::TCPBind`). The crate drives the typestate itself - for the BIND shape (ADR-007); the vendor carries the shape - in-tree (`reply_success`'s `sock_addr` as the reply#1 carrier; - the reply#2 writer using the published `new_reply`). -2. **`new_reply` publication** — the reply encoder becomes public - (a `reply(code, addr)`-shaped constructor) so the BIND reply#2 - writer doesn't re-encode the 10/22-byte reply form. - -Dissolved: the `net` feature gate (no net code in-tree) and the -`run_udp_proxy_custom` bind seam (the handler isn't extracted). - -Plus the provenance notes: the vendor point (v1.0.0) and the license -headers (MIT; preserve per-file headers or a bundled notice — -ADR-011's license obligation). - -Plus the **extraction-time adjustments** (not carried changes — -mechanical edits to the copied code, ADR-011 §Amendment): - -- The `socks4` module is out entirely (upstream's separate - default-off `socks4` cargo feature, which POC #4 never compiled; - out of RFC 1928 scope). `SocksError`'s `ReplySocks4Error` variant - (also `#[cfg(feature = "socks4")]` upstream) goes with it. -- The legacy `Config`/`Authentication` auth layer is out - (ungated upstream, but consumed only by the cut legacy/net region - — `Socks5Server`/`Socks5Socket`/the `run_*` handlers; the - typestate's own `accept_no_auth`/`accept_password_auth` are the - crate's in-band auth surface). With it go `async-trait` (its four - upstream uses are all on this layer), the example auth impls - (`SimpleUserPassword`/`DenyAuthentication`/`AcceptAuthentication`), - and `SocksError`'s `AuthenticationRejected`-consuming legacy - plumbing that only they exercise. -- Upstream `log` calls in the extracted region are dropped or - re-pointed at the crate's `tracing`. -- Upstream `anyhow` error wrapping (`Context` / `Other(#[from] - anyhow::Error)`) is dropped — the crate's error surface is - `thiserror`-typed; the vendored `SocksError`/`SocksServerError` - stay the wire-level vocabulary. -- The panic paths in the copied region are translated (AGENTS.md - convention 2 — no panics in library code; the extraction - post-dates the client task's summary, corrected by the extraction - review's F-1): `ReplyError::from_u8`'s upstream `unreachable!()` - arm for codes outside the RFC table maps unassigned codes - (`0x09`–`0xFF`, RFC 1928 §6) to `GeneralFailure` instead — the - value comes off the wire (`Socks5Stream::read_request_reply` - feeds the raw reply byte there), so a misbehaving peer surfaces - as a typed error, never a panic. (`client.rs`'s - `use_password_auth` `unreachable!()` and `server.rs`'s - `reply_error` `unwrap` were already translated by their tasks.) -- The client `Config` derives `Default` instead of upstream's manual - impl (upstream's manual impl only spelled the cut - `connect_timeout` field), and `request_header` writes with - `write_all` where upstream wrote with `write` (a partial write of - a request header would corrupt the conversation; byte-identical on - a healthy stream — `clippy::unused_io_amount` is deny-by-default). - Both are recorded here so a future re-vendor re-applies them. - -## Maintenance Posture - -- **The vendor is first-class code, not a patch stack**: the carried - changes live in-tree and co-evolve with the wrapper; the extracted - subset's RFC fidelity is the crate's own test suite's job. -- **Differential testing**: `/workspace/fast-socks5` stays the - reference for RFC edge cases (reply-code mapping, domain - addressing, fragmentation). The crate's test suite covers the - edge cases the extracted code must keep passing; when in doubt, - diff behavior against the reference checkout. The kernel-net - region lives there too — the reference checkout is the only place - the full upstream surface exists. -- **Upstream posture**: fast-socks5 is third-party; nothing waits on - it. Offering a net-gate PR upstream is optional courtesy (POC #4: - small enough to offer as-is) and unrelated to this crate's build — - the extraction doesn't need it. A future upstream release may be - re-based against deliberately, never absorbed silently (the - extraction cut + carried changes would need re-verification). -- **No second codec, ever**: the vendored codecs are the only RFC - wire vocabulary in the crate (ADR-011's rationale; the alknet - anti-prior-art record is in phase-0 §Anti-prior-art). - -## Design Decisions - -| ADR | Decision | Summary | -|-----|----------|---------| -| [002](decisions/002-protocol-base.md) | Protocol base | the explicit typestate API only | -| [009](decisions/009-feature-gates.md) | Feature gates | `local`, `composed`; the default crate is wasm-clean with no `net` feature (ADR-011 §Amendment) | -| [011](decisions/011-inline-vendor.md) | The inline vendor | the extraction in-tree; two carried changes (BIND, `new_reply`); v1.0.0 noted | - -## Open Questions - -Open questions are tracked in [open-questions.md](open-questions.md). -None currently blocking this document. - -## References - -- OQ-SK-04 (the resolved posture), OQ-SK-08 (BIND + `new_reply`), - OQ-SK-03 (the bind seam) -- POC #4 (`poc-connect-wasm-findings.md` — the measured gate), POC - #5 (`poc-bind-findings.md` — the BIND driving + the `new_reply` - finding), POC #2 (the typestate-driving shape for ASSOCIATE) -- `/workspace/fast-socks5` (v1.0.0, MIT — the reference checkout); - `docs/research/iroh-socks5-eval.md` (why not the alternative base) \ No newline at end of file diff --git a/docs/architecture/wire.md b/docs/architecture/wire.md index 8a9842a..eec7f32 100644 --- a/docs/architecture/wire.md +++ b/docs/architecture/wire.md @@ -17,7 +17,8 @@ pass-through, with exactly one framed stage (ASSOCIATE datagrams). Design decisions live in the ADRs ([001](decisions/001-alpn-and-open-op.md), [003](decisions/003-udp-handle-swap.md), [007](decisions/007-bind-in-base.md), -[011](decisions/011-inline-vendor.md)); this document is the normative +[011](decisions/011-inline-vendor.md), [013](decisions/013-targeted-fork.md)); +this document is the normative WHAT. RFC 1928's own wire rules (method negotiation, command encoding, reply encoding, the RFC UDP datagram header) are specified by the RFC itself — this document covers the channels-specific surface and the @@ -232,7 +233,7 @@ not touch this surface. - ADR-001 (op/params), ADR-003 (the handle swap + frame), ADR-004 (the seams + phase-model refusal paths), ADR-007 (BIND), - ADR-011 (the vendored codecs) + ADR-011 (the forked codecs) - POC findings: `poc-connect-wasm-findings.md` (CONNECT pass-through, refusal mapping), `poc-udp-associate-findings.md` (the stage, readiness contract, framing-once invariant, association lifetime),