- 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
12 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| implementation/vendor-client-session | Vendor (the client session, Socks5Stream::use_stream path) | completed |
|
narrow | low | component | implementation |
Description
Extract the client session, the consumer half's stream-session base
(ADR-002; POC #1's fit) into src/vendor/client.rs: the
Socks5Stream<S>::use_stream path plus its AsyncRead/AsyncWrite
impls, generic over the backing stream. The consumer layer (a later
task group) opens a channels BiStream and feeds it here; the
first-party associate session in consumer.rs was informed by this
shape (vendor.md's extraction table).
What is extracted (upstream src/client.rs → src/vendor/client.rs):
Socks5Stream<S>struct + the generic impl block, verbatim:use_stream,request,send_version_and_methods,which_method_accepted,use_password_auth,request_header,read_request_reply,get_socket/get_socket_ref/get_socket_mut.- The
AsyncRead/AsyncWriteimpls forSocks5Stream<S>(upstreamclient.rs:641–681) verbatim; the consumer's session rides these (aSocks5Streamis itself a stream, sopump_bidi-shaped consumers and the front doors can pipe through it). - The client
Configstruct, pruned: upstream carriesconnect_timeout(only consumed by the cutconnect_raw) andskip_auth(consumed byuse_stream). Keepskip_auth; dropconnect_timeoutand its setter (its only consumer is cut). If dropping it leavesConfiga single-field struct, keep it anyway (fidelity: the field is load-bearing inuse_stream's handshake branch) or inline the flag, which is the implementer's choice, but theuse_streambehavior must stay byte-identical.
What is NOT extracted (vendor.md's cut table row 2):
Socks5Datagram(struct + impl, the kernelUdpSocketassociate client; the crate's associate session is first-party inconsumer.rsper ADR-003's handle swap).- The
Socks5Stream<TcpStream>convenience impl (connect/connect_with_password/connect_raw, thetokio::net::TcpStreamdialers; the consumer's transport is a channelsBiStream, not a TCP dial).
Extraction-time adjustments (mechanical):
- Drop
anyhow::Context: upstream wraps nearly every client error in.context(...). The error surface isSocksError(thiserror): replace.context("...")?with plain?where the error variant already carries the meaning, and with the typed variants (SocksError::Io,AuthMethodUnacceptable, ...) where upstream usedanyhow::Error::msg(...).into()(upstreamclient.rs:285, the missing-target-addr case) orSocksError::Other(anyhow!(...))(upstreamclient.rs:528, but that site is inside the cutSocks5Datagram, so it goes with the cut). No behavior change to the wire: the same bytes are read/written; only the error typing changes. - Drop
logcalls (debug!in the extracted region). use_password_auth'sunreachable!()(upstreamclient.rs:207:methods.get(1)beingSome(AuthenticationMethod::None)): convention 2 forbids panics in library code. Replace with a typed error path (SocksError::AuthenticationRejected(...)or anAuthMethodUnacceptable); the state is reachable only by misuse of the public API, but the faithful translation is an error return, not a panic.client_src-style"[::]:0".parse().unwrap()sites are all inside the cutSocks5Datagramregion, so verify none remain.
Acceptance Criteria
src/vendor/client.rsexists with the inventory above;cargo testcompilescargo check --target wasm32-unknown-unknownclean- No
Socks5Datagram, noSocks5Stream<TcpStream>convenience impl (connect/connect_with_password/connect_raw) anywhere undersrc/vendor/ - No
anyhow,log,tokio::net,UdpSocketanywhere undersrc/vendor/ - No
unwrap/expect/unreachable!/panic paths in the extracted client code (convention 2) Socks5Stream::use_streamover an in-memory duplex completes: no-auth handshake, password handshake (both accept and reject paths), CONNECT request + reply read (success and each mapped failure code), domain-target and IP-target request encoding, against a raw-byte mock server (pinned byte sequences written directly, not the vendored server typestate; this keeps this task parallel with the typestate extraction and the client's wire expectations uncorrelated from the server implementation; tokio duplex streams, no sockets)Socks5Stream<S>implementsAsyncRead+AsyncWrite(the impls extracted) and the impls are exercised by at least one test (e.g.tokio::io::copythrough the session)cargo clippy --all-targets -- -D warningsandcargo fmt --checkclean
References
- docs/architecture/vendor.md (the extraction table row 5, the cut table row 2)
- docs/architecture/decisions/002-protocol-base.md (the client side:
Socks5Stream<S>::use_stream) - docs/architecture/decisions/003-udp-handle-swap.md (why
Socks5Datagramis cut, since the associate session is first-party) - docs/research/poc-connect-wasm-findings.md §Wire/API notes
(
Socks5Stream::requesttarget construction; theAuthenticationRejectedunit-variant note) - Upstream:
/workspace/fast-socks5/src/client.rs - POC #4 gate map: docs/research/poc-connect-wasm-findings.md (client.rs row)
Notes
- Verbatim-code lint posture: no module-level clippy allows were
needed for this file (unlike
server.rs'swrong_self_convention): upstream's client region compiles clean under the newer clippy after the mechanical adjustments. The posture note stays in the module doc for the re-vendor diff. - Clippy
unused_io_amountfix (the one non-verbatim line): upstream'srequest_headerwrites withself.socket.write(&packet [..padding])(upstreamclient.rs:331); in-tree it iswrite_all;deny(clippy::unused_io_amount)is on by default and a partial write of a SOCKS5 request header would corrupt the conversation anyway, sowrite_allis the faithful translation (byte-identical on the wire for a healthy stream; strictly better on a partial-write stream). The comment above the write (why the packet is length-limited) is upstream's, preserved. anyhow::Error::msg("target addr should be present").into()(upstreamclient.rs:285) became the typedSocksError::MissingTargetAddrvariant, added toSocksErrorinmod.rs(the wire-vocabulary task's error enum, extended by one variant; the message text is upstream's, kept as the variant's#[error]string). This is the task's "typed variants where upstream usedanyhow::Error::msg(...).into()" adjustment; the variant is wire-inert (error surface only).use_password_auth'sunreachable!()(upstreamclient.rs:207): upstream's match has three arms,Some(Password{..})→ Ok,Some(None)→unreachable!(),None→AuthenticationRejected("missing user pass"). In-tree theSome(None)andNonearms collapse to a singleAuthenticationRejected("Authentication rejected, missing user pass")return (the same message as upstream'sNonearm, an error return instead of a panic, per convention 2). The state is reachable only by misuse (auth = Some(AuthenticationMethod::None)while the server picks password).Configpruned toskip_auth:connect_timeout+ its setter are cut withconnect_raw(their only consumer). With one field left, the struct stayed (fidelity: the field is load-bearing inuse_stream's handshake branch);Defaultwas derived instead of the manual impl (upstream's manualDefaultonly existed to spell the now-cut field).set_skip_authis upstream verbatim.- Dropped
logcalls leave bindings unused: thedebug!drops inread_request_reply/use_password_authleaversv/versionunconsumed → renamed_rsv/_version(same mechanical adjustment as the server-typestate task).which_method_accepted's version binding is still consumed (the mismatch check). use_password_authgains a version check: upstream read the auth-sub-negotiation reply and compared onlyis_success; the destructuredversionfed only a droppeddebug!. In-tree the dropped-log rename would have discarded it entirely; instead it is checked againstSOCKS5_VERSION(→UnsupportedSocksVersion), the same check every other reply read in this file performs, since the alternative (an_versionbinding) silently accepts a v4 reply on the sub-negotiation. Byte-level behavior on a compliant server is unchanged.request's droppeddebug!: thedebug!("Requesting headers ...")consumed&self.target_addronly via formatting; nothing renamed (the field is written on the previous line, read inrequest_header).- Tests are first-party (upstream's client test module rides the
cut net region, testing
connectagainst real listeners): the test module here is written fresh against the crate's test layout: raw-byte mock servers over tokio duplex streams, pinned byte sequences written directly (uncorrelated from the vendored server typestate, per the task). Coverage: no-auth handshake + CONNECT success (greeting[5,1,0], method-choice[5,0], domain request5 1 0 3 len… port, reply → bind addr, and post-reply pass-through both directions), password accept (greeting[5,2,0,2], the 14-byte user/pass packet, status[1,0], IP-target request bytes), password reject (status[1,0xff]→AuthenticationRejected), unacceptable method ([5,0xff]reply written by the client, thenAuthMethodUnacceptable), version mismatch (both greeting directions), missing target addr (typedMissingTargetAddr), UDP ASSOCIATE zero fallback ([5,3,0,1,0,0,0,0,0,0]),skip_auth(greeting bypassed, request being the first bytes), the full mapped failure-code table (0x01–0x08 →SocksError::ReplyErrorwith the code round-tripped), domain255 (→
ExceededMaxDomainLen(256)),tokio::io::copythrough the session (theAsyncRead/AsyncWriteimpls), and the socket accessors (get_socket_ref/get_socket_mut/get_socket). request_headeris exercised directly in two tests (the missing-target and the zero-fallback one); since it is private, the tests live in the same module, so this is in-crate visibility, no surface change.
Summary
Extracted the client session from fast-socks5 v1.0.0 into
src/vendor/client.rs per ADR-011 §Amendment + vendor.md (cut table
row 2): Socks5Stream<S> with the use_stream path (send_version_ and_methods, which_method_accepted, use_password_auth,
request_header, read_request_reply, get_socket/
get_socket_ref/get_socket_mut), the AsyncRead/AsyncWrite
impls, and the pruned Config (skip_auth only). The cut regions
(Socks5Datagram struct + impl, the Socks5Stream<TcpStream>
convenience impl with connect/connect_with_password/
connect_raw, Config::connect_timeout + setter) were not copied
(verified by sweep; only doc-comment provenance mentions remain).
Extraction-time adjustments applied: anyhow dropped (the
client.rs:285 site became the typed SocksError::MissingTargetAddr
variant (one variant added to the vocabulary task's SocksError;
the client.rs:528 Other(anyhow!(...)) site went with the cut
Socks5Datagram), log calls dropped (unused bindings renamed),
use_password_auth's unreachable!() replaced with an error return
(upstream's None-arm message), the "[::]:0".parse().unwrap()
sites verified gone with the cut region, and one clippy-driven
correction (write → write_all in request_header). 14 first-party
tests pin the wire behavior against raw-byte mock servers over tokio
duplex streams (no sockets, no dependency on the vendored server
typestate): both handshakes (accept + reject paths), the 0xff method
refusal, version mismatch, CONNECT success with the domain-target and
IP-target request forms byte-pinned, the full mapped failure-code
table (0x01–0x08), the UDP-ASSOCIATE zero fallback, skip_auth,
domain-length rejection, the AsyncRead/AsyncWrite impls via
tokio::io::copy, and the socket accessors. Verified: cargo test
(37 pass), --all-features, clippy --all-targets -- -D warnings
native + wasm32, cargo check --target wasm32-unknown-unknown,
cargo fmt --check, cargo doc --no-deps, and the negative sweeps.