consumer: AssociateSession — the framed datagram stage + e2e suite
The UDP ASSOCIATE consumer session (POC #2's shape, consumer.md §The associate session, ADR-003): - `AssociateSession::associate` — open → no-auth → ASSOCIATE → reply; the sentinel BND.ADDR is received (`relay_addr()`, RFC fidelity) and never used for addressing (the handle is the channel). The ASSOCIATE DST.ADDR is the vanilla-mirror unspecified address. - `send_datagram` — frames exactly once via the crate's wire `frame()` (the framing-once mirror; the u16 joint bound enforced at frame time). Takes `impl ToTargetAddr` (ADR-012 posture); a `ToTargetAddr for &TargetAddr` impl was added to the forked codec so a `TargetAddr` value survives across sends. - `recv_datagram` — the incremental `DatagramReader` + batched- datagram pending queue, state persisting across calls (the POC's reader-state finding); `Ok(None)` on channel EOF; fail-loud on a stream end mid-frame and on undecodable frames (`WireError` → the forked `AddrError`/`UdpHeaderError` RFC classes — the consumer mirror of the producer stage's `UndecodableFrame` exit). - `shutdown` — the half-close; the mux's implicit-EOF sentinel reaches the producer's stage as EOF (association lifetime = channel lifetime). `close()`/`Drop` own the teardown (sync reap + structural EOF, the CONNECT/BIND posture); teardown-at-birth on in-stream refusals; no `Clone` (compile-fail asserted). - Typed error surface: open failures are `SocksSessionError::Open` (ADR-049); the missing-egress 0x07 and factory-failure codes surface from `associate` as `Rfc`. E2E suite (`tests/consumer-associate.rs`, 8 tests over the harness): round-trips (multiple in-order datagrams, one association, sentinel relay address asserted), the split-datagram persistence test (a 60 KiB datagram reassembling across the call boundary + the pending- queue drain — both halves of the persistence invariant), the empty- payload round-trip (F-2, never EOF), shutdown → a fresh associate succeeds immediately (the POC's test 4), Drop teardown + connection health, the missing-egress refusal (typed 0x07, no phantom channel), the egress-factory failure mapping, and the CONNECT+ASSOCIATE regression guard (interleaved on one registration). Unit tests (4) pin the reader internals over a duplex (`skip_auth`'s handshake-free path). Verification: cargo test 305 (7 suites, all green), clippy --all-targets -D warnings, fmt, cargo doc --no-deps, and both wasm32-unknown-unknown checks clean.
This commit is contained in:
1 parent
776844e2fb
commit
c57451545d
5 files changed
+1095
-18
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: implementation/core-consumer-associate
|
||||
name: "Core — consumer ASSOCIATE: AssociateSession + e2e tests"
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [implementation/core-consumer-connect, implementation/core-producer-associate]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -51,16 +51,16 @@ adapted to the crate's wire codec):
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `AssociateSession` with `associate`/`send_datagram`/
|
||||
- [x] `AssociateSession` with `associate`/`send_datagram`/
|
||||
`recv_datagram`/`shutdown` per the POC shape
|
||||
- [ ] Reader state persists across `recv_datagram` calls (the
|
||||
- [x] Reader state persists across `recv_datagram` calls (the
|
||||
split-datagram test)
|
||||
- [ ] Framing-once mirror (send frames exactly once; recv decodes
|
||||
- [x] Framing-once mirror (send frames exactly once; recv decodes
|
||||
incrementally)
|
||||
- [ ] Teardown ownership on Drop; no `Clone` (compile-asserted)
|
||||
- [ ] E2E tests through the harness (round-trips, shutdown, Drop
|
||||
- [x] Teardown ownership on Drop; no `Clone` (compile-asserted)
|
||||
- [x] E2E tests through the harness (round-trips, shutdown, Drop
|
||||
teardown, regression guard)
|
||||
- [ ] `cargo test` green; clippy/fmt clean; both wasm checks clean
|
||||
- [x] `cargo test` green; clippy/fmt clean; both wasm checks clean
|
||||
|
||||
## References
|
||||
|
||||
@@ -72,8 +72,87 @@ adapted to the crate's wire codec):
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- **`send_datagram` takes `impl ToTargetAddr`** (the CONNECT
|
||||
session's target-construction posture — ADR-012: IP-string forms
|
||||
parse, domains pass through unresolved), not the POC's
|
||||
`&TargetAddr`: the same entry shape as `connect`. A
|
||||
`ToTargetAddr for &TargetAddr` impl was added to the forked
|
||||
address codec (our owned code, ADR-013) so callers can reuse a
|
||||
`TargetAddr` value across sends.
|
||||
- **Teardown ownership matches the CONNECT/BIND sessions** (the
|
||||
crate's posture, strengthened from the POC's "close/join"): the
|
||||
session owns the adopted channel entry — Drop sync-reaps
|
||||
(`teardown_channel`, best-effort; `Drop` cannot await) + the data
|
||||
plane EOFs structurally (the mux's implicit-EOF sentinel);
|
||||
`shutdown()` half-closes (the graceful path, returning the reap
|
||||
result); `close()` is the `shutdown`+reap alias-shaped variant
|
||||
kept for the connect/bind symmetry; teardown-at-birth covers the
|
||||
in-stream refusal path (a refused ASSOCIATE never leaves a
|
||||
phantom channel).
|
||||
- **`recv_datagram` returns `Result<Option<(TargetAddr, Bytes)>>`**
|
||||
(the POC's shape): `Ok(None)` = channel EOF (the association's
|
||||
end); a stream end mid-frame is fail-loud (an `UnexpectedEof`
|
||||
I/O error through `SocksSessionError::Rfc` — never a silent
|
||||
partial datagram); an undecodable frame (garbage ATYP) errors
|
||||
loud through the wire codec's `WireError` → RFC mapping
|
||||
(`AddrError` class for address-encoding refusals,
|
||||
`UdpHeaderError` for the garbage shapes — the consumer-side
|
||||
mirror of the producer stage's `UndecodableFrame` exit).
|
||||
- **The pending queue stores decoded datagrams** (`(TargetAddr,
|
||||
Bytes)`) — the POC stored pre-decode inner bytes; decoded storage
|
||||
keeps the delivery shape uniform (decode once per frame either
|
||||
way; no wire.rs change needed — `decode_inner` stays private).
|
||||
- **Unit tests ride a duplex-backed session** (`skip_auth`'s
|
||||
handshake-free `use_stream` path over a `BiStream::from_joined`
|
||||
duplex — no channels needed) for the reader-internals probes:
|
||||
reader-state persistence (the 2-byte-prefix chunk shape, the POC
|
||||
finding's trigger), mid-frame EOF fail-loud, garbage-ATYP fail-
|
||||
loud, and the wire-error mapping table.
|
||||
- **The e2e split-datagram test uses a 60 KiB payload** (60007
|
||||
inner bytes — legal under the u16 joint bound, well over the 16
|
||||
KB read buffer): first → big → third, in order, with the big
|
||||
datagram reassembling across the call boundary and the third
|
||||
drained through the pending queue — both halves of the
|
||||
persistence invariant pinned end to end.
|
||||
- **No harness changes**: the consumer-side topology's
|
||||
`wire_consumer_side_with` + the producer-associate task's
|
||||
`echo_egress`/`refusing_egress` closures cover every test.
|
||||
- The lib.rs re-exports gained `AssociateSession` +
|
||||
`TakenAssociateInner`; the forked `target_addr.rs` gained the
|
||||
`&TargetAddr` `ToTargetAddr` impl (ergonomics; owned code,
|
||||
ADR-013).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Implemented the UDP ASSOCIATE consumer session in `src/consumer.rs`
|
||||
(POC #2's shape, adapted to the crate's teardown-owning session
|
||||
posture): `AssociateSession::associate` (open → no-auth → ASSOCIATE
|
||||
→ reply, capturing the sentinel BND.ADDR as `relay_addr()` —
|
||||
received, never used for addressing; the ASSOCIATE DST.ADDR is the
|
||||
vanilla-mirror unspecified address), `send_datagram` (frames once
|
||||
via the crate's wire `frame()` — the framing-once mirror, the joint
|
||||
bound enforced at frame time), `recv_datagram` (the incremental
|
||||
`DatagramReader` + batched-datagram pending queue, **state that
|
||||
persists across calls** — the POC's reader-state finding, with the
|
||||
fail-loud shapes: mid-frame EOF and undecodable frames), `shutdown`
|
||||
(the half-close; the mux's implicit-EOF sentinel reaches the
|
||||
producer's stage as EOF — association lifetime = channel lifetime),
|
||||
plus `split`/`into_inner` accessors and the CONNECT sessions'
|
||||
teardown ownership (Drop sync-reap + structural EOF, `close()`
|
||||
graceful, teardown-at-birth on in-stream refusals, no `Clone` —
|
||||
`compile_fail`-asserted). Typed error surface throughout: open
|
||||
failures are `SocksSessionError::Open` (ADR-049 reasons); the
|
||||
missing-egress 0x07 and factory-failure codes surface from
|
||||
`associate` as `Rfc`. E2E suite (`tests/consumer-associate.rs`, 8
|
||||
tests over the harness): the round-trip (sentinel relay address →
|
||||
multiple in-order datagrams, one association), the split-datagram
|
||||
persistence test (60 KiB datagram across the call boundary +
|
||||
pending-queue drain), empty-payload round-trip (F-2, never EOF),
|
||||
shutdown → fresh associate succeeds immediately (the POC's test 4),
|
||||
Drop teardown + connection health, the missing-egress refusal
|
||||
(typed 0x07, no phantom channel), the egress-factory failure
|
||||
mapping, and the CONNECT+ASSOCIATE regression guard on one
|
||||
registration (interleaved, both serving). Verification: `cargo
|
||||
test` 305 (incl. the 3 doctests — the compile-fail assertions ride
|
||||
among them), clippy `-D warnings` (all targets), fmt, `cargo doc
|
||||
--no-deps`, and both wasm checks clean.
|
||||
Reference in new issue
Block a user