Files
alksocks/docs/architecture/decisions/011-inline-vendor.md
T
glm-5.3-flash 001aac1fd5 docs: the targeted fork — ADR-013, the RFC 1928 core is owned
ADR-013 supersedes ADR-011's maintenance posture (not its mechanics):
the extracted T-generic subset is fully owned code, the vendor
framing is retired. Module renamed src/vendor/ -> src/rfc1928/
(naming decided in the ADR: 'vendor' names an ownership relation
now gone; 'socks5' would stutter and collide with the future
producer/consumer layers). Fork point = the tree as of the
extraction-review commit (5b18d4d); provenance stays permanent
(module docs + the extended LICENSE notice).

- ADR-013: fork posture, license mechanics (notice extended, not
  replaced; post-fork modifications under the crate license),
  hand-port replaces re-vendor, lint-allow posture dissolved
- ADR-011 marked Superseded (extraction mechanics remain
  authoritative); ADR-002 references re-pointed
- vendor.md -> rfc1928.md rewritten as the owned-core spec (the cut
  table re-read as the hand-port map)
- spec sweep: README index (ADR-013 row, doc table, principle 8),
  overview (module map, deps, ADR table), producer/consumer/wire/
  bast reference vocabulary, open-questions OQ-SK-04 resolution,
  AGENTS.md conventions 4/17 + verification note + context bullet,
  Cargo.toml description

Verification: cargo doc --no-deps clean; no stale vendor.md/src-vendor
references outside the ADR-011 historical text.
2026-09-22 12:33:36 +00:00

8.7 KiB
Raw Blame History

ADR-011: the Inline Vendor — fast-socks5's T-Generic Subset, In-Tree

Status

