# ADR-004: The Dial-Callback Shape: Function, not Trait, at Three Seams ## Status Accepted (2026-09-20, Phase 1; shape resolved empirically, with POC #1 (2026-09-13), POC #2 (2026-09-14), POC #5 (2026-09-15)) ## Context The SOCKS5 protocol must be substrate-agnostic (AGENTS.md convention 7): the far side of a session may be a kernel socket, an alktunnels channel, or any other byte/datagram source. The question is the inversion point for the three I/O planes a SOCKS5 server touches: the CONNECT dial, the ASSOCIATE datagram egress, and the BIND accept. alktunnels resolved the same question for its establisher with the no-trait finding (OQ-TN-05 → alktunnels ADR-004): "produce boxed halves for a resource" is a function, not a trait; two real implementations existed as callbacks, and the trait added nothing. The POCs extended that finding to all three SOCKS5 seams: - **CONNECT**: the dial cannot live in the establisher; the target is unknown at open time (it arrives in-stream at command read). The injected `DialFn(target) -> TargetHandle {read, write}` closure is called at the command-read interception point (`poc-connect-wasm-findings.md` §Structural findings). Per-target refusal is an RFC reply code; session-level refusal stays channels-level. - **ASSOCIATE**: the `EgressFactory`, called once per association; the datagram stage owns the egress; stage exit drops it (`poc-udp-associate-findings.md` §The per-association egress instance). - **BIND**: the `BindAcceptFactory`, one instance per session; its future owns the listener/tunnel and is dropped by the quiet wait's `select!` on the control-stream branch (the client-gone teardown; `poc-bind-findings.md` §The quiet wait is select!-single-shot). ## Decision - **No dialer/egress/accept trait.** All three seams are function-shaped callbacks injected at registration/assembly: - `DialFn(target) -> TargetHandle` (boxed halves, `+ Sync`, satisfying the `ChannelPlan` payload bound; the bound binds handle/plan payloads downstream; the SOCKS5 establisher itself carries no plan, `Establishment::default()`) - `EgressFactory() -> impl Egress` (per-association instance; `next_reply` readiness contract: `Pending` when nothing is ready) - `BindAcceptFactory` (per-session; the future owns its resources end-to-end, the ownership-rides-the-handle lesson) - **Ownership rides the handle structurally.** Plan/handle payloads own their channel sessions and socket halves inside the payload (the third witness of the teardown-at-birth pattern: POC #2's egress table, alktunnels ADR-005, POC #5's `BindTargetHandle`). The compiler should enforce it: no `_` bindings of owning values. - **The trait re-evaluation threshold is the same as alktunnels ADR-004's**: revisit if three real implementations exist as trait impls in practice. Evidence so far: local-socket and composed providers are callbacks; the no-trait hunch is evidence-backed at all three seams. ## Consequences - The protocol layer never names a substrate type. Substrate-specific providers live in feature-gated backend modules and are wired at the assembly layer (see [producer.md](../producer.md) §Wiring and the module map in [overview.md](../overview.md)). - The composed providers (alktunnels direct-op egress, bind-first listen accept) are assembly-layer compositions, not crate dependencies of the protocol layer; the protocol crate depends on alkcall only (see [overview.md](../overview.md) §Dependencies; the composed-provider helpers that reference alktunnels types live behind the composition feature, ADR-009). - The seams' contracts (readiness, ownership, refusal mapping) are load-bearing API surface, pinned by the POC findings and re-pinned by the crate's test suite. ## References - OQ-SK-01 (the dial policy, the A/B/C options; this ADR closes the dialer-trait-vs-callback half), OQ-SK-03 (the egress seam), OQ-SK-08 (the accept seam) - POC #1/#2/#5 findings (the three seams' empirical records) - alktunnels ADR-004 (the no-trait precedent), alknet ADR-078 / alkcall ADR-050 (the two-pump contract the handles feed)