--- id: tunnels/fix-udp-framed-adapter name: "U-1 remediation — make UdpHalf the FRAMED adapter (ADR-003 at the local boundary)" status: pending depends_on: [tunnels/review-impl] scope: moderate risk: medium impact: component level: implementation tags: [local, udp, codec, adr-003, review-remediation] --- ## Description Review 001 finding U-1 (`docs/reviews/001-implementation-review.md`): the `local` feature's `UdpHalf` (`src/local/mod.rs`) is NOT the framed adapter ADR-003 mandates at the substrate boundary — it is boundary-preserving (one datagram per poll_read/poll_write) but raw. The `[len: u16 BE]` codec exists only on the session side, so UDP-over-`local` tunnels: 1. deliver codec-framed bytes to real targets (the target sees `[len]payload`, not payload — executable-pinned in the review probes); 2. tear the tunnel down on an empty datagram (a raw zero-length datagram is `Ok(())` with 0 bytes filled = EOF-shaped to `tokio::io::copy` — the F-2 violation end-to-end; the session-side codec exists precisely to prevent this); 3. split datagrams > ~8 KiB at the target (`tokio::io::copy`'s `DEFAULT_BUF_SIZE` bounds each read; the raw adapter sends each chunk as its own datagram); 4. silently drop unframed target-initiated datagrams (the session codec parses a raw datagram's first two bytes as a length header). The session-side tests pass because the harness's `framed_udp_echo_dial` frames symmetrically at the test boundary — a correct-shape stand-in for what `connect_udp` itself must do. ### The fix Apply ADR-003's placement: `UdpHalf::poll_write` frames the caller's buffer (`frame_datagram`) and sends one datagram per framed frame; `poll_read` de-frames (feed `DatagramReader`, emit the next payload). Both halves of the association then speak the same framing; `len=0` becomes a 2-byte frame (unambiguous with EOF — the F-2 fix end-to-end). ### Verification gates (from the review) 1. A recording UDP echo server asserts the datagrams it receives are **payload bytes only** and an **empty datagram round-trips** (session `recv_datagram` yields `Some(b"")`, channel alive). 2. A >8 KiB datagram round-trips as ONE datagram at the target. 3. An unframed target-initiated datagram resolves at the session. 4. The existing `local_halves` suite stays green unchanged (its session-level round-trips are framing-symmetric today). 5. `cargo test` + `--features local` + wasm checks as usual; 3× repeat-run stable. ### Riders (from the same review, same file) - U-2: `connect_udp` binds `("0.0.0.0", 0)` — resolve the target family first (or document the v4-only posture). - U-3: size the recv scratch to the codec's MTU discipline instead of a per-tunnel 65535-byte allocation (or justify the bound). ## Acceptance Criteria - [ ] `UdpHalf` frames on write and de-frames on read (ADR-003's placement; the pump stays raw) - [ ] The 5 verification gates pass; suites green 3× in both configs - [ ] U-2/U-3 riders resolved or explicitly deferred with rationale - [ ] Review 001's U-1/U-2/U-3/N-1 sections get resolution notes ## References - `docs/reviews/001-implementation-review.md` §U-1 (executable-pinned probe evidence), §U-2, §U-3, §N-1 - `docs/architecture/decisions/003-codec-and-udp-framing.md` (the framed-adapter mandate + F-2), `docs/architecture/wire.md` §Datagram substrate - `src/local/mod.rs` (`UdpHalf`), `src/wire.rs` (the codec to compose)