Files
alksocks/tasks/implementation/core-producer-associate.md
T
glm-5.3-flash df772ccbeb 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.
2026-09-23 21:05:33 +00:00

7.8 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
implementation/core-producer-associate Core — producer ASSOCIATE: the select!-driven datagram stage completed
implementation/core-wire-codec
implementation/core-producer-connect
moderate medium component implementation

Description

Implement the UDP ASSOCIATE producer path in src/producer.rs per wire.md §UDP ASSOCIATE and ADR-003: replace the dispatch arm's refusal with the datagram stage (POC #2's validated scope).

Deliverables (port from /workspace/alksocks-udp-poc/src/producer.rs

  • egress.rs, adapted to the crate's wire codec and seam types):
  1. The ASSOCIATE arm: reply_success(sentinel) — the sentinel 127.0.0.1:0 BND.ADDR (pure RFC-compatibility surface; the channel is the relay handle, ADR-003) — then the datagram stage.
  2. The datagram stage — select!-driven (channel reads ∥ egress replies), with the POC's three load-bearing findings as invariants:
    • The concurrent select shape (not poll-then-block — the poll-then-block window loses replies; the concurrent shape is also the honest full-duplex shape).
    • The egress readiness contract: next_reply is Pending when nothing is ready — an instantly-ready Ok(None) starves a biased select into livelock. Write the contract test: an egress returning instant Ok(None) must not starve the stage's channel branch (the POC's load-bearing API finding, re-pinned by this suite per ADR-004's "re-pinned by the crate's test suite").
    • Framing applied exactly once, by the wire layer: the stage reads frames via DatagramReader (channel → egress send gets the payload) and writes frames via frame() (egress next_reply's inner bytes → the channel). The egress never sees a length prefix.
    • Association lifetime = channel lifetime: channel EOF ends the stage; no extra control vocabulary. A undecodable frame (garbage ATYP) fails the association loud (udp_undecodable_frame_ends_association).
  3. The EgressFactory lifecycle: called once per ASSOCIATE; the stage owns the egress; stage exit drops it. Ownership rides the handle structurally — no _ bindings of owning values (the POC's teardown-at-birth witness).
  4. Missing-egress refusal: a registration without an EgressFactory refuses ASSOCIATE RFC-faithfully (CommandNotSupported-class) — never a panic (ADR-008).
  5. Tests (via the harness + raw RFC client): associate → sentinel reply → datagram round-trip through the echo egress (in-order, multiple datagrams, one association); empty-payload datagram legal (not EOF); undecodable frame ends the association loud; channel EOF ends the association and the connection stays healthy (a fresh associate succeeds immediately after — the POC's test 4); the readiness-contract test; missing-egress refusal; CONNECT still served on the same registration (the regression guard).

Acceptance Criteria

  • The datagram stage per the three POC invariants (concurrent select, readiness contract, framing-once)
  • Sentinel BND.ADDR in the reply; association lifetime = channel lifetime (EOF teardown tested)
  • 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 commands)
  • cargo test green; clippy/fmt clean; both wasm checks clean

References

  • docs/architecture/wire.md §UDP ASSOCIATE (the normative stage)
  • docs/architecture/decisions/003-udp-handle-swap.md (the handle swap; the readiness + framing invariants)
  • POC sources: /workspace/alksocks-udp-poc/src/producer.rs (serve_udp_associate), /workspace/alksocks-udp-poc/src/ egress.rs (the readiness findings' origin)
  • docs/architecture/decisions/008-provider-defaults.md (the missing-callback posture)

Notes

  • 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

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.