fix: UdpHalf is the FRAMED adapter (U-1) — U-2 bind posture, U-3 lazy scratch

Review 001 remediation (tasks/tunnels/fix-udp-framed-adapter):

- U-1 [major]: UdpHalf now composes the ADR-003 codec at the
  substrate boundary — poll_write de-frames channel bytes (incremental
  DatagramReader) and sends each payload as one raw datagram;
  poll_read frames raw target datagrams and hands frame bytes to the
  pump. Targets see payload-only bytes; empty datagrams round-trip
  (the F-2 fix end-to-end); >8 KiB datagrams arrive as ONE datagram.
  Mechanics the verification gates forced beyond the sketch:
  poll_write accepts all offered bytes (tokio copy re-offers a full
  window unsatisfied — partial-frame holds deadlock >8 KiB frames),
  backpressure is an accepted-byte high-water mark, socket drain
  pumps on write/flush/shutdown; >caller-buffer frames stage across
  reads (the truncation probe's assertion surface updated per the
  re-validation correction).
- U-2 [minor]: connect_udp resolves the target and binds the
  unspecified address of the matching family (v6 targets dial from
  [::]:0); bind_tcp gains the wildcard-exposure doc warning.
- U-3 [minor]: the 65535 recv scratch allocates lazily on first recv
  (full-size read kept — try_recv truncates silently; the MTU-shrink
  idea confirmed unsafe in re-validation).
- N-1: the module/connect_udp/UdpHalf docs now describe the composed
  codec truthfully.

Gates (permanent tests in tests/local_halves.rs): recording-target
suite (payload-only at the target, empty-datagram round-trip, 9000-
byte single datagram, unframed-target resolution), the 65507-byte
max-datagram staging probe, and the v6 dial gate.

Docs: ADR-003 §Amendment (the boundary mechanics), wire.md truncation
bullet, consumer.md adopt sketch (substrate param). Review 001
resolution notes on U-1/U-2/U-3/N-1; task marked completed.

Verified: 63 default / 76 local tests ×3 repeat runs; clippy
-D warnings (all-targets + wasm32), fmt --check, doc --no-deps
(warning-free), wasm check, publish dry-run.
This commit is contained in:
glm-5.3-flash committed 2026-09-08 21:22:52 +00:00
1 parent aa7b5b7169
commit 8cf4cc8bb5
7 files changed
+765 -106

No files matched your search

