alksocks 0.3.2

SOCKS5 (RFC 1928) proxy protocol over alkcall channels: targeted-forked fast-socks5 typestate core plus producer/consumer sessions. Producer/consumer protocol crate.
Documentation
---
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)