- 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
87 lines
4.1 KiB
Markdown
87 lines
4.1 KiB
Markdown
# 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) |