- CHANGELOG: the [0.3.2] compare link added (v0.3.1...v0.3.2); the
[Unreleased] base re-pointed v0.3.0...HEAD -> v0.3.1...HEAD, the
stale-base residue from the 0.3.1 entry landing without the
0.3.2 refs.
Verification: cargo test (145 lib / 375 total), --all-features (171),
clippy -D warnings (default + wasm targets), fmt, doc, fuzz corpus
replay (3/3), publish dry-run (64 files) all green.
Per-version release notes (0.1 through 0.3.2) duplicated the
changelog; that section was also stale-titled. The section is
removed; the changelog stays the release-history home.
- 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
- Additive upstream API only (the forwarded-op establisher variant,
register_tunnel_forwarded_openable_with_establisher + ForwardedGate);
no alkcall bump (0.10.1 rides on), no wire change, and no change on
this crate's paths: our composed accept keeps the base
register_tunnel_forwarded_openable byte-for-byte.
Verification: cargo test (default + composed: 145/148 lib tests, all
suites green), cargo clippy --all-targets -D warnings (default +
composed), cargo fmt --check, cargo check/clippy wasm32-unknown-unknown.
- alkcall 0.9.0 -> 0.10.0 (the gateway ownership-seam release): the
GatewayDispatch ownership-provider threading + the loud unwired-spine
default are not on this crate's paths (no gateway module use; registry
dispatch on the native wire untouched). Zero API/wire change here.
- alktunnels 0.3.0 -> 0.4.0 (its dependency-major ride): no alktunnels
API/wire change; the composed providers see none of it.
- fuzz/shared: the harness's own alkcall pin bumped in lockstep (its
ChannelOpenError construction makes a drifting pin a duplicate-type
compile failure, not silent drift) — the only bump contact.
- Docs: CHANGELOG 0.3.0 entry, README version blurb (alktunnels style),
AGENTS.md pin update, deps tree in overview.md.
- Minor-not-patch for the established reason: semver-incompatible
dependency bumps + this crate's public ChannelOpenError re-exports
(SocksOpenError::Open/open_ref) are semver-visible downstream.
Verification: cargo test (default 145 unit + suites green), --features
local, --all-features (171 unit + all suites), clippy --all-targets
--all-features -D warnings, fmt --check, wasm32-unknown-unknown check +
clippy, fuzz corpus replay 3/3, publish --dry-run clean.
- CHANGELOG: the 0.1.0 entry finalized for publish — the alkcall
0.8.1 / alktunnels 0.2.1 bumps and the fuzzing adoption folded in
(the crate is unpublished, so no separate hardening cut — the
alktunnels 0.2.1 precedent only applies post-publish); the fuzz/
workspace bullet added (three targets, 129 seeds, corpus-replay
gate, campaigns clean)
- README: the Version 0.1 note aligned (alkcall 0.8.1 — the
duplicate-adopt fix is directly relevant to the consumer open/adopt
paths; alktunnels 0.2.1; the fuzz posture noted)
- docs/architecture/overview.md: dep tree 0.8.1 / 0.2.1
Verification: cargo test (376 default / 638 all-features), fuzz/shared
corpus replay (3 pass), clippy --all-targets -D warnings (default,
all-features, wasm32-unknown-unknown), fmt --check, doc, wasm32 check,
and cargo publish --dry-run --allow-dirty all green (64 packaged
files — fuzz/, docs/research|reviews|plans/, tasks/, .opencode/,
AGENTS.md all excluded)
Adopt the alk*/fuzzing posture (alkcall 0.8.1 + alktunnels 0.2.1
precedent) before the v0.1 publish — the cheapest moment this crate
will ever have for it:
- alkcall 0.8.1 + alktunnels 0.2.1: the duplicate-adopt/open fix is
directly relevant to this crate's consumer open/adopt paths
- fuzz/ workspace (the sibling layout): nightly-pinned subtree, stable
fuzz/shared library (corpus replay = the fuzz gate), detached runner,
JSON dict, deterministic seed generator; 129 committed seeds
- three wave-1 targets over already-pub APIs: datagram_codec (the
ASSOCIATE frame codec — the alkcall-fuzz-doc-assigned downstream
target), rfc1928_codec (the forked RFC 1928 core's handshake state
machines, both protocol directions), params_parse (the wire-stable
open-op surface)
- one diagnostic: DatagramReader::retained_bytes() (the decoder-state
bound's assertion target, the alktunnels retained_bytes() posture)
- docs/research/fuzzing.md: the scoped plan + implementation log;
AGENTS.md gains the corpus-replay verification line
- campaigns: 3 × 10-min detached runs exited 0 (datagram ~1.9M execs,
rfc1928 ~2.0M, params ~3.1M; oom/timeout/crash 0/0/0 on all 33 fork
jobs per target; coverage saturated on both codec targets). No crate
bugs; three harness-model defects found + fixed (2 pre-campaign by
corpus replay, 2 campaign-found, crash inputs promoted as seeds 900/
901): the len==0 well-framed decode partition, the fieldless-struct
serde-seq form, the BadAtyp re-frame arm, the short-domain count-walk
gate
- wave 2 (session_pumps stateful target) deferred to post-0.1 — the
byte-level targets carry the attacker-reachable load for the publish
Verification: cargo test (15 suites ok, default + all-features), clippy
(all-targets + wasm + fuzz/shared), fmt, cargo doc, publish --dry-run,
corpus replay 129 seeds green, cargo fuzz build clean.
Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alktunnels
docs/research/fuzzing.md; docs/research/fuzzing.md (this crate's copy)
- OQ-SK2-09 recorded: option 2 (the same-IP lock + reply-port
refresh + the RFC drop rule) landed in 0.1.0 (31fb11c); option 3
(the control-conn-derived expected peer — the complete fix)
deferred on a concrete wildcard-bind deployment with address-level
client mobility (QUIC migration is the likely driver); the
deferred-index row added
- OQ-SK2-06: the trigger ownership checked (2026-09-28) — genuinely
external by verified default (no suite-internal surface names a
sandboxed alksocks consumer; the substrate is complete and
gate-verified, nothing waits on nothing); the Schrödinger-bind
check recorded in the OQ + the tracker task
- overview.md: the OQ index updated (SK2-09 added, SK2-06's
ownership noted); the door bullet in the CHANGELOG carries the
peer-pin posture + the async resolver note
Verification: cargo test + --all-features (15 suites each), clippy
--all-targets --all-features -D warnings clean, fmt clean, wasm check
clean, doc 0 warnings.
- serve_on pins the client's source IP (parsed datagrams only —
protocol garbage can never move the pin) and sends replies to the
latest-seen port from that IP: a NAT port rebind under a stable IP
keeps receiving (the review 003 S-1 residual, now fixed)
- the RFC 1928 MUST implemented: datagrams from a source IP other
than the pinned one are dropped (previously every parsed datagram
was forwarded regardless of source); the spoof primitive shrinks
from any-source to same-IP off-path
- the residual (a full client IP change mid-association) stays the
RFC's expected-peer posture — revisitable per OQ-SK2-09 (the
control-conn-derived expected IP is the complete fix, ADR-shaped)
- two tests pin the new behavior: the NAT port-rebind follow (the
old port stops receiving, the new one takes over) and the
foreign-IP drop (no reply to the spoofer, the association healthy)
Verification: cargo test --features local (15 suites green, door-udp
38 tests), clippy --all-targets --all-features -D warnings clean,
fmt clean.
- README.md: the alktunnels-shape quick start (producer registration,
the three consumer sessions, both doors, the producer-side
listener), the architecture table, the feature matrix, the
verification commands — self-contained, since docs/architecture/
rides the package but docs/reviews|research|plans do not
- CHANGELOG.md: the Keep-a-Changelog 0.1.0 entry (the sibling shape:
Unreleased section + tag links)
- LICENSE-MIT / LICENSE-APACHE: copied from the siblings (crates.io
license-file convention; the crate license is dual from Cargo.toml)
- Cargo.toml: `readme = "README.md"` (the sibling pattern; the
packaged crate showed `readme = false`)
Verification: cargo test + cargo test --all-features (15 suites each),
clippy --all-targets --all-features -D warnings, fmt --check, doc
--no-deps (0 warnings), wasm32-unknown-unknown check (default +
composed), publish --dry-run (64 files, 278.3 KiB).
- Review 003 written (docs/reviews/003-assembly-layer-implementation-
review.md): the batch-2 assembly layer post-landing, the review-002
template — correctness/security/smells/coverage over the five
batch-2 modules + the batch-2 consumer/producer diff surface.
Baseline: 375 default / 620 all-features green, clippy/fmt/doc
clean, both wasm checks clean, publish dry-run completes,
llvm-cov 93.68% lines / 91.01% functions.
- F-1 (major, fixed): the cred-bearing door swallowed every in-band
refusal's mapped reply code (the CONNECT/BIND session-open errors
and the accept-stage Err were bare closes) — a vanilla client got
no reply where ADR-007 pins a reply-code-shaped refusal. Fixed with
a write_refusal_reply helper (the door's boundary codec, RFC
placeholder BND.ADDR) at all three sites, pinned by
cred_door_in_band_refusal_surfaces_the_reply_code.
- F-2 (major, fixed): UdpFrontDoor::serve_on returned Ok(()) on a
fatal relay-socket I/O failure against its own doc contract —
the Io exit now surfaces as Err; the idle/association/session
exits stay clean.
- Findings recorded, not fixed (the release-polish round's residue):
D-1 (associate_with_token 0% covered), D-2 (the composed-egress
failure matrix), D-3 (the io-kind refusal-table arms), S-1 (the
UDP door's learned client address), N-1..N-8 (doc lines, posture
notes, coverage residue).
Verification: cargo test (375) + --all-features (620) green,
clippy --all-targets --all-features -D warnings clean, fmt --check
clean, doc --no-deps clean, both wasm checks clean, publish
dry-run completes.
Review checkpoint closing batch 2 (the assembly layer). The gate
opens: the never-binds posture, seam contracts, door fidelity, the
bind-first graduation, feature-gate hygiene, and POC fidelity all
verify clean; two findings were corrected in the review change.
Findings corrected:
- R-1 (major): the UDP front door's in-band decode created a fresh
DatagramReader per call and kept only the first frame of a batch —
POC #2's first-cut bug (reader state must persist across calls;
batched datagrams were silently dropped, mid-frame EOF was silent).
Fix: an owning InbandBridge (persistent reader + pending queue +
fail-loud mid-frame EOF, the AssociateSession shape) threaded
through the channel->socket pump; pinned by three bridge unit
tests (which fail against the per-call reader) and an end-to-end
doubled-echo door-udp test.
- R-2 (convention 2): seven non-test expects -> typed errors —
DoorBuildError (both door builders' .build() is now Result),
ProducerDoorBuildError/ProducerDoorError (the bind decision is
serve's typed error), the composed table invariant as an egress
error, register_forwarded_serving -> Result (the alkcall
diagnostic surfaced), TokenClientError (wrap on a spent client).
New error types re-exported; test call sites updated.
- R-3/R-4 (doc residues): producer.md's local-backend section
described a channel pipe — the landed ProducerDoor serves
serve_socks5_over on the accepted socket directly;
associate_with_token's doc (the TCP door refuses ASSOCIATE, never
calls it); SessionOwner's doc (the accept uses SyncHalves/
OwnedWrite).
Verification: cargo test (default) green; cargo test --all-features
green; cargo clippy --all-targets [--all-features] -- -D warnings
clean; cargo fmt --check clean; both wasm checks clean (default +
composed); local-on-wasm fails at mio as designed; cargo doc
--no-deps clean.
Task: tasks/implementation/review-assembly-layer.md (status:
completed; the batch-2 gate opens release polish).
- tests/assembly-integration.rs (7 tests, local + composed): the full
-D story (vanilla cred-bearing client -> TCP door + UDP door ->
producer w/ local providers -> real echo targets; CONNECT + BIND +
ASSOCIATE live, one-command-per-connection, teardown + fresh
sessions), the composed story (direct_egress + bindfirst_accept +
real TCP dial; all three commands; the lazy-table dial-count
witness), mixed assembly (channels-native + door clients on one
registration; composed seams beside native sessions), the assembly
teardown matrix (UDP-door idle bound; channel reaped; fresh
association after), the producer door driving all three commands,
and the -D domain-target pin (resolution at the producer's seam,
ADR-012).
- Fix the drift the suite surfaced: ComposedEgress::next_reply ended
fresh (never-opened-table) associations at birth — the ended-relay
Err now fires only once a tunnel existed and every relay ended; the
fresh table parks (the readiness contract). Regression-pinned in
tests/composed-egress.rs (fresh_association_parks_before_any_
tunnel_is_opened).
- Harness: the composed-accept machinery single-sourced into
tests/harness.rs (the composed suites' shapes + the five-gate
composed identity); composed-accept.rs consumes the shared shapes.
- Docs sync: README Current State (the assembly layer landed; both
suites named), lib.rs crate docs (the assembly-layer paragraph),
consumer.md (the TokenClient door-only contract + the
one-command-per-connection posture reconciled).
- Task assembly-integration-suite: completed; Notes + Summary filled
(the local-on-wasm failure observed: mio's unsupported-wasm error).
Verification: cargo test (375), cargo test --all-features (615),
clippy --all-targets -D warnings (default + all-features), fmt --check,
doc --no-deps (0 warnings), both wasm checks (default + composed),
local-on-wasm fails as designed.
Lands the composed accept (ADR-007 variant 2 / ADR-008): the BIND
accept whose listener lives on an alktunnels 0.2.0 bind-first listen
resource, at the graduation's vanilla fidelity.
- `composed::bindfirst_accept` — the BindAcceptFactory: opens the
listen channel (reply#1 advertises the open-op reply's real
`bound`, the 0.8.0 reply projection — no expected-addr stand-in),
inserts the minted `listen` token into the correlation registry,
and returns the accept future parked on the token's oneshot.
- `composed::register_forwarded_serving` — the forwarded-op serving
half on the SOCKS5 producer's connection: the shared handler
correlates by token and owns the middle pump (either-EOF
completion; a three-party per-direction shutdown cascade would
deadlock).
- The Sync-bound wrinkle: `SyncHalves`/`OwnedWrite` (mutex
Sync-proof, no unsafe) over the forwarded halves; the owner
(`ForwardedOwner`, the TunnelListener) rides inside the write
payload — dropping the handle drops the session, halves, and
listener together (teardown structural, compiler-enforced).
- `tests/composed-accept.rs`: the full story (real bound, real peer,
raw pass-through round trip), client-gone teardown drop-witness,
distinct tokens for concurrent BINDs, per-accept isolation of a
FORBIDDEN forwarded open, late-forwarded correlation miss.
- The producer-side adopted forwarded entry rides the per-adopter
contract (transport-EOF reap), as upstream's bind-first suites
concede; the no-leak probe reflects that shape.
Verification: cargo test (default + --all-features), wasm
check/clippy (hard gate), clippy --all-targets --all-features -D
warnings, fmt --check — all clean.
The `composed` feature's ASSOCIATE egress (ADR-008's composed
variant, ADR-009's gate): `composed::direct_egress` — each
destination dials through an alktunnels 0.2.0 direct-op channel
(`TunnelSession::open_direct`, `Substrate::Udp`), per-target ACL
for free, the POC #2 `ComposedEgress` shape ported to the landed
seam types.
- `Cargo.toml` — `composed = ["dep:alktunnels"]` (alktunnels pinned
"0.2.0"; rides `composed` alone, `cargo tree --edges normal`
spot-checked); dev-dep for the composed suite.
- `src/composed/mod.rs` — the lazy destination→tunnel table
(LRU-capped, `DEFAULT_MAX_TUNNELS` = 256), per-tunnel relay tasks
draining a shared inbound queue, `SessionOwner` (the owner wrapper
shared with the composed accept — the batch-1 review's
Sync-bound wrinkle), the readiness contract (real async park;
ended relays → association-end `Err`, never a hot `Ok(None)`),
framing-once, domain pass-through unresolved (ADR-012), the
backpressure bound documented on the constructor.
- Two deliberate deviations from the POC shape, documented in the
task's Notes: the closed-queue condition is a live-relay count
(the egress holds its own `Sender`, so the POC's queue-close arm
was unreachable through this surface); the tunnel codec is
alktunnels' (`[len]` + payload), not the RFC-framed reader.
- `tests/composed-egress.rs` — round-trips per destination → one
tunnel each, table reuse, LRU eviction/re-dial, the readiness
park, the closed-relay-set `Err`, framing-once through the wire
layer, domain pass-through — over a `wire_tunnel_direct` harness
topology (the consumer-side shape with the direct op on the
producer's serving registry).
Verification: `cargo test` + `--all-features` pass (13 suites);
`cargo check --features composed --target wasm32-unknown-unknown`
clean (the hard gate); clippy `--all-targets --all-features
-- -D warnings` clean; `cargo fmt --check` clean; `cargo doc
--no-deps` clean.
- ProducerDoor (feature local, src/local/producer_door.rs): a real
TcpListener accept loop handing each accepted connection to the
landed Socks5Producer::serve_socks5_over — the NoAuth in-stream RFC
conversation with the local seam providers dialing/egressing/accepting;
no new protocol-layer code.
- The accept path owns ADR-014's two knobs: the idle-first-byte peek
(non-consuming) and the max-concurrent-sessions cap (try_acquire —
an accept beyond the bound is refused door-side).
- ProducerDoorGuard: the listener's lifetime is the caller's — graceful
close() awaits the loop's end; Drop aborts best-effort (the
alktunnels TunnelListener teardown posture). In-flight sessions are
never force-closed.
- Per-connection failures are logged + isolated (the loop continues).
- The BIND gate on this path is the builder's bind_policy (no open-op
identity here; the pump handler's identity-derived gate does not
apply) — pinned by the 0x02-refusal test.
- tests/local-backend-producer.rs (9): vanilla CONNECT round-trip on
real sockets, faithful-RFC BIND (real reply#1/reply#2 addresses),
UDP ASSOCIATE through the real egress, domain resolution at the
seam, idle timeout, session cap, failure isolation, bind_policy(false).
Verification: cargo test + --all-features green; clippy clean (default,
all-features, wasm32 targets); cargo fmt --check clean; wasm checks
green (backend is local-gated).
- src/door.rs (feature local): UdpFrontDoor — a sibling of the TCP
door (one relay UDP socket = one association; no shared accept
loop). Inbound datagrams decode via parse_udp_request at the door's
codec boundary and ride in-band as {addr, data} frames; replies
encode via new_udp_header back to the client's learned source
address. FRAG != 0 refused (dropped; association healthy). The
association's lifetime is the channel's — the relay socket closes
with the pump and the teardown reaps the adopted channel. The
client idle bound (ADR-014) is constructor config.
- Pump shape: the association's stream is split (one pump owns the
in-band write half, the other the read half) — a shared-session
mutex deadlocks (the channel read parks holding it while the socket
side's send needs it); the pumps join via select!.
- tests/door-udp.rs: the POC #2 test-8 shape end to end (real UDP
echo target + local::udp_egress + vanilla RFC-header'd datagram),
the empty-datagram invariant (F-2), FRAG refusal + health, the idle
bound + fresh-association health.
Verification: cargo test green (default + all-features + local),
clippy -D warnings clean (default, local, wasm32 target), fmt clean,
cargo doc clean.
The consumer's TCP front door (feature `local`, ADR-005/ADR-014): the
ssh `-D` local exposure vanilla clients point at.
- `src/door.rs`: `FrontDoor::builder().credentials(...).bind(...)`
→ `serve`/`serve_on`. The NoAuth door is the literal raw pipe
(accept → channel BiStream via copy_bidirectional, zero protocol
translation). The cred-bearing door terminates RFC 1929 locally
(the forked accept_password_auth shape) and re-originates in-band
via the token-carrying consumer sessions — the vanilla replies are
re-encoded at the door (the RFC codec boundary, ADR-003), the
password never crosses the channel as a SOCKS password. The BIND
door rides BindSession with reply#1 fidelity (the advertised
address passes through unchanged). UDP ASSOCIATE at the TCP door
refuses RFC-faithfully (0x07 — the UDP door's task). The ADR-014
accept-path defense: the idle-first-byte window (peek-based,
door-side close) + the max-sessions cap (semaphore) as constructor
config; per-accept failure isolation (the loop continues).
- The identity mechanism (decided + recorded in the task Notes): the
per-credential identity rides the open-op `auth_token` payload
(CF-005 precedence (1)) — the token is the mapped identity's id;
the producer resolves it through ServingConfig.identity_provider.
No per-credential ChannelClient.
- `src/consumer.rs`: `TokenClient` (the token-carrying open surface
over the taken CallConnection + shared manager) and the
`*_with_token` twins of the three session constructors through the
open_with_token_and_negotiate prologue (teardown-at-birth
preserved); `Socks5ChannelSession::reply_addr` (the CONNECT
reply's BND.ADDR for the door's re-encoding). Existing surfaces
untouched.
- `tests/door-tcp.rs`: 10 integration tests — the NoAuth raw-pipe
round-trip (the POC #1 curl shape), good/bad credentials (the
establisher witness sees the mapped identity, never the password;
a rejection opens no channel), the never-pipes-raw-post-auth pin,
the BIND door's vanilla conversation, the idle timeout, the
session cap, failure isolation, and the native-session/BIND-token
regression guards. Harness: wire_consumer_side_with_provider (the
producer's identity provider configurable).
Verification: cargo test (375) + --all-features (423) green; clippy
-D warnings clean (default + all-features); fmt --check clean; wasm
check + wasm clippy green; default graph stays net-free.
The `local` feature's first-party providers (ADR-008's local variants,
ADR-009's gate): the three seams' faithful-RFC assembly-layer
constructors over the landed seam types; the protocol layer never
sees them.
- Cargo.toml: `local = ["tokio/net"]` (no socket2 — the POCs ran
tokio-only); the default graph stays net/mio/socket2-free
(cargo tree spot-checked both ways)
- src/local/: `tcp_dial()` (real TcpStream per CONNECT, domain
resolution at the provider — the ssh -D semantic; the resolved
address set iterated, faithful SocksDialError mapping,
NotAllowed never synthesized), `udp_egress()` (one real UdpSocket
per association bound 127.0.0.1:0 — the explicit bind decision;
the POC #2 LocalEgress readiness shape: park on readable(),
try_recv_from, WouldBlock -> spurious Ok(None); inner bytes
to_be_bytes() + data, framing-once; resolve at the egress),
`bind_accept(bind_host)` (real TcpListener per BIND session,
ADR-007 variant 1: requested port honored when non-zero, local_addr
as reply#1's BND.ADDR, the accept future owning the listener
end-to-end, peer byte-exact; two-stage failure mapping; the bind
host is the caller's explicit choice)
- lib.rs: cfg-gated module + re-exports; crate doc updated
- tests: 12 unit (real-socket round-trips, the readiness contract's
park-then-datagram, the drop-closes-the-listener witness, the
refusal mappings) + 6 integration (tests/local-providers.rs,
feature-gated: the three commands end to end through the producer's
real dispatch path with real targets, connect-refused -> 0x05
through the producer's reply, the readiness contract re-pinned
through the datagram stage, client-gone -> the advertised port
frees)
- task status -> completed (acceptance criteria checked; Notes +
Summary filled)
Verification: cargo test (157 green) + --all-features green,
clippy -D warnings clean (default + all-features), fmt --check clean,
both wasm checks clean, the dependency-graph spot-checks green.
The closing checkpoint (tasks/implementation/review-fix-round-002.md):
each review-002 finding verified closed against the landed tree, the
full verification sweep green, conventions re-swept, and the gate
condition (ADR-014 accepted, no major finding) holds.
Per-finding verification (all nine checklist points):
- C-1: the take-guard verified — a second await_inbound returns the
spent shape Ok(None), the re-read structurally impossible (the plane
owns the stream); pinned by the double-read + reply1-twice tests.
D-1's one-reader decision recorded (option (a), BindReply1 cut).
- C-3 closed: the builder's explicit bind_policy is the substrate-free
gate; the presence reading is only the backward-compat default; the
pump handler's identity-derived gate byte-identical.
- D-2 closed: the full builder chain through .register(...) serves all
three commands end to end (RegisterMode::ProducerBuilder + the
integration test, identity witness asserted).
- N-1/N-4 coherent: the cut list gone (zero references), keep-notes
current, the Refused vocabulary honest at all five sites + the
harness's minimal producer, wire untouched, provenance extended.
- D-3 resolved (declare side): the channel:open_failed ErrorDefinition
declared, schema pinned drift-free against the typed parse table.
- N-8 re-run: 96.30% regions / 97.28% lines; every named line covered;
the residual attributed (accept_bi log arm, fork accepted-uncovered,
test panic-guards) — no new load-bearing gaps.
- N-5/N-6 verified: the shared module compiles into both crates via
the crate-root mount + extern-crate-self alias; open_and_negotiate
single-sources the teardown-at-birth prologue.
- C-2 decided: ADR-014 accepted (option 1 — teardown-governed phases),
the spec lines carry the posture.
- Cross-cutting sweep green: test 375 green (+ all-features, local,
composed), clippy -D warnings clean, fmt clean, doc clean, both wasm
checks clean, publish dry-run completes; no unwrap/expect/panic in
non-test code (mechanically attributed across src/); no local/
composed code exists (the batch boundary holds).
One gate-round finding, F-1 (minor), fixed in this change: the
review-numbering collision — "review 002 R-1/R-2/R-4" cited the
batch-1 gate review's findings (tasks/implementation/
review-protocol-core.md) under a name that now means the general
review; reworded to "the gate review's R-N" at all six sites
(producer.rs, consumer.rs, egress.rs, the review doc). Review 002
marked resolved; the next general review doc is 003-*.
Verification: cargo test (375 green), --all-features + --features
local/composed green, clippy --all-targets -D warnings clean, fmt
--check clean, doc clean, both wasm checks clean, publish dry-run
completes, llvm-cov 96.30% regions / 97.28% lines attributed.
- new tests/common/mod.rs — the single-sourced shapes, compiled into
the lib test crate (#[cfg(test)] #[path] mod in src/lib.rs +
extern crate self as alksocks) and the integration suite
(tests/harness.rs declares it and re-exports under the consumed
names; the per-test use-harness surface is unchanged)
- single-sourced: the echo dial (plain + witnessing), the refusing
dial, the queue egress + echo factory (the harness ChanEgress /
producer QueueEgress mutex-wrapper divergence collapsed), the
refusing egress, the raw RFC 1928 client trio (generic over the
stream; harness RawRfcClient delegates), the datagram frame
codec helpers (three copies to one), the parked accept factory
- deliberately left test-local: HotNoneEgress / PureHotNoneEgress
(R-1's two shapes), Witness / FailingSendWitness (drop flags),
core-integration's HotNoneEgress (concurrency-load variant),
the typed accept-refusal factories
- zero behavior change: same test population (145 lib / 38
associate / 41 bind / 40 connect / 41 integration / 30 harness /
37 differential / 3 doc-tests), no tests folded, net -307 lines
Verification: cargo test + --all-features green (same counts),
clippy -D warnings clean (native + wasm), fmt clean, doc clean,
both wasm checks clean, publish dry-run completes.
Eighteen new tests, zero production-code changes; every line the
review attributed in §N-8 is now covered (verified by a
`cargo llvm-cov --show-missing-lines` before/after diff).
- producer: the datagram stage's failure exits pinned as direct unit
tests on `run_datagram_stage` (the serve layer discards StageExit,
so the unit shape is the only place the exact matrix is assertable)
— `ReplyFrameRejected` (a joint-bound-violating reply), `EgressEnded`
via `next_reply`'s Err, `Io` via a failing write and a failing read;
the `send`-failure arm got the serve-layer drop-witness treatment
(full ASSOCIATE conversation, EOF after the refusal, the drop flag
flips — teardown-at-birth on the failure path)
- producer: the quiet wait's `Err(_)` arm (a read *error* ends the
wait `Ok(())`, the accept future drops with its owned state — the
client-gone contract's error-shaped twin; driven directly on
`bind_quiet_wait_and_serve`) and the BIND reply#2 Domain-peer
fallback (`TargetAddr::Domain(_, port)` → `0.0.0.0:port`, the data
plane live end to end)
- producer: the establisher's `None`-witness arm (the registration's
optional witness seam) added to the existing unit test
- consumer: the two front-door-bridge `split`s tested on live
`Socks5Stream<BiStream>` streams via `build_skip_auth_bi_stream`
(two duplexes joined the `open_and_negotiate` shape) — raw framed
bytes through both halves; the BIND split leaves the take-guard
untouched (`await_inbound` still reads the reply#2 phase)
- consumer: the single-arm residue — spent `send_datagram`'s `Io` arm
(+ the spent-`recv` `Ok(None)` shape asserted alongside), the
mid-batch later-frame `Err` dropping the pending `Ok`s (the
fail-loud-drops-pending semantics), ASSOCIATE `into_inner`'s `Ok`
pairing, `BindDataPlane`'s `poll_flush`/`poll_shutdown`
passthroughs (the BIND door's half-close), `read_reply2`'s
version pin (a foreign producer's non-5 reply#2), the
ASSOCIATE/BIND session `Debug` bodies, the spent-`close` no-op
- egress/error: the three seam-handle `Debug` renders
(TargetHandle / BindTargetHandle / BindAccept) and
`SocksEstablishmentReason`'s `Display` (each variant renders as
its canonical wire code)
Coverage: 96.31% regions / 97.22% lines; no new accepted-uncovered
entries (the residual uncovered lines are the pre-existing
out-of-scope set + producer.rs 188-189's untestable `accept_bi`
substrate-failure log arm). One test-only select-shape discovery
recorded in the task Notes: `tokio::select!`'s random branch order
can drop a not-yet-polled accept future before its owned state
exists — the quiet-wait test parks once before erroring so both
branches are guaranteed started.
Verification: `cargo test` + `--all-features` green (375 tests),
clippy `-D warnings` clean (native + wasm), fmt clean, `cargo doc`
clean, both wasm checks clean.
- ADR-014 accepted (review 002 C-2): the pre-command handshake, the
BIND quiet wait, and the association lifetime are all
teardown-governed; the protocol layer never times out (option 1)
- the BIND relationship recorded as same regime (ADR-007 decided one
phase's ends by designing BIND; ADR-014 records the inherited
resolution — one principle, not two)
- the doors' accept-path obligations recorded pre-batch-2 (accept-stage
idle bound, per-credential/per-peer caps, accept-queue bounds);
revisitable additively (an optional producer-side idle seam would
be config, not wire)
- spec lines carry the decided posture: wire.md (association-lifetime
bullet, no-in-band-control bullet, preamble, frontmatter);
producer.md (the open handler, the local-backend section, decision
table, frontmatter); overview.md ADR index
- one upstream-fidelity correction: upstream's dial timeout maps to
0x06 (TTL expired), not 0x01 as the review sketch suggested
- open-questions.md unchanged (C-2 was never tracked there)
Verification: cargo test green (357 passed), clippy -D warnings clean,
fmt clean, cargo doc clean, wasm check + clippy (wasm32) clean
- C-3: Socks5ProducerBuilder.bind_policy(bool) — the substrate-free
entry's explicit BIND gate, defaulting to the accept seam's
presence; seam presence is never the sole gate
- D-2: RegisterMode::ProducerBuilder + an integration test wiring the
full builder through .register(...) serving all three commands
- S-1: run_datagram_stage's doc names the hot-Ok(None) residual
- N-2: wire.md §UDP ASSOCIATE carries the inline-send backpressure line
Verified: cargo test (352 green), clippy/fmt/doc clean, wasm checks clean.
- C-1: BindSession::await_inbound is take-guarded — the first read
takes the session's stream; the returned BindDataPlane owns it (the
TakenInner pairing posture; the session stays the teardown owner),
so a second call returns the family's spent shape Ok(None) and can
never re-read the live data plane. A failed read restores the inner
(the phase is only consumed on success; the producer ends the
session on every post-reply#1 failure exit, so a re-call EOFs —
never a live data-plane re-read either way). Pinned by the
double-await_inbound integration test (the spent shape + the
data-plane bytes asserted untouched) and the spent-BIND unit test.
- D-1: one normative reply#2 reader (option a) — the session's
await_inbound; BindReply1::await_inbound is cut and reply1() is
documented as a view (bind_addr + raw_mut), matching its actual
usage; the reply1()-twice re-entry path is closed (pinned by the
view test).
- N-6: the three session constructors' prologues collapsed into one
private open_and_negotiate helper (teardown-at-birth in one place);
zero behavior change.
- S-2: the EOF-propagation doc line on all three reap sites (the
dropped teardown_channel handle is deliberate — the graceful shape;
clear_all aborts instead).
- BindDataPlane is un-lifetimed (owns the stream); into_inner returns
the owned Socks5Stream<BiStream>; docs synced (module + session
docs, consumer.md §The BIND client).
Verification: cargo test 352 green (all-features too), clippy
-D warnings clean, fmt clean, cargo doc clean, both wasm checks clean.
The general review (docs/reviews/002-protocol-core-implementation-review.md)
decomposed into the fix-round group, grouped so no session carries
both a code-fix vertical and the test sweep:
- fix-bind-reply2-reentry: C-1 (the BIND reply#2 re-entry guard) +
D-1 (the reader decision) + N-6 (the constructor prologue
helper) + S-2 (the reap doc line) — one consumer session
- fix-producer-gate-policy: C-3 (the explicit BIND gate input) +
D-2 (the register/builder coverage) + S-1/N-2 (the stage doc
lines) — one producer session
- fix-rfc1928-posture: N-1 (the keep-or-cut pass) + N-4 (the honest
refusal vocabulary) — one fork posture session
- fix-hygiene-residue: D-3 (the ErrorDefinition declaration) +
N-3 (the unreachable let-else) + N-9 (the is_mid_frame note)
- fix-coverage-residue: N-8's attributed line list — tests only,
after the code fixes (final shapes)
- fix-test-helper-consolidation: N-5 — after the coverage round
- architecture/adr-014-idle-timeout-posture: C-2 — the ADR-sized
decision, independent, before the doors exist
- review-fix-round-002: the closing checkpoint; the batch-2 gate
The review doc's disposition table now names the landed tasks.
N-7 is recorded-only (the review's own recommendation).
General review of the landed batch-1 protocol core (docs/reviews/
002-protocol-core-implementation-review.md), complementing the
narrow fidelity review: correctness hazards, security posture,
dead surface, and cargo-llvm-cov attribution, cross-checked against
alkcall 0.8.0 and the sibling crates.
Two majors:
- C-1: BindSession::await_inbound (and BindReply1::await_inbound)
is re-entrant with no spent-marker — a second call re-reads the
live data plane as reply#2, silently corrupting the pipe. Fix
sketched: inner.take() (the session's own None-arm posture) +
tests.
- C-2: no timeout surface on the pre-command RFC handshake or the
association (bounded memory, unbounded time; the alkcall
establishment bound covers only the open op). A decision to make
before batch 2's doors route vanilla clients through the same
phases; sketched as an ADR-level choice, not a core defect.
Minors: C-3 (the substrate-free can_bind gate conflates
factory-wired with allowed), D-1 (BindReply1::await_inbound is
spec'd, 0% covered, never called — the two-reader ambiguity), D-2
(Socks5Producer::register + the builder's egress/accept setters
unexercised — the spec'd assembly shape has no test), D-3
(error_schemas declared vec![] while params.rs's doc claims the
ADR-049/ADR-016 contract; alktty declares it), S-1 (the stage's
Ok(None) defense is livelock-safe but rate-unbounded — posture
note), N-1 (review 001's keep-or-cut triggers fired:
StandardAuthentication/skip_auth_this_is_not_rfc_compliant/
CheckResult impls are cut candidates; ConnectError/transfer/
wait_on_tcp stay as hand-port-map companions; Bug is now consumed).
Coverage (94.52% regions / 95.55% lines): the load-bearing gaps
attributed in N-8 — the two untested split surfaces, ASSOCIATE
into_inner's Ok arm, the datagram stage's three failure exits
(ReplyFrameRejected/EgressEnded/Io), read_reply2's version arm,
plus single-arm cosmetics.
Verified: cargo test 348 green; --all-features green; clippy
--all-targets -D warnings clean; fmt --check clean; cargo doc
--no-deps clean; both wasm checks clean; cargo publish --dry-run
completes; cargo llvm-cov 94.52/94.67/95.55 (regions/functions/
lines). No code changed in this review — findings documented for
the fix decomposition.
The batch-1 gate review (tasks/implementation/review-protocol-core.md):
all eight criteria examined (seam contracts, wire fidelity, access
control, teardown/lifecycle, conventions, wasm, POC fidelity, docs
sync); the gate to batch 2 opens. Findings R-1/R-2 were corrected in
the review change; R-3..R-5 are doc residues fixed in the same change;
R-6 is accepted with rationale.
- R-1 (major): the datagram stage's Ok(None) re-park is unsafe against
a zero-yield egress — a next_reply that is a pure instantly-ready
Ok(None) with no await wins every select! poll and livelocks the
stage on a current-thread runtime (empirically reproduced; the
landed unit test's mutex masked the hazard at its lock's yield
boundary; multi-thread survives). Fix: the Ok(None) handler yields
before re-parking; a pinning unit test drives the strictest
zero-yield violator.
- R-2: the two into_inner expect()s (consumer sessions) become typed
Result<_, SpentSessionError> — convention 2's no-panic rule; spent-
session unit tests pin the shape; test call sites updated.
- R-3/R-4/R-5: doc residues — stale task-time phrasings in
egress.rs/producer.rs, producer.md's register() sketch reconciled
to the landed signature, the establishment-bound test doc reworded
to what the substrate-free probe actually pins.
- R-6 (accepted): the wasm gate rides a transitive getrandom/wasm_js
feature unification (alkcall -> jsonschema/referencing -> ahash) —
verified compiling clean today; noted for OQ-SK2-06.
Verified: cargo test 348 green; --all-features green; clippy
--all-targets -D warnings clean; fmt --check clean; both wasm checks
clean (from a clean wasm target dir); cargo doc --no-deps clean.
The bind-only-identity reconciliation from the integration session
was independently verified as correct (ADR-006 as landed; the suite
pins it). Batch boundary holds: no local/composed code exists.
The batch-1 closing task: nine cross-cutting integration tests
(tests/core-integration.rs) pinning the whole protocol core end to
end over the channels harness, plus the architecture-sync docs pass.
Tests:
- One registration, all three commands: CONNECT + ASSOCIATE + BIND
interleaved live on the one channels/socks5/sub op; a teardown
ends only its own session (mux/demux isolation) — the POC #2/#5
regression guard, complete.
- Concurrency: nine concurrent mixed-command sessions (raw topology)
and six through the typed consumer sessions (consumer-side
topology) — the mux/demux pinned in both channel-id allocation
directions; fresh round-trips after the burst; no channel leaks.
- Teardown matrix (wire.md §Sentinels and lifecycle): CONNECT
half-close, ASSOCIATE EOF, BIND client-gone (a parked accept so
the quiet-wait exit is deterministic) — each followed by fresh
sessions of every command (no connection poisoning).
- Identity/scope matrix (ADR-006 end to end): open-only (CONNECT +
ASSOCIATE serve, BIND refused 0x02 at the command gate), bind-only
fail-closed at the open gate for all commands (the spec drift the
task's phrasing carried, reconciled — ADR-006's open gate requires
socks5:open unconditionally), both-scopes (everything, granted
explicitly); the CF-006 witness asserted per row.
- The readiness contract re-pinned under concurrent load: a
hot-Ok(None) egress across concurrent associations does not starve
the datagram stage's channel branch.
Docs sync: README Current State ("the protocol core is implemented"
+ the landed inventory; the decomposition residue gone); consumer.md
§Wiring reconciled to the landed session signatures; ADR-006
§Consequences states the grant asymmetry explicitly; lib.rs crate
docs describe the landed surface.
Verification: cargo test 302 green; --all-features green; clippy
--all-targets -D warnings clean; fmt --check clean; both wasm checks
clean; cargo doc --no-deps clean. Batch boundary holds: no
local/composed code, the default crate is protocol-only.
The UDP ASSOCIATE consumer session (POC #2's shape, consumer.md §The
associate session, ADR-003):
- `AssociateSession::associate` — open → no-auth → ASSOCIATE → reply;
the sentinel BND.ADDR is received (`relay_addr()`, RFC fidelity)
and never used for addressing (the handle is the channel). The
ASSOCIATE DST.ADDR is the vanilla-mirror unspecified address.
- `send_datagram` — frames exactly once via the crate's wire
`frame()` (the framing-once mirror; the u16 joint bound enforced
at frame time). Takes `impl ToTargetAddr` (ADR-012 posture); a
`ToTargetAddr for &TargetAddr` impl was added to the forked codec
so a `TargetAddr` value survives across sends.
- `recv_datagram` — the incremental `DatagramReader` + batched-
datagram pending queue, state persisting across calls (the POC's
reader-state finding); `Ok(None)` on channel EOF; fail-loud on a
stream end mid-frame and on undecodable frames (`WireError` → the
forked `AddrError`/`UdpHeaderError` RFC classes — the consumer
mirror of the producer stage's `UndecodableFrame` exit).
- `shutdown` — the half-close; the mux's implicit-EOF sentinel
reaches the producer's stage as EOF (association lifetime =
channel lifetime). `close()`/`Drop` own the teardown (sync reap +
structural EOF, the CONNECT/BIND posture); teardown-at-birth on
in-stream refusals; no `Clone` (compile-fail asserted).
- Typed error surface: open failures are `SocksSessionError::Open`
(ADR-049); the missing-egress 0x07 and factory-failure codes
surface from `associate` as `Rfc`.
E2E suite (`tests/consumer-associate.rs`, 8 tests over the harness):
round-trips (multiple in-order datagrams, one association, sentinel
relay address asserted), the split-datagram persistence test (a 60
KiB datagram reassembling across the call boundary + the pending-
queue drain — both halves of the persistence invariant), the empty-
payload round-trip (F-2, never EOF), shutdown → a fresh associate
succeeds immediately (the POC's test 4), Drop teardown + connection
health, the missing-egress refusal (typed 0x07, no phantom channel),
the egress-factory failure mapping, and the CONNECT+ASSOCIATE
regression guard (interleaved on one registration). Unit tests (4)
pin the reader internals over a duplex (`skip_auth`'s
handshake-free path).
Verification: cargo test 305 (7 suites, all green), clippy
--all-targets -D warnings, fmt, cargo doc --no-deps, and both
wasm32-unknown-unknown checks clean.
- Socks5ChannelSession::connect: open_channel(OP_SOCKS5_OPEN, {},
SOCKS5_ALPN) then the forked client typestate's use_stream path
over the channel BiStream (POC #1's fit); post-reply the session
is the raw byte pipe (0 B framing)
- Target construction via ToTargetAddr: IP-string forms parse to IP
targets, domain form passes through unresolved (ADR-012)
- split() borrowed halves (SplitPipe) / into_inner() owned pipe
(TakenInner — the session rides along as the teardown owner);
channel_id() for correlation
- Teardown ownership (alktunnels ADR-005 posture): Drop sync-reaps
the adopted entry (teardown_channel is sync; Drop cannot await) +
the pipe drop EOFs the producer pump structurally; close() is the
graceful path (write-half shutdown, then reap); the in-stream
refusal tears the channel down in connect (teardown-at-birth —
no phantom channel); no Clone (compile_fail doctest)
- Error surface: open failures are the typed SocksOpenError reasons
(ADR-049 §4); a refused target is the RFC reply code through
SocksSessionError::Rfc (consumer.md §Error surface)
- Harness: the consumer-side topology (wire_consumer_side[_with] —
the consumer dials with a real ChannelClient; the producer accepts
with the real registration) + witnessing_echo_dial; the
allow(dead_code) attr rides the alktunnels harness posture
- tests/consumer-connect.rs: 9 e2e tests — echo round-trip + CF-006
witness, target forms at the dial seam (ADR-012), refused dial →
typed RFC error + no phantom channel, FORBIDDEN open → typed open
error + no session, drop teardown + connection health, half-close
EOF semantics, into_inner pairing, Send shape
Verified: cargo test (219 green incl. the compile_fail doctest),
clippy --all-targets -D warnings, fmt --check,
check/clippy --target wasm32-unknown-unknown
The TCP BIND dispatch arm replaces its refusal with the full phase
model (POC #5-validated scope), plus the command gate at dispatch:
- The command gate: TCP BIND requires socks5:bind from the per-call
opener identity (CF-005/CF-006) — the pump handler resolves it
from the open-op AuthContext; serve_socks5_over<T> takes the
resolved can_bind boolean explicitly. A scope-less identity gets
the RFC-faithful ConnectionNotAllowed (0x02) with no factory call
(the gate precedes the seam) while CONNECT still serves it.
- The phase model (wire.md §BIND): the BindAcceptFactory at the
command-read interception point → reply#1 carrying BND.ADDR (the
factory's bind_addr) → the quiet wait (single-shot select!, exactly
two exits: the one accept, or the control stream's EOF/garbage/
error; any byte = protocol garbage → session ends, no data plane;
the accept future's drop IS the client-gone teardown) → reply#2
carrying the peer address (via the fork's published new_reply) →
pump_bidi inline (the CONNECT shape, R-02).
- Two-stage RFC-faithful failure mapping: factory failure → reply#1's
mapped code; accept-stage failure → reply#2's mapped code, no data
plane. No phantom listeners, no phantom data planes.
- The no-factory default is the wrapper-aware in-band variant
(ADR-007 variant 3, documented): sentinel reply#1 + sentinel reply#2
(0.0.0.0:0), the full phase model preserved — the channel is the
connection.
- The accept wait lives in the teardown-governed pump phase, never
under the establishment bound (ADR-007).
Tests: 16 unit tests (raw RFC client over duplex) + 7 harness
integration tests over channels — happy path, the gate (0x02 while
CONNECT serves, both scopes), quiet-wait garbage, client-gone
teardown (owner-flag witness), both failure stages over channels,
no-factory variant 3, and the three-command regression guard (CONNECT
serves, BIND serves under its gate, ASSOCIATE still refuses).
Verification: cargo test green (96 lib + 26 harness + 37
differential); clippy -D warnings clean (all targets, native +
wasm32-unknown-unknown); cargo fmt clean; wasm check clean.
Task: tasks/implementation/core-producer-bind.md (status → completed)
The crate's core task (implementation/core-producer-connect): the
producer half of src/producer.rs per producer.md, adapted to the
crate's seam types and the alkcall 0.8.0 establishment surface.
- The session-gate establisher (`socks5_session_establisher`):
validates/records the opener identity (the CF-005/CF-006 seam,
ADR-005) via the optional `identity_witness` probe (the alktunnels
posture); returns `Establishment::default()` — no plan payload
(the dial happens post-establishment; POC #1's wire note).
- The pump handler (`make_socks5_pump_handler`): `accept_bi()` →
the forked typestate over the `BiStream` (`accept_no_auth →
read_command`) → dispatch. CONNECT: the dial seam (`DialFn`,
ADR-004) at the command-read interception point; success:
`reply_success(sentinel)` then `pump_bidi` awaited inline (R-02 —
the JoinHandle tracks the data-plane lifetime). Refusal:
`reply_error` with the mapped RFC code — no phantom data plane.
- The ASSOCIATE/BIND arms land as RFC-faithful refusals
(`CommandNotSupported`) — the pre-vertical posture (ADR-008); the
dispatch skeleton + registration signature carry the seam
parameters from day one so the associate/bind tasks swap the arms
without touching the registration shape.
- `register_socks5_openable`: spec + establisher + pump via
`register_openable_with_establisher` (`timeout: None` = the 10s
default), scope-gated `socks5:open`. Missing callbacks = those
commands refuse RFC-faithfully, never a panic.
- `serve_socks5_over<T>`: the substrate-free protocol entry (the
POC shape), directly testable without channels; the builder
(`Socks5Producer::builder()`) is the spec'd assembly shape.
- Tests: 11 in-module (substrate-free duplex-driven — round-trip +
half-close teardown, refusal mapping per variant, missing-dial
refusal, ASSOCIATE/BIND arms, dispatch guard, establisher witness,
builder shape) + 5 harness integration (end-to-end CONNECT echo
with the identity witness, dial refusal 0x05 with no phantom data
plane, scope-class refusal 0x02, ASSOCIATE/BIND refusals with
CONNECT still serving, repeated CONNECTs on one registration).
The harness retargeted to the real producer (`RegisterMode::
Producer`); the minimal stand-in is a harness-internal helper.
Verification: cargo test 87+19+37 green; clippy --all-targets -D
warnings clean; fmt clean; wasm check + wasm clippy clean; cargo
doc clean; --all-features green.
tests/harness.rs — the producer<->consumer wiring over one
tokio::io::duplex carrying the channels 8-byte chunk format, the
alkcall 0.8.0 establishment surface consumed for the first time by
this crate (register_openable_with_establisher + the call/adopt
dance) — the first-consumption risk retired.
- Identities: consumer (socks5:open) / bind (socks5:bind only) /
both-scopes (explicit, ADR-006) + TestIdProvider (token path).
- In-process seams, no sockets, no `local`: echo_dial /
refusing_dial (variant-preserving), echo_egress (readiness
contract verified — next_reply parks Pending when idle) /
refusing_egress, echo_accept / refusing_accept_factory /
refusing_accept (two-stage BIND refusal shapes, mapped through
to_reply_error()).
- Producer-side registration helper: register_socks5_openable_minimal
+ socks5_session_establisher + RegisterMode — the single place the
producer wiring lives; the CF-006 identity witness rides it and is
asserted end-to-end.
- RawRfcClient: the hand-rolled RFC 1928 client helper over any
AsyncRead + AsyncWrite, round-tripped against a byte-level
hand-rolled server (success + refusal replies).
- Smoke test: harness up -> producer registered -> consumer open +
adopt -> raw RFC CONNECT -> echo round-trip through pump_bidi.
Fail-closed probe: scope-less identity -> FORBIDDEN, no channel
anywhere (the load-bearing POC #1 set_identity wiring requirement,
explicit in the install hook and tested).
14 harness tests. Verification: cargo test (127 green), clippy
(all-targets + wasm32) clean, fmt clean, doc zero warnings,
test --all-features green, both wasm checks clean (harness is
dev-only).
- egress.rs: the I/O-plane inversion point, function-not-trait at the
three factories; Egress is the one trait (the per-association
returned object)
- dial: TargetHandle (boxed halves, +Sync per the ChannelPlan bound),
DialFn over the typed TargetAddr (the POCs' JSON stand-in retired),
SocksDialError with the five-variant RFC-faithful mapping
- egress: Egress (send payload-only; next_reply returns inner bytes;
the readiness contract — Pending when nothing is ready — documented
on the trait), EgressFactory, EgressError, the egress_factory
sync-lift constructor
- accept: BindAccept (the accept future owns its resources end-to-end;
the quiet wait's drop IS the client-gone teardown), BindTargetHandle
(ownership structural; the composed keep-alive owner rides inside
the halves provider-side), BindAcceptFactory, the two-stage error
pair (factory → reply#1; accept-stage → reply#2)
- no substrate type named anywhere in the module; ownership rides the
handle structurally (no _ bindings)
- 9 new tests: the four error→reply-code tables (all variants,
byte-compared), the sync-lift Ok/Err paths, the closure compile
shapes, the Send/Sync/Unpin assertions; lib.rs re-exports the
surface
Verified: cargo test (113 green), --all-features green, clippy -D
warnings clean (native + wasm32), fmt clean, cargo doc zero warnings,
wasm32 check clean.
- frame(addr, payload): [len: u16 BE][addr + data] over
TargetAddr::to_be_bytes() + payload; the joint bound enforced at
frame time (checked_add; 65507 + 255-char domain = 65766 > 65535 →
Oversize) with addr_encoding_len exposed (7 v4 / 19 v6 / 259 max
domain) and MAX_INNER_LEN pinned
- DatagramReader: incremental reassembly across chunk boundaries
(split frames, batched frames, partial headers), infallible,
returning per-frame Results; the addr sub-layout decoded by hand
(ATYP-keyed, UTF-8-validated) with typed decode errors —
BadAtyp / ShortAddress / DomainNotUtf8 / EmptyFrame — so the
producer stage fails the association loud on protocol garbage
(udp_undecodable_frame_ends_association, POC #2)
- module docs state the framing-once invariant (the egress speaks
inner bytes; this layer owns the single length prefix — the
double-framing bug class POC #2 caught), the F-2 invariant at
payload level (a zero-length payload is a legal empty datagram,
never EOF; wire len == 0 is unreachable garbage), and the EOF
layering (the channels-level sentinel, outside the codec)
- doc-sync: wire.md + bast.md residue corrected — "len == 0 is a
legal empty datagram" replaced with the payload-level F-2
statement (len == 0 unreachable since addr >= 7 bytes)
- lib.rs re-exports frame, DatagramReader, addr_encoding_len,
MAX_INNER_LEN, WireError (convention 15)
Verification: cargo test (58 lib + 37 differential), clippy
--all-targets -- -D warnings, fmt --check, doc --no-deps, and both
wasm checks (check + clippy on wasm32-unknown-unknown) — all clean.
- Cargo.toml: alkcall = "0.8.0" (convention 11 — pinned, first
consumption of the 0.8.0 surface), serde/serde_json, bytes, and
the std-only futures subset (BoxFuture seam shape). tokio's
wasm-clean subset and the empty local/composed features unchanged.
- lib.rs: the six skeleton modules per overview.md §Module Map
(params, wire, producer, consumer, egress, error) — stub files
with module docs naming their filling tasks; no re-exports yet
(each implementation task adds its own).
Verified: cargo test (76 tests), clippy --all-targets -D warnings,
fmt --check, cargo doc --no-deps, both wasm checks
(check + clippy on wasm32-unknown-unknown), and the dep-graph
spot-checks (no mio/socket2/tokio::net in the default normal graph;
normal tokio features = bytes/io-util/macros/rt/sync/time only).
alkcall 0.8.0 from crates.io confirmed wasm-clean via probe build
before wiring.
Task: tasks/implementation/core-crate-deps.md (completed)
Batch 1 of the Phase 1 decomposition: the default, wasm-clean crate
(producer + consumer + wire + seams + params/errors), per the
architecture spec set (ADRs 001-013) and the three POCs' validated
shapes. The assembly layer (local/composed providers, front doors) is
batch 2, decomposed after review-protocol-core opens.
- foundation: crate-deps (alkcall 0.8.0 first consumption), wire-codec
(the ASSOCIATE frame + the joint bound), seam-types (the three
ADR-004 seams), params-errors (the one-way-door surface)
- harness: duplex transport + identities + in-process seams (the
alkcall 0.8.0 establishment surface proven before the producer)
- verticals: producer/consumer splits per command (CONNECT,
ASSOCIATE, BIND) — each POC's scope, producer tasks test via the
raw RFC client, consumer tasks e2e through the harness
- closing: integration suite (three-command regression, teardown
matrix, identity matrix, docs sync) + review-protocol-core (the
batch gate)
taskgraph: 8 generations, no cycles, risk path 2.55 (no high-risk
tasks). Also records the wire.md/bast.md len=0 residue (payload-level
F-2 is the operative invariant) as a doc-sync deliverable of the
wire-codec task.
Verification: cargo test (76 passed), fmt clean; taskgraph
validate/cycles/parallel/critical clean.
Security + code-smell + coverage review of the targeted fork as owned
code (the extraction review was narrow; this is the full pass).
Findings: 1 major (F-1: reply_success/reply_error write replies with
.write() — a partial-write hazard invisible to clippy's
unused_io_amount under the err_when-chaining idiom, executable-probe
verified; rfc1928.md's write_all rationale is false for these sites),
7 minors (2 security-posture notes, 2 extraction leftovers, 3
coverage gaps), 4 observations. No critical findings.
Verification: 38+32 tests, clippy (all-targets + wasm32) -D warnings,
fmt, doc, wasm check, publish dry-run, MSRV 1.88 native+wasm — all
green. Coverage 92.63% regions / 95.40% lines (cargo-llvm-cov); gaps
attributed per finding. Differential suite's 15 upstream citations
spot-checked against the v1.0.0 reference checkout — all accurate.
The mechanical half of the posture change: src/vendor/ ->
src/rfc1928/ (module alksocks::rfc1928), tests/vendor-differential.rs
-> tests/rfc1928-differential.rs (git mv — history preserved).
- Fork point pinned: the tree as of 5b18d4d (the extraction review's
gate commit); provenance permanent in the module docs and the
extended LICENSE notice (upstream MIT preserved, fork-point +
post-fork modification statement added — extend, never replace)
- The lint-allow posture dissolved: all 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, the
AuthMethod::new self-taking signature renamed
into_starting_state, the F-4 ToSocketAddrs string re-worded — it
no longer names the cut resolve_dns(), the unused ready! macro
and dead AuthenticationMethod::from_u8 removed, the upstream
commented-out async-std note folded into the impl doc)
- Module docs rewritten: ownership statement (project conventions
apply unconditionally), extraction provenance (ADR-011) +
ownership (ADR-013) + maintenance posture (behavioral reference,
hand-port path)
- Differential suite survives the rename untouched (32 tests pin
bytes, not upstream text)
Verification: cargo test (38+32 green), cargo clippy --all-targets
-D warnings, cargo fmt --check, cargo check/clippy
--target wasm32-unknown-unknown, cargo test --all-features, cargo
doc --no-deps — all pass.
The extraction review against vendor.md's cut table, ADR-011
§Amendment, and AGENTS.md conventions. All five file diffs vs the
fast-socks5 v1.0.0 reference checkout read hunk-by-hunk and
categorized (cut / carried changes / adjustments / provenance /
tests); wasm, clippy, fmt, doc all green; RFC 1928 spot-checks
(reply form, method order, UDP zero fallback, domain cap, FRAG
split) independently verified against the RFC text.
F-1 (critical, resolved here): ReplyError::from_u8 kept upstream's
unreachable!("ReplyError code unsupported.") arm while being
wire-reachable — Socks5Stream::read_request_reply feeds the raw
reply byte from the wire into it, and RFC 1928 §6 marks 0x09-0xFF
unassigned, so a misbehaving peer would panic the consumer task.
Unassigned codes now map to GeneralFailure (the RFC's catch-all),
pinned by a new test, and the panic-translation adjustment is
recorded in vendor.md's adjustment list (with the Config-Default
and write_all notes) in the same change.
Findings F-2..F-4 (dead commented code dropped with its region;
Cargo.toml feature-layout comments; the preserved resolve_dns error
string) reviewed and accepted. Review task file carries the full
record; the gate to the downstream task groups is open.
Verified: cargo test (38 + 32), cargo test --all-features, clippy
--all-targets and --target wasm32-unknown-unknown (-D warnings),
cargo check --target wasm32-unknown-unknown, cargo fmt --check,
cargo doc --no-deps.
Extract the T-generic client session from fast-socks5 v1.0.0's
client.rs into src/vendor/client.rs (ADR-011 §Amendment extraction,
vendor.md cut table row 2): Socks5Stream<S> with use_stream (the
greeting, method-choice read, password sub-handshake, request
encoder, reply reader, socket accessors), the AsyncRead/AsyncWrite
impls, and the pruned Config (skip_auth only).
- typed MissingTargetAddr variant replaces upstream's
anyhow::Error::msg(...).into() at client.rs:285 (the one SocksError
addition; the client.rs:528 Other(anyhow!) site went with the cut
Socks5Datagram)
- use_password_auth's unreachable!() is an error return (upstream's
None-arm message; convention 2), and the sub-negotiation reply's
version byte is now checked (was only a dropped debug!)
- write -> write_all in request_header (clippy unused_io_amount;
byte-identical on a healthy stream)
- cut (not copied): Socks5Datagram, the Socks5Stream<TcpStream>
convenience impl (connect*/connect_raw), Config::connect_timeout
- 14 first-party duplex tests vs raw-byte mock servers (uncorrelated
from the vendored server typestate): both handshakes, 0xff
refusal, version mismatch, domain/IP request forms, the full
failure-code table, UDP-ASSOCIATE zero fallback, skip_auth,
io-impl copy, socket accessors
- mod.rs: doc touch-ups (client module now landed; fixed the
pre-existing broken intra-doc link)
Verification: 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.
Extract fast-socks5 v1.0.0's non-net lib.rs region into
src/vendor/mod.rs (scaffolding-pinned root; the task's lib.rs target
name corrected — a second root file would conflict, see task Notes):
consts, Socks5Command (as_u8/from_u8 made pub), AuthenticationMethod,
SocksError pruned to the extracted region's consumers (no anyhow/
Redaction/InvalidHeader/ReplySocks4Error/ArgumentInputError), the
Result alias, ReplyError codec, UdpHeaderError + new_udp_header/
parse_udp_request. util/target_addr.rs gains TargetAddr/AddrError
(minus resolve_dns + its two variants)/ToTargetAddr/Addr/read_address;
util/stream.rs gains the read_exact!/ready! macros (de-exported to
pub(crate) use) + ConnectError + to_reply_error. SocksServerError
lands verbatim in server.rs early — SocksError's #[from] needs it
before the typestate task. 16 first-party codec tests (round-trips,
mapping tables, garbage-RSV/unknown-ATYP); no sockets, no env_logger.
Newer-clippy style lints allowed at module level in vendored files
(verbatim diff posture, rationale recorded).
Verification: cargo test (16 pass) + --all-features; clippy
--all-targets -D warnings (native + wasm32); cargo check
--target wasm32-unknown-unknown; cargo fmt --check; cargo doc.
Negative sweeps: no anyhow/log/tokio::net/socket2/tokio-stream/
async-trait under src/vendor/; no #[macro_export]; resolve_dns and
the pruned error variants absent.
The inline vendor (vendor.md, ADR-011 §Amendment) is the root of the
crate's dependency graph; this decomposes it before any downstream
group (params/wire/producer/consumer/doors).
- vendor-crate-scaffolding: Cargo.toml (wasm-clean tokio subset, no
alkcall yet), lib.rs, vendor/ skeleton, bundled MIT notice
- vendor-wire-vocabulary: consts, codecs, errors, macros — with the
extraction-boundary pins (error-variant pruning, pub command
codecs, macro de-exporting)
- vendor-server-typestate: Socks5ServerProtocol + the new_reply
publication (carried change #2); BIND's driving is producer-group
work, the vendor carries only the surface
- vendor-client-session: Socks5Stream::use_stream path + the
AsyncRead/AsyncWrite impls; Config pruned (connect_timeout cut);
tests against a raw-byte mock server so it runs parallel with the
typestate task
- vendor-differential-tests: RFC edge-case suite over duplex streams,
pinning upstream behavior per vendor.md's differential posture
- review-vendor-extraction: the gate before downstream groups —
file-by-file diff vs /workspace/fast-socks5 must contain only
cut/carried-change/adjustment hunks
Graph validated: all dependency ids resolve, no cycles; topological
order scaffolding → wire → (typestate ∥ client) → tests → review.
Complete the ADR-011 §Amendment (extraction, not wholesale-copy-plus-
gate) across every doc that stated the pre-amendment posture:
- ADR-011: add the two extraction-recipe exclusions the amendment
missed (the socks4 module — upstream's separate default-off cargo
feature, out of RFC 1928 scope; the legacy Config/Authentication
auth layer — ungated but consumed only by the cut region) and the
dependency-surface adjustments (log -> tracing, anyhow dropped,
async-trait dropped with its legacy-layer uses)
- ADR-009: amend — the vendor's net gate dissolves; features collapse
to local + composed; local carries tokio/net + socket2 itself
- vendor.md: replace the contradictory old "four carried changes"
section with the two-change reading + extraction-time adjustments;
extend the cut table (socks4 row, legacy auth layer)
- overview.md: dependency diagram + feature table drop the net
feature; module map reflects two carried changes
- README.md: principles 7/8 + doc/ADR tables updated
- ADR-002/012, open-questions.md (OQ-SK-04), phase-0.md current-state
sections, AGENTS.md conventions 4/17 + stale wasm-caveat sentence,
POC findings posture notes: aligned with the extraction
Verification: repo-wide grep for stale "four carried"/default-on net/
implies-net statements returns only amendment-context matches;
extraction recipe re-verified against POC #4's gate map and the
fast-socks5 v1.0.0 source (async-trait usage confined to the cut
legacy layer; AuthMethod<T> traits are native async-fn-in-trait).
The Phase 0 doc had accumulated stratified updates (each revision layered
on top of the prior text), leaving duplicated statements, superseded
reasoning presented as live, and a few internal conflicts. This rewrite
is a consolidation only — no decisions changed:
- Each OQ opens with a status line; resolved content stated as settled
fact; superseded reasoning retained only where it carries Phase 1 ADR
weight (e.g. OQ-SK-04's decision tree as the reasoning trail).
- The inline-vendor decision now stated once (OQ-SK-04 + the Convergence
Layering bullet) instead of ~10 restatements; stale 'file the upstream
ask' residue removed (Convergence protocol-base bullet).
- The component-3 transfer to alknet stated once in §Vision; the noq
section is background-only; OQ-SK-06 compressed to its resolution.
- OQ-SK-01/03's duplicated direct-op update merged into its OQ;
OQ-SK-08's superseded sequencing consistently framed as the retired
POC workaround against alktunnels 0.1.0.
- Checklist: OQ-SK-03/06/07 and the POC box checked to match their own
text; 'RFC-refused in both POCs' corrected (fast-socks5 refuses BIND;
POC #5 implemented it).
- Preserved: all external anchors cross-referenced by the findings files
(§OQ-SK-07 register + numbering, §Prior art's run_udp_proxy_custom
caveat, OQ-SK-04's decision record, the Convergence Layering bullet),
every ADR/API/file:line reference, POC numbers, and measurement
figures. Restored on audit: the 65507 u16-suffices rationale,
skip_auth_this_is_not_rfc_compliant, lib.rs:91, the wasm-resolver
interaction note.
Verified: anchor audit against all cross-referencing docs (no dangling
references), content-loss audit vs the prior version (no unsuperseded
fact dropped), fast-socks5 v1.0.0 spot-checks (TCPBind refusal sites,
wait_on_tcp:1195, private new_reply, typestate methods — all true).
The ownership premise ('we own fast-socks5') was wrong — it is
third-party upstream and has not been forked. The corrected posture
resolves OQ-SK-04: inline vendor. The wrapper's needs exceed the wasm
gate regardless (BIND absent upstream — POC #5 proved the wrapper
drives it; the run_udp_proxy_custom bind seam; private new_reply), so
carrying the T-generic subset (~120 cfg lines, POC #4-verified) pays
for itself independent of the adapter story.
AGENTS.md:
- convention 17 rewritten: 'Upstream: alkcall is ours to shape;
fast-socks5 is inlined' — the ownership split stated plainly, the
inline vendor + its four carried changes named, the courtesy PR
kept optional/never-waited-on, the vendor point (v1.0.0) noted
- convention 4: wasm posture note updated to the resolved vendor
- prior-art entry: fast-socks5 marked third-party/NOT ours
phase-0.md:
- OQ-SK-04 RESOLVED (2026-09-20): resolution note headers the OQ (the
two settling facts: wrong ownership premise; needs exceed the gate);
decision tree retained as reasoning trail with both legs annotated
(Case 2's premise falsified by the BIND need); the root question
reframed as retrospective; checklist item checked
- §Prior art fast-socks5: header + run_udp_proxy_custom caveat
reworded from 'fileable ask' to in-tree change
- settled-foundations wasm caveat resolved; survey + checklist
prior-art entries marked; 'What Phase 0 could NOT answer' wasm item
struck as resolved; convergence Layering bullet now enumerates the
four in-tree carried changes; upstream-asks bullet: fast-socks5
items are no longer asks
poc-connect-wasm-findings.md / poc-bind-findings.md /
iroh-socks5-eval.md: 'we own it' ask sections corrected to
in-tree-vendor changes (dangling fragment from an earlier edit also
fixed); ledger item 3 annotated
Verified: fast-socks5 upstream still 1.0.0, no net feature, BIND
refused (lib.rs:384-386/server.rs:813-816) — the needs-are-real facts
stand. Docs-only change; no build surface affected.
The ask ledger's two alktunnels items landed 2026-09-18/20 — superseded
in shape, not implemented as posed. The graduation (alktunnels
tunnels-graduation.md -> ADRs 007/008) adopted the ssh
forwarded-tcpip-shaped alternative over the per-ask carriers.
phase-0.md:
- settled-foundations: establishment gains the reply-projection note
(ADR-049 amendment 3); the relay/hub note records the in-tree
ChannelRelay/HubLegTemplate (ADR-051); identity-seam note marked
current at 0.8.0
- OQ-SK-01: Option A updated — the direct op (channels/tunnel/direct,
ADR-007) is the dynamic-target channel; POC #5's scope-gate +
ADR-007 rationale point the same way (separation lives at the ACL)
- OQ-SK-03: composed egress's vehicle is open_direct +
Substrate::Udp; the ~0.8 ms measurement carries over; the stale
POC-candidate hunch paragraph updated
- OQ-SK-08: the landed note — ask #1 dissolved (bind-first establisher;
the real bound rides the open reply; POC #5's open-rides-the-accept
workaround + the 10s-bound finding retire), ask #2 landed at the
wire's native moment (forwarded op's {listen, substrate, peer}
params), correlation token per ADR-008 Amendment 1
- convergence: egress seam composes against 0.2.0; the upstream-asks
bullet records the landing per crate; the BIND hand-off bullet notes
variant 2 is now fully vanilla-fidelity
- checklist OQ-SK-08 item + POC #5 register entry point at the landed
note
poc-bind-findings.md: ledger statuses appended (superseded in shape;
evidence stands as the alktunnels-0.1.0 record)
AGENTS.md: alkcall pin 0.7.1 -> 0.8.0 (reply projection noted);
architecture context: alktunnels v0.2.0 entry added, alkcall 0.8.0
surface listed
Verified against the repos: alkcall 0.8.0 changelog (U-1/U-2/ADR-051),
alktunnels 0.2.0 changelog + ADR-007/008 + exports
(bindfirst_establisher producer.rs:641, register_tunnel_bindfirst_
openable :924), fast-socks5 still 1.0.0 with no net feature (the
courtesy tail). Docs-only change; no build surface affected.
POC #5 run 2026-09-15 (alksocks-bind-poc, standalone, 9 tests) —
OQ-SK-08 POC-validated; the POC register (OQ-SK-07) is closed.
- The BIND phase model extends end to end on one channel: request →
reply#1 (BND.ADDR) → quiet wait → one inbound accept → reply#2
(peer) → pump_bidi pass-through (0 B overhead, like CONNECT)
- Both accept variants implemented: local (real TcpListener, faithful
RFC) + composed (alktunnels listen-tunnel — per-listener ACL for
free, hub-relayable)
- The address-first-vs-accept-first deadlock resolved in shape:
reply#1 first, the tunnel open rides the accept future
- The socks5:bind scope gate distinguishes ingress from egress
(RFC-faithful ConnectionNotAllowed refusal; CONNECT unaffected)
- Vanilla front door re-validated for the BIND conversation (raw pipe,
zero translation); CONNECT + ASSOCIATE regression-guarded on the
same registration — the RFC 1928 command surface is complete
- fast-socks5: zero BIND ask (typestate-driven; reply_success carries
reply#1); courtesy ask: publish the private new_reply encoder
- alktunnels ask ledger pinned at two items for the one-version bump:
(1) listen-addr inspection without consuming an accept, (2) accepted
handles carrying peer metadata — both POC-evidenced, both additive
Verification: cargo test (9/9), clippy --all-targets -D warnings,
fmt --check clean in alksocks-bind-poc.
- OQ-SK-07: add a POC register table (one-place accounting of all five
numbered POCs: #1/#2/#4 passed, #3 dropped, #5 pending); mark the
priority order as historical; note numbering must not be renumbered
- §Vision: fix the components 1+2 layout sentence now that component 3
transferred; keep the transfer reasoning
- alknet-client ancestor section: replace the stale 'this crate's
client half rehomes that work' sentence with a historical note (the
rehoming is alknet's, per the 2026-09-14 scope decision)
- noq prior-art section + OQ-SK-06: add transfer banners so ~25 lines
of transferred research are not read as live scope
- OQ-SK-02: spell out what the 'yes / planes-don't-compare /
probably-not' shorthand maps to
- OQ-SK-04: note the component-3 transfer does not change the wasm
decision inputs
- OQ-SK-07 #3: rewrite the ambiguous 'at the time of the decision'
parenthetical as before/after
- Survey list: mark noq and iroh entries as alknet-facing background
- Convergence checklist: uncheck the POC item (BIND POC still pending)
and explain the checkbox semantics
Verified against the repo: findings files, POC crates
(alksocks-connect-poc 8 tests, alksocks-udp-poc 11 tests,
alksocks-fs5-wasm-poc exists), upstream line refs (fast-socks5
lib.rs:384-386 / server.rs:813-816 / 1195, noq runtime/mod.rs:44 /
endpoint.rs:162, alktunnels producer.rs:207,392), op name
channels/socks5/sub in both POC crates.
Verification: docs-only change; no build/test surface affected.
Post-POC #2 findings review: a real BIND use case and mechanism
surfaced, plus a channel-capacity analysis for proxy workloads.
- OQ-SK-08: fast-socks5 parses but refuses TCPBind — the wrapper owns
the whole shape (zero upstream ask). Phase-model extension: reply#1
(BND.ADDR) -> quiet wait -> one inbound accept -> reply#2 (peer) ->
raw pass-through; iroh-socks5's channels-native BIND is convergent
precedent. Accept variants mirror OQ-SK-03's egress variants:
feature-gated local listener (faithful RFC) vs composed alktunnels
listen-tunnel (needs a listen-addr-inspection ask) vs wrapper-aware
only. Security note: BIND is inbound egress — SOCKS5_OPEN_SCOPE must
distinguish it from CONNECT/ASSOCIATE.
- OQ-SK-09: 256-channel default vs proxy fan-out; wire change
rejected, assembly-layer config + documented proxy-workload
recommendation is the mechanism; in-channel port multiplexing
([port:u16] per chunk) considered and rejected (re-opens OQ-SK-03's
demux-vs-phase trade, breaks 0-B pass-through, AGENTS.md #10).
- OQ-SK-05 addendum: virtual address space / identity-scoped subnets
adoptable with no wire change — DST.ADDR/BND.ADDR translation via
the dial callback; hub exposure via terminate-and-re-produce.
- POC #5 (BIND, both accept variants, vanilla front-door test) added
to OQ-SK-07; checklist + convergence updated; upstream-asks list
pinned (fast-socks5: net gate + UDP bind seam; alktunnels:
listen-addr inspection).
Verification: docs-only change (phase-0.md).
- Scope resolution: the quinn/noq AsyncUdpSocket client wrapper
(component 3) transfers to the alknet rewrite (its ancestor work —
ADR-090, quinn-proxy POC — lives there; it consumes no channels and
has no consumers planned here). OQ-SK-06 and would-be POC #3 go
with it. alksocks = the producer/consumer pair only.
- Convergence section added (Phase 0's output for the Architect): the
validated approach pieces (fast-socks5 typestate driven directly for
both commands, the handle-swap wire shape, DialFn + EgressFactory
egress seam, "both" auth split by the door, alktty/alktunnels
layering, vendored-subset fork posture) and the list of Phase 1 ADR
decisions with their collected inputs (datagram framing, demux vs
phase, params/ALPN, scope convention, door mapping, wasm posture).
- Checklist: OQ-SK-06 resolved-by-transfer; POC list complete (#1/#2/#4
passed, #3 dropped); convergence checked off.
- alksocks-udp-poc (/workspace/alksocks-udp-poc/): 11 tests pass,
clippy -D warnings + fmt clean; the handle-swap baseline validated
end to end (one channel per association, [len][addr+data] frames
in-band, RSV/FRAG only at front doors, channel EOF = association
lifetime, per-datagram addressing without a flow table)
- Both egress variants implemented behind the EgressFactory callback;
composed per-destination alktunnels udp-tunnel open cost measured at
~0.8 ms mean (N=20, in-process loopback) — comfortable for DNS +
browsing
- Front-door RFC-codec composition confirmed (RFC UDP header codec at
the vanilla boundary only); CONNECT regression-guarded on the same
registration
- Load-bearing API findings for Phase 1: the select!-driven stage's
egress readiness contract (Pending-when-idle; readable() not
writable(); closed queues end, never spin) and the length framing
applied exactly once by the wire layer (egress returns inner bytes)
Verification: cargo test (11 pass), cargo clippy --all-targets -- -D
warnings, cargo fmt --check, cargo doc --no-deps (all clean in the
POC crate); phase-0.md OQ-SK-03/07 statuses updated.
channel_id is u32 on the wire (~4B ids, monotonic counter, ADR-040);
256 is the default per-connection max_channels (memory bound, 256x1MiB
reassembly buffers) and the default per-identity DoS cap (ADR-041) —
both configurable without a wire change. AGENTS.md convention 10 and
the OQ-SK-03 composed-egress note now carry the qualifier.
- endgame story makes both mechanisms real: consumer binds the produced
SOCKS5 resource locally (optional front door, ssh -D shape without
the forced bind) and points tun2proxy at it
- producer side: identity seam only (CF-005/CF-006); a password there
is moot against the ACL system — in-band conversation runs NoAuth
- consumer front door: owns RFC 1929 where vanilla clients need it
(tun2proxy/curl user:pass), maps accepted credentials to the channel
identity it opens the producer with; the SOCKS password never crosses
the channel as a SOCKS password
- replace-or-second-gate sub-question dissolves: the mechanisms occupy
different planes (door-local creds vs channel-level identity)
- remaining for Phase 1: credential->identity mapping shape; producer-
side local backend door surface; AuthMethod trait vs door-owned check
- the ASSOCIATE reply's BND.ADDR/BND.PORT is a relay handle, not a
destination (per-datagram destinations ride the datagram header);
swapping "tell the client about a UDP port" for "tell the client
about the UDP tunnel" preserves every RFC property: one channel per
association, {addr, data} frames in-band, channel EOF = association
lifetime, optional front-door binds on either side
- sentinel-address hunch superseded: no virtualized relay address is
ever needed on the in-band path; BND.ADDR there is pure
RFC-compatibility surface
- producer egress has two variants, both dial-callback policy (OQ-SK-01
shape): local UdpSocket (no flow table, native-only) or composed
per-target alktunnels udp-tunnels (lazy destination->tunnel table;
flow table reborn as channel handles; per-target ACL for free —
dissolves most of OQ-SK-05 for the composed path)
- no upstream ask: alktunnels' connected-udp substrate is correct as-is
for per-target composition; an unconnected-egress resource would be
SOCKS5 again (circular)
- POC #2 baseline settled: sketch both egress variants behind the dial
callback, measure per-destination open cost of the composed variant
- new docs/research/iroh-socks5-eval.md: full source review of
iroh-socks5 0.5.0 (github.com/mattgeddes/iroh-socks5, MIT OR
Apache-2.0) — quality verified, structural sibling of the alksocks
shape, but iroh-coupled (no generic-T seam) with an RFC subset, so
fast-socks5 stays the protocol base (POC #1/#4 conclusions stand)
- phase-0: prior-art section for iroh-socks5; OQ-SK-03 updated with its
UDP design as the POC #2 baseline (framed {addr, data} in-band
datagrams, RFC header codec at front doors only, no producer-side
flow table, control-EOF association lifetime, FRAG != 0 refused)
- POC #1 passed (alksocks-connect-poc, 8 tests + curl front-door
example): BiStream-as-T genericity, register_openable_with_
establisher fit, pump_bidi data plane, typed session refusal,
RFC reply-code dial-refusal mapping, vanilla-client front door.
Structural resolution: the dial belongs at the command-read
interception point (target is unknowable at open time); the
establisher is the session gate. OQ-SK-01/02 shapes resolved
empirically.
- POC #4 passed (fs5-wasm-poc, kept at /workspace/alksocks-fs5-wasm-poc):
the fork is genuinely minimal — ~120 lines of #[cfg] insertions
gate kernel-net behind a default-on 'net' feature; the wasm-clean
subset compiles for wasm32-unknown-unknown (--no-default-features)
and still completes a full RFC 1928 CONNECT conversation. OQ-SK-04
reduces to the adapter-story question.
- Corrected the run_udp_proxy_custom prior-art claim: it binds a
kernel socket unconditionally (relay closure + reply IP only);
the channels path drives the typestate directly. Fileable ask.
- Findings: docs/research/poc-connect-wasm-findings.md
The root question is 'does wasm make sense for alksocks?', not 'how
do we get wasm': Case 1 (wasm wanted) -> fork-first, PR as courtesy,
carry regardless; Case 2 (wasm unwanted) -> plain dependency on the
router.rs native path, no fork (the alkhttp precedent, strictly
better when its premise holds). Decision inputs written: (a) is
there a planned sandboxed (wasm) consumer of the SOCKS5 service,
(b) is the fork genuinely minimal (POC #4 checks). Fork-first is a
conditional preference, not a general one — do not carry a fork for
an invariant's sake. AGENTS.md conventions 4/17 aligned.
- phase-0 vision: components split — (1) producer half (expose the
SOCKS5 service over channels), (2) consumer half (consume it,
optionally bind it locally — the ssh -D front door + tun2proxy
path), (3) standalone SOCKS client wrapper for quinn/noq that must
work with any RFC 1928 server (iroh relay/peer IP-leak motivation,
alknet ADR-090 generalized); the earlier docs conflated the
consumer-side bind with the producer-side local backend
- principle 6: the vpn-like endgame — tunnel a produced SOCKS5
resource to a local port, point tun2proxy (--proxy socks5://
natively) at it; verified tun2proxy src/args.rs
- OQ-SK-04 resolved as fork-first: 'fork' = vendor-and-vet inline in
the crate (minimal diff, feature-gate native-net), PR upstream as
courtesy, never wait on approval; wasm-drop (alkhttp precedent) is
the escape hatch, not the plan
- AGENTS.md conventions 4/9/17 aligned (fork-first, binds on either
side, wasm posture)
- OQ-SK-03: RFC 1928 control/data never interleave (fast-socks5
wait_on_tcp treats post-reply control bytes as garbage), so the
alktty 5-stream demux solves a problem SOCKS5 doesn't have; phase
model (pass-through CONNECT, len-prefixed datagrams post-reply)
subsumes it; the demux's real advantage is an additive in-band
stream-type vocabulary — noted as the honest one-way-door fork
- OQ-SK-04: no structural fork needed for the channels surface
(router.rs interception shape covers CONNECT; run_udp_proxy_custom
+ reply_success + the pure UDP codec cover ASSOCIATE); the
fork/wrap/reimplement decision is driven entirely by the wasm
feature-gating outcome; full fork documented as least attractive