--- status: complete last_updated: 2026-09-20 (ledger statuses updated — the graduation landed; both alktunnels asks superseded in shape) --- # alksocks POC findings — channels-native SOCKS5 BIND POC #5 from phase-0 §OQ-SK-07, run 2026-09-15. Crate: `alksocks-bind-poc` (standalone, `/workspace/alksocks-bind-poc/`), against alkcall 0.7.1 (crates.io), alktunnels 0.1.0 (crates.io, `local` feature — the listen surface is complete in the published version), fast-socks5 1.0.0 (crates.io). 9 tests, all pass; `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` clean. Validates OQ-SK-08's design shape: **one channel per BIND session; the phase model extends (request → reply#1 with `BND.ADDR` → quiet wait → exactly one inbound accept → reply#2 with the peer address → raw pass-through data plane); both accept variants behind the accept callback; the `socks5:bind` scope gate.** fast-socks5 parses the command but refuses it (`lib.rs:384-386`, `server.rs:813-816`); the wrapper drives the typestate directly and owns the whole BIND shape — **zero fast-socks5 upstream ask for BIND itself** (the typestate's `read_command()` returns `(cmd, target)` and the wrapper branches; `reply_success`'s `sock_addr` parameter is the reply#1 carrier; `pump_bidi` is the data plane). ## What was validated 1. **The BIND phase model — confirmed end to end.** The RFC conversation and the data plane ride the same channel `BiStream`: the typestate runs `accept_no_auth → read_command` (BIND arrives as `Socks5Command::TCPBind`), reply#1 carries `BND.ADDR`, the quiet wait selects the one inbound accept against the control stream, reply#2 carries the peer address, and `pump_bidi` (inline — R-02) two-pumps the accepted connection against the channel. Zero per-packet headers in-band (the CONNECT pass-through conclusion extends literally — BIND's post-reply#2 stage is a plain byte stream). 2. **The local accept variant (variant 1, the faithful RFC shape) — confirmed.** A real `TcpListener` per BIND session: reply#1 carries the listener's *actual* `local_addr` (a real network-B address); the "app server" dials the advertised address; reply#2 carries the *real* peer address (`accept()`'s peer, byte-exact — the test asserts the app's ephemeral port); the data plane echoes both directions. Vanilla front-door clients work (test 6). 3. **The composed accept variant (variant 2) — confirmed, with the sequencing finding below.** The listener lives on an alktunnels listen-tunnel resource (`register_tunnel_listen_openable` + `AcceptQueue` + a real far-side `TcpListener`); the SOCKS5 producer opens the tunnel *during the quiet wait* and the popped accept's halves become the data-plane peer. The full stack round-trips: SOCKS5 consumer → SOCKS5 producer → composed accept → `alk/tunnel` listen channel → tunnel producer → real listener → the app server's connection → back through the pump. Per-listener ACL for free (the tunnel resource's ACL governs who can mint the listener), hub-relayable — the variant's whole point. 4. **The scope gate — confirmed.** The pump handler reads the open-op `AuthContext`'s identity and requires `socks5:bind` before serving a BIND; an identity without it gets the RFC-faithful `ConnectionNotAllowed` (0x02) refusal — while CONNECT still works for the same identity (the gate distinguishes ingress listeners from proxy egress; one identity having egress does not imply ingress). The gate is the open-op identity seam consumed at command-dispatch time — no new ops, no params change, no one-way-door exposure. 5. **The quiet wait's semantics — confirmed.** A control byte after reply#1 is protocol garbage (the ASSOCIATE post-reply semantic BIND shares; RFC 1928 §3's "BIND: no further control data" reading); the session is refused with no data plane. A client that BINDs and vanishes (EOF mid-wait) tears the listener down with its session — the accept future (which owns the listener) is dropped by the `select!` branch; a fresh dial to the advertised address fails with connection refused. The session's natural end (half-close post-data-plane) releases the listener the same way. 6. **Failure mapping — confirmed RFC-faithful.** A factory failure (pre-reply#1: an unbindable port, a held port) maps to reply#1 with the factory error's reply code (shortage → GeneralFailure); an accept-stage failure (post-reply#1) maps to reply#2 with the mapped refusal code and no data plane. No phantom listeners, no phantom data planes; refusals are typed end to end. 7. **One registration, all three commands — regression-guarded.** CONNECT (POC #1's data plane, unchanged) and UDP ASSOCIATE (the POC #2 skeleton) still serve on the same `channels/socks5/sub` op after the BIND branch lands. The RFC 1928 command surface is now complete in the crate's shape: CONNECT + BIND + ASSOCIATE. 8. **The vanilla front door — confirmed for BIND.** The raw-pipe bridge (POC #1's CONNECT front-door shape, reused verbatim) carries a full vanilla BIND conversation: an in-process RFC 1928 client (greeting → BIND command → reply#1 → reply#2 → data plane) through a `copy_bidirectional` pipe onto the channel. Zero protocol translation at the door; the RFC conversation passes through transparently (0 B pass-through, exercised for the BIND conversation this time). ## Structural findings (the load-bearing discoveries) **The address-first-vs-accept-first deadlock — the POC's one real discovery, and the composed variant's sequencing is now pinned.** SOCKS5 BIND is address-first (reply#1 must carry `BND.ADDR` *before* the app server can dial), while alktunnels' listen establisher is accept-first (its establishment phase pops an already-accepted connection — the open op blocks until one arrives). The first cut opened the listen-tunnel in the factory call (before reply#1) and deadlocked into the establishment timeout: the app server cannot dial until reply#1 tells it the address, and the open cannot complete until it dials. **The resolution: reply#1 first, the tunnel open rides the accept future** — the quiet wait is exactly where the establisher's accept-pop block belongs. What remains upstream-blocked is *learning the bound address without consuming an accept* (ledger ask #1): the stand-in is the factory's carried `bind_addr` (the expected `local_addr` the far-side real listener reports). Consequence for the ADR: variant 2 needs TWO upstream asks, not one — (1) listen-addr inspection without consuming an accept (the address half), and (2) accepted handles carrying peer metadata (the reply#2 half). Both are additive surface additions, not changes; the ask ledger below records both with the evidence. **Establishment-bound interaction (the composed variant's wait bound).** The accept future's tunnel open runs under the listen establisher's default 10s establishment bound — a BIND wait longer than that resolves as a refusal (reply#2, GeneralFailure via the mapped open error). Correct-by-backstop, but a real BIND deployment (FTP control sessions idle far longer than 10s) needs either a raised bound (the open-op's `timeout` parameter — a registration choice, no wire change) or the accept future structured so the bound covers only the open, not the wait (POC note: the bound covered the whole accept here; the crate's shape should bound only the open-op round trip). Phase 1 pins this with the registration posture. **The teardown-at-birth lesson repeats — and the fix is structural.** The composed variant's first working cut dropped the `TakenHalves::session` (the channel-entry owner) while destructuring it — the tunnel EOF'd at birth and the data plane read `early eof` (POC #2's findings predicted exactly this: "the session must live inside the table entry; the *compiler* should enforce it"). The fix: `BindTargetHandle` carries `_session: Option` — the owner rides the handle and drops with it. This is now the third witness of the pattern (POC #2's egress table, alktunnels ADR-005's teardown contract, here); the crate's Phase 1 shape should make the plan payload own its channel handles structurally, never by let-binding. **Peer-address fidelity is lost through the composed path** (ledger ask #2). The popped accept handle (`alktunnels::TargetHandle`) carries boxed halves only — the real peer address exists at the tunnel producer's listener; in-band the composed variant reports the sentinel (`0.0.0.0:0`) for reply#2 (the handle-swap reading applied to BIND: the reply is RFC-compatibility surface; the data plane is the channel). The local variant reports the real peer (no hop). Ledger item #2 is the fidelity ask. **The quiet wait is `select!`-single-shot, not a loop** (unlike ASSOCIATE's stage): the accept resolves at most once, and the control stream's first byte ends the session (garbage). A loop was unnecessary — clippy's `never_loop` caught the first cut's vestigial `loop`; the phase model's clean statement is that BIND's quiet wait has exactly two exits (the accept, or the control stream's EOF/garbage/error). The data plane starts only from the accept branch. **`BindAccept`'s future-ownership shape is the accept-side twin of the egress's readiness contract.** The accept future owns the listener (local variant) or the tunnel-open + popped halves (composed variant); the quiet wait's `select!` drops it on the control-stream branch — that drop IS the client-gone teardown. The crate's Phase 1 equivalent: the accept callback's future must own its resources end-to-end (the same ownership lesson as POC #2's egress, mirrored). ## Wire/API notes for the wrapper - fast-socks5's client `request()` reads exactly one reply — BIND's reply#2 is hand-read with fast-socks5's own public `read_address` codec (reply#1's codec applied again; no second codec hand-rolled). The consumer's reply#2 reader (`read_reply2`) is the Phase 1 client-session sketch. - `Socks5ServerProtocol::reply_success(sock_addr)` is reply#1's carrier — the `sock_addr` parameter carries `BND.ADDR` verbatim (the local variant's real listener; the composed variant's expected far-side address). No upstream change needed. - fast-socks5's `new_reply` helper is private; the BIND reply#2 writer hand-rolls the 10-byte reply (VER, REP, RSV, ATYP, addr, port — 10 bytes v4 / 22 v6). Ledger ask #3 (fast-socks5, courtesy): publish `new_reply` (or a `reply(code, addr)` constructor) so embedders don't re-encode replies. (2026-09-20: rides the inline vendor — see §ledger statuses.) - `ToTargetAddr::to_target_addr()` (not `From`) is the `SocketAddr → TargetAddr` conversion — `TargetAddr::from` does not exist (the POC hit this; the wrapper's accept layer will too). - The listen establisher's default 10s bound covers the composed variant's whole accept future in this POC's shape — see the establishment-bound finding above for the Phase 1 posture. - The scope gate is read from the `OpenHandler`'s `AuthContext` (CF-005/CF-006's per-call opener identity) — the same seam POC #1 validated for session-gating, now consumed for a *command-class* distinction. `socks5:bind` as a scope string follows the `socks5:open` convention (Phase 1 pins the exact convention). ## Upstream-ask ledger (the one-version alktunnels bump) **Ledger statuses (2026-09-20 — the graduation landed).** The user's standing note held: alktunnels 0.2.0 (ADRs 007/008, over alkcall 0.8.0) shipped in one version bump — but adopted the ssh `forwarded-tcpip`-shaped alternative over the per-ask carriers, so both asks are **superseded in shape**, not implemented as posed: 1. **Listen-addr inspection without consuming an accept — dissolved.** The bind-first establisher (`bindfirst_establisher`, ADR-008 §1) binds during establishment, so the open-op reply natively carries the real OS-chosen bound address (the `bound` reply field via alkcall 0.8.0's `Establishment::with_reply_field`, ADR-049 amendment 3; consumed via `open_channel_with_reply` / `TunnelListener::bound()`). There is no accept to avoid consuming; this ask's stand-in (the factory's carried `bind_addr`) is retired, as is the POC's "open rides the accept future" sequencing workaround — and the 10s establishment-bound finding dissolves with it (the accept wait lives in the pump phase now). The accept-first surface (`listen_establisher` + `AcceptQueue`) remains in 0.2.0 unchanged for the accept-as-open shape. 2. **Accepted handles carrying peer metadata — landed at the wire's native moment.** The forwarded op (`channels/tunnel/forwarded`, ADR-008 §3) carries `{listen, substrate, peer}` params per accept; `TargetHandle` gained no peer field — the graduation chose the forwarded-op shape over this item's option-i carrier. Correlation is a producer-minted u32 token (Amendment 1, 2026-09-18), not the originally-planned channel-ID key. 3. **(fast-socks5, courtesy)** `new_reply` is private — publish it (or a constructor) so embedders don't re-encode the reply wire form. Also still open from POC #4: the `net` feature gate and the `run_udp_proxy_custom` unconditional-bind seam. (Posture note, 2026-09-20: fast-socks5 is third-party upstream and these are no longer asks — all three move into the inline vendor, OQ-SK-04 resolved. 2026-09-21: under the extraction — ADR-011 §Amendment — `new_reply` is the one that rides in-tree; the `net` gate and the bind seam dissolve with the kernel-net region, which is not copied.) The POC's evidence stands as the record of what was validated against alktunnels 0.1.0; the Phase 1 shape composes variant 2 against 0.2.0 (`register_tunnel_bindfirst_openable` + `BindFirstInjections`). The user's standing note: collect alktunnels additions so one version bump ships them all. The POC ran against published alktunnels 0.1.0 (no fork needed — the listen surface is complete there), and the asks are additive surface, discovered in priority order: 1. **Listen-addr inspection without consuming an accept** (the address half of address-first-vs-accept-first). The plan carries `local_addr` + an inspect-only open, or a `listen/addr` query op (OQ-SK-08 variant 2's original blocker, now POC-evidenced: the factory's carried `bind_addr` stand-in is the exact shape the ask retires). Evidence: the composed accept's reply#1 advertises an *expected* address; without the ask, a producer whose listener binds an OS-chosen port cannot advertise it before the accept. 2. **Accepted handles carrying peer metadata** (the reply#2 half). `AcceptFn`'s `TargetHandle` carries boxed halves only; the real peer address exists at the tunnel producer's listener. Evidence: the composed accept's reply#2 reports the sentinel; the RFC's "reply#2 carries the address of the incoming connection" is unfilled on the composed path. Shape: `TargetHandle` gaining an optional `peer: Option` (or a sibling `AcceptedHandle`) — additive, no wire change. 3. **(fast-socks5, courtesy)** `new_reply` is private — publish it (or a constructor) so embedders don't re-encode the reply wire form. Also still open from POC #4: the `net` feature gate and the `run_udp_proxy_custom` unconditional-bind seam. (2026-09-21: under the extraction — ADR-011 §Amendment — `new_reply` is the one that rides in-tree; the `net` gate and the bind seam dissolve with the kernel-net region, which is not copied.) No asks block the POC's conclusions; 1–2 shape variant 2's *full* fidelity, and the POC's stand-ins (carried `bind_addr`; sentinel reply#2) are honest interim shapes. ## OQ status after this POC - **OQ-SK-08 (BIND)**: POC-validated end to end — the phase model extends (reply#1 → quiet wait → accept → reply#2 → pass-through); both accept variants implemented (local faithful + composed listen-tunnel); the scope gate (`socks5:bind`) distinguishes ingress from egress; the address-first-vs-accept-first question is *resolved in shape* (reply#1 first, the open rides the accept future) with the two upstream asks pinned as the remaining blockers for variant 2's full fidelity. Remaining for Phase 1: ship-in-base- crate + default-accept-variant ADR (the support is additive — old consumers still get the RFC refusal — so nothing here is a one-way door); the establishment-bound posture; the exact scope-string convention. - **OQ-SK-07**: #1, #2, #4, #5 complete; #3 dropped (transferred with OQ-SK-06). The register is closed for this crate's Phase 0. - **OQ-SK-01 (dial policy)**: the accept seam is the dial-callback shape's mirror (`BindAcceptFactory` — function-not-trait, boxed halves, one instance per session). The no-trait precedent now holds for the accept plane too. ## POC placement Standalone crate (global workspace, `/workspace/alksocks-bind-poc/`) per the placement conventions — it composes three published crates (alkcall 0.7.1, alktunnels 0.1.0, fast-socks5 1.0.0) and needs no code from this repo. Findings land here; the crate is the artifact.