docs: the targeted fork — ADR-013, the RFC 1928 core is owned

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.
This commit is contained in:
glm-5.3-flash committed 2026-09-22 12:33:36 +00:00
1 parent 5b18d4d292
commit 001aac1fd5
16 files changed
+538 -305

No files matched your search

+26 -16
View File
@@ -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
+6 -6
View File
@@ -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
+8 -7
View File
@@ -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
@@ -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)
@@ -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),
@@ -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).
@@ -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
@@ -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)
+16 -10
View File
@@ -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
+28 -22
View File
@@ -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
+5 -4
View File
@@ -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
+225
View File
@@ -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<T, states::*>`, 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<T>`/`AuthMethodSuccessState<T>`) | 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<S>::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<A>` + `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<TcpStream>` 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<A>`/`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)
-188
View File
@@ -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<T, states::*>`, 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<T>`/`AuthMethodSuccessState<T>`) | 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<S>::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<A>` + `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<TcpStream>` 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<A>`/`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)
+3 -2
View File
@@ -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),