- 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
261 lines
15 KiB
Markdown
261 lines
15 KiB
Markdown
---
|
||
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<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](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) |