producer: the UDP ASSOCIATE datagram stage — the select!-driven relay (ADR-003)

- serve_udp_associate: sentinel reply (127.0.0.1:0 — the handle-swap
  surface) then the datagram stage; missing egress refuses
  RFC-faithfully (CommandNotSupported, ADR-008); egress-factory
  failure maps to reply#1's code
- run_datagram_stage: select! over channel reads ∥ egress replies
  (the concurrent full-duplex shape), typed StageExit diagnostics
  (EOF = the clean exit; undecodable frames fail loud; the egress
  readiness contract tolerated as spurious wakeups)
- wire: frame_inner — the reply-path half of framing-once (identical
  wire to frame(), pinned); egress inner bytes never re-prefixed
- tests: the readiness-contract test (instant-Ok(None) egress does
  not starve the stage), empty-payload datagram legal (never EOF),
  undecodable-frame loud fail, EOF teardown + fresh associate, the
  egress drop witness (stage owns it), missing-egress refusal +
  CONNECT guard — in-process (duplex) and over channels (harness);
  the three-command regression guard drives all commands serving

Verified: cargo test 174/174; clippy --all-targets -D warnings;
fmt --check; wasm check + clippy (wasm32-unknown-unknown); doc.
This commit is contained in:
glm-5.3-flash committed 2026-09-23 21:05:33 +00:00
1 parent 048493fefa
commit df772ccbeb
5 files changed
+1181 -54

No files matched your search

@@ -1,7 +1,7 @@
---
id: implementation/core-producer-associate
name: "Core — producer ASSOCIATE: the select!-driven datagram stage"
status: pending
status: completed
depends_on: [implementation/core-wire-codec, implementation/core-producer-connect]
scope: moderate
risk: medium
@@ -62,16 +62,16 @@ Deliverables (port from `/workspace/alksocks-udp-poc/src/producer.rs`
## Acceptance Criteria
- [ ] The datagram stage per the three POC invariants (concurrent
- [x] The datagram stage per the three POC invariants (concurrent
select, readiness contract, framing-once)
- [ ] Sentinel BND.ADDR in the reply; association lifetime = channel
- [x] Sentinel BND.ADDR in the reply; association lifetime = channel
lifetime (EOF teardown tested)
- [ ] Readiness-contract test present (instant-Ok(None) egress does
- [x] Readiness-contract test present (instant-Ok(None) egress does
not starve the stage)
- [ ] Missing-egress refusal RFC-faithful
- [ ] CONNECT regression guard passes (one registration, both
- [x] Missing-egress refusal RFC-faithful
- [x] CONNECT regression guard passes (one registration, both
commands)
- [ ] `cargo test` green; clippy/fmt clean; both wasm checks clean
- [x] `cargo test` green; clippy/fmt clean; both wasm checks clean
## References
@@ -86,8 +86,75 @@ Deliverables (port from `/workspace/alksocks-udp-poc/src/producer.rs`
## Notes
> To be filled by implementation agent
- **Wire codec addition** (`src/wire.rs`): `frame_inner()` — the
reply-direction half of the framing-once invariant (frames
pre-encoded inner bytes from the egress; the forward-path
`frame()` and the reply-path `frame_inner()` produce identical
wires — pinned byte-for-byte). Exported from lib.rs. `DatagramReader`
and `frame()` were already in place from the wire-codec task.
- **Stage shape** (`src/producer.rs`): `serve_udp_associate` drives
the typestate directly (POC #2's shape — no `run_udp_proxy_custom`),
then `run_datagram_stage` is a `select!` loop over channel reads ∥
`egress.next_reply()`, returning a typed `StageExit` diagnostic
(ChannelEof / UndecodableFrame / EgressEnded / ReplyFrameRejected /
Io). The egress `send` is awaited inline before the next select
iteration (the POC's measured interleaved shape); no
spawn-per-datagram, no flow table.
- **Exit semantics**: channel EOF is the clean exit (the association
lifetime); undecodable frames (garbage ATYP, `len == 0`) and
egress `Err` end the association loud; a reply the joint bound
rejects (`ReplyFrameRejected`) is its own exit (never a wire
overflow). The stage owns the egress structurally — the egress
binding is moved into the stage call; every exit drops it
(drop-witness test pins the teardown-at-birth discipline).
- **Missing-egress refusal**: `CommandNotSupported` (0x07) — the
ADR-008 missing-callback class, same shape as CONNECT's
missing-dial refusal. Egress-factory failure (post-command,
pre-reply) → reply#1 with the mapped RFC code
(`NetworkUnreachable` for the Unreachable variant), surfaced
in-stream as `SocksServerError::Bug("egress refused in-stream")`.
- The stale ASSOCIATE-refuses tests (`associate_arm_refuses_…`
in producer.rs, `producer_associate_refuses_0x07_…` in the
harness) were re-pinned to the missing-egress semantics; the
harness three-command regression guard now drives all three
commands serving (CONNECT + BIND + a datagram round-trip).
- The consumer side (the `AssociateSession` mirror) is the
consumer-associate task; its `send_datagram` will frame via
`frame()` and its `recv_datagram` will decode via the persistent
`DatagramReader` — both already exported.
## Summary
> To be filled on completion
The producer ASSOCIATE path is implemented and verified. The
dispatch arm in `serve_socks5_over` now branches to the datagram
stage: sentinel reply (`127.0.0.1:0`, ADR-003's RFC-compatibility
surface) → the `select!`-driven stage (channel reads ∥ egress
replies) with the three POC invariants as code:
1. **Concurrent select shape** — both branches of the loop are
awaited concurrently; no poll-then-block window (the reply-loss
hazard the POC measured).
2. **The egress readiness contract** — spurious `Ok(None)`s are
tolerated by re-parking; the contract test (`udp_instant_ok_none_egress_does_not_starve_the_stage`)
pins that an instantly-ready `Ok(None)` egress must not starve
the channel branch (the POC's load-bearing API finding,
re-pinned by this suite).
3. **Framing applied exactly once, by the wire layer** — the stage
reads frames via `DatagramReader` (egress `send` gets the bare
payload) and writes frames via the new `frame_inner` (egress
`next_reply`'s inner bytes → the channel); the egress never sees
a length prefix.
Association lifetime = channel lifetime (EOF is the clean exit;
tested in-process and over channels — a fresh associate succeeds
immediately after, the POC's test 4). Missing egress refuses
RFC-faithfully (0x07); egress-factory failure maps to reply#1's
code. CONNECT still serves on the same registration (regression
guards at both layers).
Verification: `cargo test` 174/174 (107 lib + 30 harness + 37
differential), `cargo clippy --all-targets -- -D warnings` clean,
`cargo fmt --check` clean, `cargo check --target
wasm32-unknown-unknown` + `cargo clippy --target
wasm32-unknown-unknown -- -D warnings` clean, `cargo doc --no-deps`
clean.