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:
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.
|
||||
Reference in new issue
Block a user