Files
alksocks/tasks/implementation/door-udp.md
T
glm-5.3-flash 2413646b6a door: the UDP front door — the RFC codec boundary (ADR-003)
- src/door.rs (feature local): UdpFrontDoor — a sibling of the TCP
  door (one relay UDP socket = one association; no shared accept
  loop). Inbound datagrams decode via parse_udp_request at the door's
  codec boundary and ride in-band as {addr, data} frames; replies
  encode via new_udp_header back to the client's learned source
  address. FRAG != 0 refused (dropped; association healthy). The
  association's lifetime is the channel's — the relay socket closes
  with the pump and the teardown reaps the adopted channel. The
  client idle bound (ADR-014) is constructor config.
- Pump shape: the association's stream is split (one pump owns the
  in-band write half, the other the read half) — a shared-session
  mutex deadlocks (the channel read parks holding it while the socket
  side's send needs it); the pumps join via select!.
- tests/door-udp.rs: the POC #2 test-8 shape end to end (real UDP
  echo target + local::udp_egress + vanilla RFC-header'd datagram),
  the empty-datagram invariant (F-2), FRAG refusal + health, the idle
  bound + fresh-association health.

Verification: cargo test green (default + all-features + local),
clippy -D warnings clean (default, local, wasm32 target), fmt clean,
cargo doc clean.
2026-09-25 13:20:14 +00:00

7.2 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
implementation/door-udp Front doors — the UDP door (the RFC codec boundary) completed
implementation/door-tcp
narrow medium component implementation

Description

The consumer's UDP front door (feature local): a real UDP socket exposing an ASSOCIATE association to vanilla clients — the POC #2 test-8 shape. The RFC UDP-header codec lives ONLY at this boundary (ADR-003); the in-band wire carries {addr, data} frames.

Deliverables:

  1. src/door.rs additions (feature local) — the UDP door:
    • Bind a real UDP socket (the relay socket — the only place a real address exists; the vanilla client sendto()s it).
    • Open the association in-band via AssociateSession::associate (the reply's sentinel BND.ADDR is received and never used — the handle is the channel, ADR-003).
    • The datagram pump (two pumps, joined — the ADR-050 two-pump contract): socket→channel decodes the RFC header (parse_udp_request), FRAG ≠ 0 → refuse (drop the datagram; the v1 posture — fragmentation cannot be expressed in-band), sends {addr, data} in-band (send_datagram); channel→socket encodes the reply header (new_udp_header) and sends the RFC-header'd datagram back to the client's source address. Zero payload translation.
    • Association lifetime = channel lifetime (ADR-003): the door's association ends when the channel ends; the door's UDP socket closes with it. A per-client idle bound (no datagrams within a window → close the association) is the door-side defense (ADR-014) — constructor config, mirroring the TCP door's knobs.
    • The door's bind address is constructor config (consumer.md: "the UDP door's bind address is assembly-layer configuration").
    • Surface shape: follow the TCP door's builder (FrontDoor additions or a sibling UdpFrontDoor — decide at implementation against the landed door.rs shape and record in Notes; the spec leaves the exact constructor shape open).
  2. Tests (tests/door-udp.rs): the POC #2 test-8 shape end to end — a real UDP echo target, the producer with local::udp_ egress(), the door, a vanilla RFC-header'd datagram in → the echo back RFC-header'd (the vanilla client simulated at the RFC boundary); FRAG ≠ 0 refused (dropped, association stays healthy); the empty-datagram invariant through the door (a zero-length payload round-trips, never EOF — F-2); the idle bound closes an idle association; a fresh association succeeds after teardown.

Acceptance Criteria

  • cargo test passes; cargo test --all-features passes
  • cargo clippy --all-targets -- -D warnings clean; cargo fmt --check clean
  • Default wasm checks stay green (door code is local-gated)
  • The RFC UDP-header codec appears only at the door boundary (in-band stays {addr, data} frames) — pinned by test
  • FRAG ≠ 0 refused at the door — pinned by test
  • The empty datagram (zero-length payload) round-trips through the door, never EOF — pinned by test (the F-2 invariant at the vanilla boundary)
  • Association lifetime = channel lifetime; teardown leaves the connection healthy — pinned by test
  • The idle bound is constructor config — pinned by test

References

  • docs/architecture/decisions/003-udp-handle-swap.md (the RFC codec at doors only), 014-idle-timeout-posture.md
  • docs/architecture/consumer.md §The Front Doors (the UDP door), §Wiring
  • POC reference: /workspace/alksocks-udp-poc/tests/udp_poc.rs (udp_front_door_vanilla_client_round_trip — the pump shape)
  • Landed surfaces: src/consumer.rs (AssociateSession — send_datagram/recv_datagram/split), src/rfc1928/mod.rs (new_udp_header/parse_udp_request), src/wire.rs (the frame codec)

Notes

  • Surface shape (the spec left the constructor open): a sibling UdpFrontDoor with its own builder (bind + idle), not FrontDoor additions — the UDP door shares no accept loop with the TCP door (UDP has no connections; one relay socket = one association), and the shared shape would have forced a mode split on every knob. serve(channel_client) + serve_on(socket, client) (the test/assembly seam), mirroring the TCP door.
  • The pump shape (an implementation finding worth recording): the POC #2 shape (both pumps sharing one AssociateSession behind a mutex) deadlocks in this landed surface — the channel read parks holding the lock while the socket side's send needs it. The door instead splits the association's stream (AssociateSession::split — the door bridge seam): one pump owns the in-band write half (socket→channel, framing via the wire codec), the other the read half (channel→socket, the incremental DatagramReader decode). The two pumps join via select! (whichever ends first closes the door); the pump is inlined in serve_on because the split halves borrow the session (the borrow must end before the teardown's shutdown). The read half's decode carries a fresh per-call DatagramReader — the door serves one association per socket and the relay loop drains one frame per call; the mid-frame persistence case is the session's own recv_datagram contract (tested in consumer-associate).
  • The client's source address is learned from the first inbound datagram (std mutex, held never across an await) and reused for every reply — the relay's single-client shape. A reply before the first inbound datagram is parked (none exists in practice).
  • FRAG ≠ 0 drops the datagram and keeps the association (the v1 posture); a malformed RFC header also drops (garbage never goes in-band — the in-band refusal shape is the producer's UndecodableFrame exit, which the door never triggers by construction).
  • The idle bound covers the socket→channel pump's recv_from only (the client's silence is the defense target); the channel→socket pump has no window (its park IS the association's aliveness signal).

Summary

Landed the UDP front door (src/door.rs, feature local): UdpFrontDoor — a real relay UDP socket (constructor-config bind address) bridged onto one in-band association (AssociateSession::associate; the sentinel BND.ADDR received, never used). The RFC UDP-header codec appears only at the door boundary (parse_udp_request in / new_udp_header out); the in-band wire carries {addr, data} frames (framed once via the wire codec). FRAG ≠ 0 refused (dropped, association healthy); the empty-datagram invariant round-trips (F-2); association lifetime = channel lifetime (the door's socket closes with the pump; teardown reaps the channel); the client idle bound is constructor config and closes an idle association door-side. Two-pump shape with the split-halves correction recorded in Notes. Tests: tests/door-udp.rs (4 tests — the round-trip + empty datagram, FRAG refusal + health, idle bound + fresh-association health, fresh-association-after-teardown). Verification: cargo test green, cargo test --all-features green, clippy -D warnings clean (default + local + wasm target), cargo fmt --check clean, cargo doc --no-deps clean.