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:
1 parent
aa7b5b7169
commit
8cf4cc8bb5
7 files changed
+765
-106
No files matched your search
@@ -46,8 +46,8 @@ are identical:
|
||||
```rust
|
||||
// Per local accept (assembly-owned listener):
|
||||
let channel_id = open_reverse_channel(&hub_call, ¶ms, 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]`
|
||||
|
||||
Reference in new issue
Block a user