Files
alksocks/docs/architecture/rfc1928.md
T
glm-5.3-flash e196ed43e3 docs: remove em dashes from repo prose and forbid them going forward
- 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
2026-11-03 19:31:53 +00:00

15 KiB
Raw Permalink Blame History


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:

  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<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). 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 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 + 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)