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.
7.1 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-09-07 |
alktunnels — Consumer Half
The consumer half: TunnelSession — the typed client for tunnel
channels. The consumer is whoever wants the bytes; it opens tunnel
channels toward the producer (forward path), or per local accept
toward a connect-side serving producer (the reverse flow). Both are
the same session type — role follows the resource, not the connection
(OQ-TN-03). Design decisions: ADR-005
(session owns teardown), ADR-003
(codec); this document is the normative WHAT.
Opening a Tunnel
Forward path (-L/direct — the consumer dials the producer's transport)
let session = TunnelSession::open(&channel_client, params).await?;
params:{resource, substrate}(ADR-001) — the produced resource- substrate discriminator.
- Flow:
ChannelClient::open_channel("channels/tunnel/sub", params, "alk/tunnel")→ adopt the returned channel ID → split the channelBiStream→ present the substrate-shaped data plane. - Errors: a failed open resolves a typed error —
ChannelOpenError::CallFailedcarrying the wireCallError; branch onestablishment_reason()forchannel:open_failed's reason (dial_failed / unknown_resource / resource_shortage / handler_error / timeout), or the pre-establishment codes (FORBIDDEN,channel:too_many_channels). A failed open never yields a session — no phantom session, mirroring the no-phantom-channel property (POC-verified).
Reverse path (-R — the consumer initiates toward a connect-side serving producer)
The hub/accept-side shape (the reverse POC's ReverseTunnel, the
seed of this path). Construction differs from the forward path only
in how the channel is obtained — the session type and teardown API
are identical:
// 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, 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 (call_with_payload; optionalauth_token), extract the worker-allocatedchannel_id. Caller identity rides the transport by default (alkcall 0.7.0 CF-005 (b):Connection::set_identitybefore dialing); the payloadauth_tokenis the optional hub-forwarding path (precedence: token >ServingConfig.identity> transport).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).adopttakes the same typed-error surface asopen(an adopt failure isAdoptFailed-class, not an establishment error).pump_against(accepted_halves)— spawnpump_bidiagainst the accepted local connection and hold the pump handle (ADR-005).
The session type and its teardown API are identical on both paths —
one API, -L and -R alike.
The Data Plane (substrate-shaped)
- Stream variant (
tcp,unix):stream_halves()— rawAsyncRead/AsyncWritehalves; the halves ARE the tunnel (raw pass-through, ADR-003). For owned access (spawning local pumps),take_halves()consumes the session's halves. - Datagram variant (
udp):send_datagram(&[u8])/recv_datagram() -> Option<Bytes>over the mandatory[len: u16 BE]codec (ADR-003).recv_datagramreturnsSome(bytes)per datagram — possibly empty (len=0is a legal empty datagram) — andNoneonly on stream EOF (the channels-level sentinel; the codec never collides with it, F-2). A >65535-byte send is rejected at frame time (Oversize).
Lifecycle and Teardown (the point of ADR-005)
One session = one channel = one tunnel. The session owns:
- The adopted channel entry — nothing upstream awaits the adopter's pump (the W3 gap); the session closes it structurally.
- The session-owned pump handle (when the session spawned one —
the reverse path's
pump_against).
Teardown API:
close(self)— abort the pump (if session-owned), tear down the adopted channel (ChannelManager::teardown_channel), consume the session. The ungraceful path.join(self)— await pump completion (both directions finished; half-close semantics fall out ofpump_bidi— one direction EOFs, the opposite sink shuts down, the other keeps pumping until its own EOF; W4-validated), THEN reap the adopted channel. Returns the(u64, u64)copy counts for observability. The graceful path. Pump-less sessions (aftertake_halves, or a datagram session the caller drives directly): there is no session-owned pump —joincompletes immediately (nothing to await) and reaps only; the copy counts are(0, 0).Drop— aborts the pump and sync-reaps the channel (teardown semantics ofclose, best-effort —Dropcannot await). Dropping without close/join never leaks the channel entry.TunnelSessiondoes not implementClone. Aliasing a session would alias its teardown; multi-channel consumers hold aVec<TunnelSession>(or the assembly layer does).
Out-of-band close
A peer-initiated channel/close (the generic channel op on the
producer's serving registry) tears the SERVING side down; the
consumer's session still owns its adopted entry — close/join reaps
it. The reverse POC pinned this split (the close ran on the worker;
the hub reaped its own entry, W3). EOF propagation is symmetric:
either side's teardown reaches the other as the channels-level
sentinel.
Both-Sides Sanity
The consumer and producer halves compose on one connection in both
roles (alkcall ADR-022 §2): a worker dials the hub (connect side),
serves its open ops (serving side), and its own outbound calls still
resolve while serving (reverse POC: worker_outbound_calls_still_
resolve_while_serving). The consumer half adds nothing to the
connection's serving posture — a pure consumer (from_connection)
never dispatches inbound requests; a reverse-flow initiator needs no
serving registry of its own.
Open Questions
- OQ-TN-12: hub re-produce composition (deferred — a hub proxy
re-exporting a consumed resource would add a
register_openable_with_establisherwhose establisher opens a consumer channel; no concrete consumer yet) - OQ-TN-13: UDP truncation (resolved — fail-loud, ADR-003)
References
- producer.md (the serving side's shapes), wire.md (the data plane)
- Reverse POC
docs/research/reverse-poc-summary.md(the seed shape:ReverseTunnelopen_and_pump / close / join_and_reap; W3, W4) - alkcall ADR-047 §5 (allocation — the initiator adopts), ADR-050
(
pump_bidi), ledger CF-005/CF-006 (identity on the reverse path)