--- status: draft last_updated: 2026-09-24 (the keep-or-cut sweep + the refusal vocabulary variant: the fork's owned post-fork changes section; review 002 N-1/N-4; the targeted fork 2026-09-22, with ADR-013 superseding 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; `ConnectError` kept-with-note, review 001 C-1: constructed nowhere in-tree, kept as the hand-port map's companion for a future kernel-net port) | | `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 **owned-code post-fork changes** (the fork is first-class code; extend this list, never replace it; convention 17): - **The keep-or-cut sweep** (review 002 N-1): the producer-replaced machinery is cut from the typestate: the `auth_method_enums!` macro (upstream's enum generator) with its generated `StandardAuthentication` enum + `allow_no_auth`, `skip_auth_this_is_not_rfc_compliant`, and `CheckResult`'s `Option`/`Result` impls (`bool` stays; re-add with a typed-rejection design if a door ever wants it). None had an in-tree consumer once the producer drove `negotiate_auth` directly (`&[NoAuthentication]`; per-door auth is ADR-005's, not the typestate's). Kept with note (the hand-port map, ADR-013): `ConnectError` (constructed nowhere in-tree; the dial-error → reply-code mapping a future kernel-net port reuses) and `transfer`/`wait_on_tcp` (differential-suite-consumed; their semantics stay pinned there). The upstream `Bug` variant is likewise kept, its display intact. - **The refusal vocabulary variant** (review 002 N-4): `SocksServerError::Refused(&'static str)`: the honest log/observability vocabulary for the producer's in-stream policy refusals (the command gate + the four seam-refusal sites). Upstream has no such variant; it is an owned addition, never reaching the wire (the RFC reply goes out first; the variant's `to_reply_error` mapping is the `GeneralFailure` fall-through, pinned by the mapping-table test). 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). `reply_success`/`reply_error` were likewise converted at the fork review (001 F-1): replies are small, single-shot frames that must be all-or-nothing; a partial write desynchronizes the conversation silently. (The `clippy::unused_io_amount` lint, deny-by-default, does flag bare `.write()` on `AsyncWriteExt`, but the `err_when`-chaining idiom type-infers the result through a generic blanket impl and suppresses it; the linter is not a substitute for the conversion.) 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`, and the `ToSocketAddrs` error string re-worded (it no longer names the cut `resolve_dns()`); 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)