- 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.
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 |
|
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:
src/door.rsadditions (featurelocal) — 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 (
FrontDooradditions or a siblingUdpFrontDoor— decide at implementation against the landeddoor.rsshape and record in Notes; the spec leaves the exact constructor shape open).
- Bind a real UDP socket (the relay socket — the only place a
real address exists; the vanilla client
- Tests (
tests/door-udp.rs): the POC #2 test-8 shape end to end — a real UDP echo target, the producer withlocal::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 testpasses;cargo test --all-featurespassescargo clippy --all-targets -- -D warningsclean;cargo fmt --checkclean- 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
UdpFrontDoorwith its own builder (bind+idle), notFrontDooradditions — 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
AssociateSessionbehind 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 incrementalDatagramReaderdecode). The two pumps join viaselect!(whichever ends first closes the door); the pump is inlined inserve_onbecause the split halves borrow the session (the borrow must end before the teardown'sshutdown). The read half's decode carries a fresh per-callDatagramReader— 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 ownrecv_datagramcontract (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
UndecodableFrameexit, which the door never triggers by construction). - The idle bound covers the socket→channel pump's
recv_fromonly (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.