Files
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

261 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)