+9 -7
View File
@@ -46,8 +46,8 @@ are identical:
```rust
// Per local accept (assembly-owned listener):
let channel_id = open_reverse_channel(&hub_call, &params, auth_token).await?;
let session = TunnelSession::adopt(hub_manager, channel_id, TUNNEL_ALPN).await?;
let pump_handle = session.pump_against(accepted_halves).await; // or drive the halves directly
let session = TunnelSession::adopt(hub_manager, channel_id, substrate, TUNNEL_ALPN).await?;
let pumped = session.pump_against(accepted_halves).await; // or drive the halves directly
```
- `open_reverse_channel` — call the worker's open op on channel 0
@@ -57,11 +57,13 @@ let pump_handle = session.pump_against(accepted_halves).await; // or drive the h
before dialing); the payload `auth_token` is the optional
hub-forwarding path (precedence: token > `ServingConfig.identity` >
transport).
- `TunnelSession::adopt(manager, channel_id, alpn)` — adopt the
worker-allocated ID (connection-owner rule, ADR-047 §5 — the
SERVING side allocates; the initiator adopts; early-arrival parking
covers the adoption race), install the session's data plane from
the adopted halves. `adopt` takes the same typed-error surface as
- `TunnelSession::adopt(manager, channel_id, substrate, alpn)` —
adopt the worker-allocated ID (connection-owner rule, ADR-047 §5 —
the SERVING side allocates; the initiator adopts; early-arrival
parking covers the adoption race), install the session's data plane
from the adopted halves. The substrate parameter is required — a
datagram session cannot exist without it (the data plane variant is
substrate-shaped). `adopt` takes the same typed-error surface as
`open` (an adopt failure is `AdoptFailed`-class, not an
establishment error).
- `pump_against(accepted_halves)` — spawn `pump_bidi` against the
@@ -3,7 +3,12 @@
## Status
Accepted (2026-09-07; supersedes the Phase 0 "codec direction" hunch
by making the UDP framing mandatory — reverse POC finding F-2)
by making the UDP framing mandatory — reverse POC finding F-2).
Amended (2026-09-08, review 001 U-1 remediation): the substrate-
boundary placement is pinned mechanically — the `local` feature's
`UdpHalf` composes the codec at the boundary (channel side framed,
target side raw), with the pump-window and recv-scratch disciplines
the composition requires. See §Amendment (2026-09-08).
## Context
@@ -77,6 +82,55 @@ The data plane inside a `alk/tunnel` channel is:
## Consequences
### Amendment (2026-09-08 — the boundary mechanics, from review 001 U-1's remediation)
The placement sentence ("the establisher wraps the dialed UDP socket
in the framed adapter before returning the plan") leaves the adapter's
*mechanics* open; review 001 found `UdpHalf` shipping as a raw
boundary-preserving adapter (the codec on one end only — real targets
saw the tunnel's length prefixes; empty datagrams collapsed to EOF at
the pump's zero-read check; >8 KiB datagrams split at the copy
buffer). The remediation pins the mechanics:
- **Orientation is channel-relative.** The adapter's caller is the
pump; the CHANNEL side is the framed side. `poll_write` (fed
channel bytes) **de-frames** (an incremental `DatagramReader`) and
sends each decoded payload as one raw datagram; `poll_read` **frames**
(`frame_datagram`) each raw target datagram and hands frame bytes to
the pump. "Frame on write, de-frame on read" describes the TARGET's
face and is the inverted (wrong) reading — the review's original
fix sketch had it inverted and was corrected in re-validation.
- **The write half accepts all offered bytes and reports them
consumed.** The pump's copy engine re-offers a *full* window
unsatisfied until it drains (tokio `copy`'s `read_done` path never
tops the window up), so a partial-frame tail could sit forever in a
full window if the adapter accepted only complete frames — the
>8 KiB frame would deadlock the pump. Completed datagrams queue for
the socket; a trailing partial frame waits in the `DatagramReader`
(its state is bounded by construction — the u16 cap, review 001
N-2). Backpressure is an accepted-byte high-water mark on
`poll_write`, and the socket drain pumps opportunistically on
write/flush/shutdown (tokio's copy polls flush after every window
drain).
- **The recv scratch is full-size (65535) and lazy.** `try_recv`
truncates SILENTLY at the buffer size — a smaller scratch corrupts
the framing before any size check could reject it (the review's
"size to the MTU" suggestion is unsafe as written). The 64 KiB
eager allocation is instead lazy (first recv), the U-3 mitigation.
- **A >caller-buffer frame is staged, not an error.** The frame's
length prefix reaches the pump on the first read; the remainder
hands out across reads. The pump's far-side de-framer reassembles
the true length — the raw adapter's read-level `InvalidData` is
replaced by staging (the fail-loud posture now protects the *frame
stream*, and a wire-level over-bound datagram remains a loud
`Oversize`-class error at the frame path).
The v1 verification gates (review 001 §Verification gates) pin these
end-to-end over real sockets in `tests/local_halves.rs` (the
recording-target suite).
## Consequences (original)
- **TCP/unix tunnels have 0 B tunnel overhead** — the SSH
`direct-tcpip` experience (the payload is the bytes).
- **UDP correctness requires the codec** — raw pass-through over UDP
+8 -3
View File
@@ -111,9 +111,14 @@ the codec:
payload, not the theoretical 1472 max) ride the bounded-buffer path;
the 64-parked-chunks bound (alkcall early-arrival cap) is upstream
sizing, not tunnel policy.
- **Truncation:** a receive buffer smaller than the datagram fails
loudly at the adapter level (OQ-TN-13, resolved fail-loud per
ADR-003); silent truncation would corrupt the framing invariants.
- **Truncation:** silent truncation never happens (OQ-TN-13, resolved
fail-loud per ADR-003): the `local` adapter recvs on a full
65535-byte scratch (UDP `try_recv` truncates silently at the buffer
size — a smaller scratch would corrupt the framing) and a
>caller-buffer frame is staged across reads, the length prefix
intact, so the far-side de-framer reassembles the true length. A
wire-level over-bound datagram (>65535 — impossible for a
well-formed peer) is a loud error at the frame path.
### Sentinels and lifecycle (both codecs)
+52
View File
@@ -12,6 +12,7 @@ consolidated here per the review-impl deliverable).
**U-1 is load-bearing for the UDP wire promise and is NOT yet fixed** —
see the remediation section (it owns the remaining remediation task).
**FIXED 2026-09-08** — see §Remediation resolution below.
**C-1 (`AcceptQueue` lost wakeup) is fixed with this review's commit**,
including its regression test. The trivial minors (N-4, N-5, N-7, N-8,
N-9, N-10) are fixed with the same commit — the review's "trivial
@@ -263,6 +264,30 @@ the codec path.
**Remediation task:** `tasks/tunnels/fix-udp-framed-adapter.md`
(created with this review).
**RESOLUTION (2026-09-08, remediation commit):** `UdpHalf` is the
framed adapter — de-frames on `poll_write` (channel bytes → raw
datagrams, via an incremental `DatagramReader`), frames on `poll_read`
(raw datagrams → channel frames, via `frame_datagram`). All five
verification gates landed as permanent tests in `tests/local_halves.rs`
(the recording-target suite: payload-only at the target, empty
round-trip, 9000-byte single datagram, unframed-target resolution,
staging-not-truncation at 65507) plus a v6-dial gate for U-2. Two
mechanics the gates forced beyond the sketch (pinned in ADR-003's
amendment):
1. The write half ACCEPTS ALL offered bytes (reports them consumed)
and queues completed datagrams for the socket — tokio `copy`
re-offers a FULL window unsatisfied until drained, so accepting
only complete frames deadlocks a >8 KiB frame (its tail sits in a
full window forever). Backpressure is an accepted-byte high-water
mark; the socket drain pumps on write/flush/shutdown (copy polls
flush after every window).
2. A >caller-buffer frame is STAGED across reads (length prefix
first) rather than the raw adapter's read-level `InvalidData` —
gate 4's predicted assertion-surface change; the 65507-byte
max-datagram round-trip through 16 KiB reads is the new fail-loud
probe (silent truncation would still corrupt the framing).
## U-2 [minor] — `connect_udp` binds `0.0.0.0` unconditionally — no v6 or dual-stack posture
`src/local/mod.rs:116`: `UdpSocket::bind(("0.0.0.0", 0))`. A UDP
@@ -306,6 +331,14 @@ The rule the remediation implements:
Recorded here so the posture survives the implementation; the
executable rider lives on `tunnels/fix-udp-framed-adapter.md` §Riders.
**RESOLUTION (2026-09-08, remediation commit):** `connect_udp`
resolves the target first and binds the UNSPECIFIED address of the
matching family (`0.0.0.0:0` for v4 targets, `[::]:0` for v6 — no
implicit wildcard mismatch, no loopback default); no bind-address
parameter added. `bind_tcp` gained the wildcard-exposure doc warning.
Pinned by `connect_udp_binds_the_matching_family_v6_targets_dial` (a
real `::1` echo round-trip through the full path).
## U-3 [minor] — `UdpHalf` recv scratch allocates 65535 bytes per read half
`src/local/mod.rs:164`: `recv_scratch: vec![0u8; 65535]` — a 64 KiB
@@ -318,6 +351,13 @@ the codec's MTU discipline (1400) with an oversized-datagram
the fail-loud check (a >MTU datagram currently *is* received then
error-matched against the caller buffer). Fold into the U-1 task.
**RESOLUTION (2026-09-08, remediation commit):** the scratch is
allocated LAZILY on the first recv (the re-validation's mitigation —
the MTU-shrink idea was unsafe as written; the full-size read is
correctness-load-bearing on the de-frame path). The write half
carries no scratch. Memory posture: one 64 KiB allocation per
*receiving* UDP tunnel, on first recv.
## C-1 [major] — `AcceptQueue::pop` can miss the final `push` (lost wakeup in the check→register window) — FIXED in-review
**Status: FIXED during the review** (commit with this doc).
@@ -374,6 +414,11 @@ Filed separately from U-1 because even if U-1's fix is deferred, the
docs currently describe behavior that does not exist — a conformance
review must not leave that standing.
**RESOLUTION (2026-09-08, remediation commit):** U-1's fix landed —
the docs are now true. The module header, `connect_udp`, and
`UdpHalf` docs describe the composed codec (channel side framed,
target side raw) with the ADR-003 amendment's mechanics.
## N-2 — NON-FINDING (retracted during the review): `DatagramReader` buffering is bounded by construction
Filed as a suspect ("a lying `len` header buffers without bound"),
@@ -588,6 +633,13 @@ U-1 remediation docs.
# Verification gates for the U-1 remediation
(All five landed 2026-09-08 as permanent tests in
`tests/local_halves.rs` — see §U-1's resolution note. Gates 1–3 are
the `gate_*` recording-target suite; gate 4's truncation-surface
update is `truncation_fails_loud_oq_tn_13` (rewritten for the staged
frame) + `oversized_frame_stages_across_reads`; gate 5 is the standing
battery.)
1. A real-socket probe test (the review's probe shape, made permanent
in `tests/local_halves.rs`): a recording UDP echo server asserts
the datagrams it receives are **payload bytes only** (no `[len]`