Files
alksocks/docs/architecture/decisions/004-dial-callback-shape.md
T
glm-5.3-flash 9587670138 docs: Phase 1 architecture — specs, ADRs 001-012, open-questions tracker
- docs/architecture/: README index, overview, wire, producer,
  consumer, vendor, bast specs + open-questions.md (the OQ-SK-01..09
  ledger promoted; new questions as OQ-SK2-06..)
- ADRs 001-012: ALPN/op/params, protocol base, the handle swap,
  the dial-callback shape, auth split by the door, access control
  (open gate + BIND command gate), BIND in base, provider defaults,
  feature gates, channel capacity, the inline vendor, target policy
- deferred OQs (SK2-06, SK2-07) with tracker tasks in
  tasks/architecture/ per the sdd_process deferred-OQ protocol
- review pass applied: frame-length joint bound (65766 corner),
  accept_no_auth vs skip_auth corrected against fast-socks5 source,
  composed-feature wasm posture, cross-reference fixes

Verified: cross-references (all ADR/OQ refs resolve), doc sizes
under split threshold, Phase 0 fidelity spot-checks
2026-09-20 18:41:56 +00:00

4.1 KiB

ADR-004: The Dial-Callback Shape — Function, not Trait, at Three Seams

Status

Accepted (2026-09-20, Phase 1; shape resolved empirically — 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 — satisfies 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 §Wiring and the module map in 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 §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)