- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
15 KiB
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; 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; 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:
- 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'ssock_addras the reply#1 carrier; the reply#2 writer using the publishednew_reply). new_replypublication: the reply encoder becomes public (areply(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 generatedStandardAuthenticationenum +allow_no_auth,skip_auth_this_is_not_rfc_compliant, andCheckResult'sOption/Resultimpls (boolstays; re-add with a typed-rejection design if a door ever wants it). None had an in-tree consumer once the producer drovenegotiate_authdirectly (&[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) andtransfer/wait_on_tcp(differential-suite-consumed; their semantics stay pinned there). The upstreamBugvariant 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'sto_reply_errormapping is theGeneralFailurefall-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
socks4module is out entirely (upstream's separate default-offsocks4cargo feature, which POC #4 never compiled; out of RFC 1928 scope).SocksError'sReplySocks4Errorvariant (also#[cfg(feature = "socks4")]upstream) goes with it. - The legacy
Config<A>/Authenticationauth layer is out (ungated upstream, but consumed only by the cut legacy/net region:Socks5Server/Socks5Socket/therun_*handlers; the typestate's ownaccept_no_auth/accept_password_authare the crate's in-band auth surface). With it goasync-trait(its four upstream uses are all on this layer), the example auth impls (SimpleUserPassword/DenyAuthentication/AcceptAuthentication), andSocksError'sAuthenticationRejected-consuming legacy plumbing that only they exercise. - Upstream
logcalls in the extracted region are dropped or re-pointed at the crate'stracing. - Upstream
anyhowerror wrapping (Context/Other(#[from] anyhow::Error)) is dropped; the crate's error surface isthiserror-typed; the forkedSocksError/SocksServerErrorstay 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 upstreamunreachable!()arm for codes outside the RFC table maps unassigned codes (0x09–0xFF, RFC 1928 §6) toGeneralFailureinstead; the value comes off the wire (Socks5Stream::read_request_replyfeeds 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'suse_password_authunreachable!()andserver.rs'sreply_errorunwrapwere already translated by their tasks.) - The client
ConfigderivesDefaultinstead of upstream's manual impl (upstream's manual impl only spelled the cutconnect_timeoutfield), andrequest_headerwrites withwrite_allwhere upstream wrote withwrite(a partial write of a request header would corrupt the conversation; byte-identical on a healthy stream).reply_success/reply_errorwere 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. (Theclippy::unused_io_amountlint, deny-by-default, does flag bare.write()onAsyncWriteExt, but theerr_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-socks5stays 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 | Protocol base | the explicit typestate API only |
| 009 | Feature gates | local, composed; the default crate is wasm-clean with no net feature (ADR-011 §Amendment) |
| 011 | The extraction | the T-generic subset copied in-tree; two carried changes (BIND, new_reply); v1.0.0 noted; the cut table |
| 013 | 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. 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 + thenew_replyfinding), 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)