Superseded by ADR-013 (2026-09-22): the maintenance posture (the vendor framing — re-base/re-vendor discipline, the verbatim/lint-allow apparatus) is retired; the extracted region is a targeted fork, module rfc1928. The extraction mechanics here remain authoritative — the cut table, the two carried changes (BIND, new_reply), the §Amendment (extraction, not wholesale copy; the net gate dissolves) describe what was copied and what was not; only the post-copy maintenance posture changed. Originally Accepted 2026-09-20 (Phase 1; OQ-SK-04's resolution), Amended 2026-09-21 (pre-decomposition).

Context

fast-socks5 is third-party upstream (v1.0.0, MIT; /workspace/fast-socks5 is a read-only reference checkout — we do not own it, and it has not been forked). The wrapper's needs go beyond what it publishes:

  1. The wasm gate — tokio::net + socket2 (via mio) hard-error on wasm32-unknown-unknown; the default crate must compile there (AGENTS.md convention 4). POC #4 measured the gate: ~120 changed lines across 5 files, all #[cfg] insertions; the wasm-clean subset is functionally complete (a full RFC 1928 CONNECT conversation passes --no-default-features).
  2. BIND is absent — Socks5Command::TCPBind is parsed but every handler refuses it; POC #5 proved the wrapper must drive the typestate itself for the BIND shape (ADR-007).
  3. The run_udp_proxy_custom bind seam — the "custom" seam is narrower than it looks: it calls udp_bind_random_port unconditionally and derives the reply port from that kernel socket; the channels path cannot use it (verified 2026-09-13). The channels path drives the typestate directly; making the bind optional / accepting a caller-supplied socket is an in-tree fix.
  4. new_reply is private — the reply encoder is private; the BIND reply#2 writer otherwise hand-rolls the 10-byte reply (POC #5's courtesy item, now a vendor change).

Two facts settled the posture: the ownership premise was wrong (no cheap "file asks / PR" path), and the carried needs (2–4) exist regardless of the wasm question — so the vendor pays for itself without leaning on the adapter story. The change set is exactly the shape POC #4 measured as minimal. (Need 1 later dissolves — §Amendment; needs 3's handler is not extracted with it.)

Decision

  • Extract the T-generic subset into this crate (see §Amendment for the mechanical reading of "extract"): the codecs, the typestate machinery, the client use_stream path — the parts already generic over T: AsyncRead + AsyncWrite + Unpin. The extraction contains zero kernel-net code and is wasm-clean by construction.
  • Carry the four needs in the same vendor: the net gate, BIND (the typestate-driven shape, ADR-007), the run_udp_proxy_custom seam fix, and the new_reply publication. (Post-Amendment: the net gate dissolves — see §Amendment; BIND and new_reply are the two that touch extracted code.)
  • Note the vendor point (v1.0.0) in the vendored code; keep the MIT license obligations (upstream is MIT; preserve the license header per file or a bundled notice).
  • /workspace/fast-socks5 stays the differential-testing reference for RFC edge cases (reply-code mapping, domain addressing, fragmentation) — keep /workspace/fast-socks5 for differential testing; never wait on an upstream merge.
  • Offering the net-gate upstream as a courtesy PR stays optional (POC #4: it is small enough to offer as-is) — never a wait, never a dependency.

Consequences

  • The crate owns its protocol code in-tree: no upstream release cadence, no feature waits, and the vendored code co-evolves with the wrapper (the carried changes are first-class, not patches).
  • The cost is maintenance of the extracted subset (the T-generic region + codecs — a fraction of upstream's ~4k LoC; POC #4's ungated inventory is the shopping list). The diff vs upstream is deliberate and bounded: the extraction cut + the carried changes + the license/provenance notes.
  • Nothing in the crate routes through the kernel-net handlers; if a future need surfaces (e.g. a standalone vanilla-server example), the fix is re-vendor that piece then, as an additive decision.
  • Differential testing against /workspace/fast-socks5 guards the RFC edge cases the extracted code must keep passing.

Amendment (2026-09-21, pre-decomposition — "extract" pinned, the net gate dissolves)

The decision's wording ("vendor the T-generic subset") supported two mechanical readings; this amendment pins the intent so the decomposition task is unambiguous:

  • Extraction, not wholesale-copy-plus-gate. Copy only the T-generic inventory into src/vendor/; the kernel-net handlers (run_tcp_proxy, run_udp_proxy*, udp_bind_random_port, the legacy Socks5Server/Incoming API, Socks5Datagram's kernel socket path, the DNS-resolve helper, the net-only tests), the socks4 module (upstream's separate default-off socks4 cargo feature, which POC #4 never compiled; out of this crate's RFC 1928 scope), and the legacy Config<A>/Authentication auth layer (ungated upstream, but consumed only by the cut legacy/net region) are not copied into the crate; they remain available in /workspace/fast-socks5, the differential-testing reference. The kernel-net region is carried only as the cfg map (POC #4's five-file inventory), recorded in vendor.md so a future re-vendor knows the cut.
  • Why: convention 17's letter ("vendor the T-generic subset"); no crate consumer routes through the kernel-net region (the producer/consumer/doors drive the typestate and the pure codecs; the local backends are first-party tokio code — ADR-004/ADR-008); and the BIND + new_reply needs touch only the extracted region (BIND's driving is wrapper-side code per POC #5 — "zero upstream ask for BIND itself").
  • The net feature gate dissolves. With no kernel-net code in-tree, the extraction is wasm-clean by construction — no #[cfg] gate needed inside the vendored files. Features collapse to two: local (the crate's first-party socket providers and front doors, ADR-008) and composed (the alktunnels providers). POC #4's gate map survives as the extraction recipe (its ungated inventory is exactly what to copy), not as in-tree cfg. tokio stays default-features = false with the wasm-clean subset (AGENTS.md convention 4); tokio/net rides local alone.
  • The carried-changes list re-reads under extraction: (1) the net gate — dissolves (no net code); (2) BIND — stays (the typestate driving + reply#2 writer are in-tree; ADR-007); (3) the run_udp_proxy_custom bind seam — dissolves (the handler isn't extracted; the typestate-driving shape POC #2 validated is the crate's ASSOCIATE path); (4) new_reply publication — stays (it is inside the extracted codec region). Net: two carried changes touch extracted code.
  • Extraction completeness is compiler-checked: a missed transitive dependency is a build error, and the wasm check (cargo check --target wasm32-unknown-unknown, plus --no-default-features native) is the structural guard. The extracted crate must compile clean with no net-shaped feature.
  • The extraction's dependency surface is adjusted, not absorbed: upstream's log calls in the extracted region are dropped or re-pointed at the crate's tracing (the alk* convention); the extracted region's anyhow::Context error wrapping is dropped (error surface is the crate's thiserror types — the vendored SocksError/SocksServerError stay the wire-level vocabulary); async-trait is dropped (its four upstream uses are all on the legacy Authentication layer, which is cut — the typestate's AuthMethod<T> traits are native async-fn-in-trait); the socks4 module and the legacy Config/Authentication layer are out (above). These are extraction-time mechanical adjustments to the copied code, not new carried changes.

The verification commands in AGENTS.md that mention the wasm check failing on the dependency graph are stale for this crate (that caveat predated OQ-SK-04's resolution); the extraction makes the wasm check a hard gate as in alktty/alktunnels.

References

  • OQ-SK-04 (the resolved posture; the reasoning trail), OQ-SK-08 (the BIND + new_reply needs), OQ-SK-03 (the bind seam)
  • POC #4 + POC #1 (poc-connect-wasm-findings.md — the measured gate), POC #5 (poc-bind-findings.md — the BIND typestate driving + the new_reply finding)
  • ADR-002 (the protocol base), ADR-007 (BIND), ADR-009 (the feature gates); /workspace/fast-socks5 (the reference checkout, v1.0.0)