Files
glm-5.3-flash e196ed43e3 docs: remove em dashes from repo prose and forbid them going forward
- Rewrote em dash prose across the whole tree outside docs/reviews/
  (the historical reviews stay verbatim, matching the alktunnels and
  alktty precedent): src doc comments and inline comments (587),
  docs/architecture (319), docs/research + sdd_process (484), tests
  (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77),
  AGENTS.md (85), and the .opencode/agents prompts (109)
- Fenced content follows the precedent: comments, ASCII-diagram label
  separators, and template placeholders rewritten; wire data, JSON/BAST
  documents, byte diagrams, and transcripts survive verbatim; assert,
  expect, panic, error-display, and format message literals rewritten
  as prose; no code, fixture, or test data change
- src/rfc1928/LICENSE: the two dashes in alksocks' own provenance
  header rewritten; the MIT legal text untouched
- AGENTS.md adds Project Convention 18: no em dashes in prose; varied
  restructure, no spaced-hyphen substitutes, fenced prose rule, en
  dash numeric ranges exempt, docs/reviews/ exempt from the greps
- tasks/*.md frontmatter: the name values rewritten by the sweep are
  now quoted (a raw colon inside an unquoted YAML scalar parsed as
  null); taskgraph validate reports 37 tasks, no issues

Verification: cargo test; cargo clippy --all-targets -D warnings;
cargo fmt --check; cargo doc --no-deps; fuzz corpus replay
(cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/ and the bast.md BAST
JSON data fence
2026-11-03 19:31:53 +00:00

143 lines
7.2 KiB
Markdown

---
id: implementation/door-udp
name: "Front doors: the UDP door (the RFC codec boundary)"
status: completed
depends_on: [implementation/door-tcp]
scope: narrow
risk: medium
impact: component
level: 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, as
the handle is the channel, ADR-003).
- **The datagram pump** (two pumps, joined, per the ADR-050
two-pump contract): socket→channel decodes the RFC header
(`parse_udp_request`), and **FRAG ≠ 0 → refuse** (drops the
datagram; the v1 posture, since 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`, because 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, since the relay's shape is single-client. 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.