Files
alktunnels/docs/architecture/consumer.md
T
glm-5.3-flash bd7d1ad8ec docs: Phase 1 architecture spec — ADRs 001..006, spec docs, OQ promotion
The Phase 0 OQ ledger (OQ-TN-01..10) promoted into
docs/architecture/open-questions.md (now OQ-TN-01..14, with four
new Phase 1 residues). Six ADRs and four spec docs, all Draft, all
decision-shaped per the SDD process (ADRs carry the WHY; specs carry
the WHAT and reference by number):

ADRs:
- 001 open-params-layout: params = {resource, substrate} — the
  producer's stable name, not an address; wire-stable from the first
  consumer; unknown substrates fail loudly at schema time
- 002 alpn-strategy: single alk/tunnel ALPN (option A); substrate in
  params selects the framing; prior art (SSH/SOCKS5/udpgw) gives no
  reason for the split
- 003 codec-and-udp-framing: raw pass-through (stream) / mandatory
  [len: u16 BE] (UDP) — F-2 mandate recorded (the codec is mandatory
  for correctness, not cosmetics); len=0 = legal empty datagram;
  OQ-TN-13 resolved fail-loud (truncation)
- 004 no-backend-trait: halves functions at the assembly layer;
  listen is an establisher shape; hub re-produce deferred(OQ-TN-12);
  trait re-evaluated at the alknet ADR-078 convergence threshold
- 005 consumer-session-owns-teardown: TunnelSession with
  open/adopt/stream_halves/take_halves/pump_against/close/join/Drop —
  the W3 adopter gap closes structurally; Drop is the best-effort
  fallback
- 006 access-control-posture: the open gate is the boundary;
  TUNNEL_OPEN_SCOPE = tunnel:open (stable once published); op-level
  ACL; ownership seam; no allowlists in v1; identity = 0.7.0's
  precedence chain

Spec docs:
- overview.md: purpose, resource model, deps (alkcall 0.7.0,
  wasm-clean default, local feature), module map
- wire.md: the open op (params/reply/typed errors), the data plane
  by substrate, sentinels + half-close, byte diagrams
- producer.md: spec, establisher (dial + listen shapes), pump
  handler (pump_bidi inline, R-02), registration API, ACL posture
- consumer.md: TunnelSession (forward open + reverse adopt
  construction), data plane, teardown API (close/join/Drop incl.
  pump-less join semantics)
- bast.md: the BAST doc for the UDP codec (convention 12's trigger
  fired — the framing IS binary beyond pass-through)
- open-questions.md: OQ-TN-01..14; 01..10 promoted (faithful to the
  phase-0 ledger's final states), 11 partially resolved (collision
  domain = per-producer registry per ADR-001; lifecycle open),
  12 deferred(scope), 13 resolved fail-loud (ADR-003), 14 open

Verified by an architecture-reviewer pass (2 criticals, 6 majors,
10 minors — all fixed: ADR-001/003-vs-OQ decision-state contradictions
resolved; the Layer-2 mislabel corrected to the ADR-047 §4 per-session
fork; alktty/alknet ADR misattributions fixed; codec placement pinned
to the establisher (pump stays substrate-agnostic); reverse-path
construction named (TunnelSession::adopt + pump_against); security
posture promoted to ADR-006; BAST doc written; F-2 rationale deduped;
README tables completed; channel_id > 0; MTU wording fixed; ADR-047 §5
attributions corrected; impacts lines added to unresolved OQs; review/
ledger pointer paths added to README references).

Verification: doc set internal cross-refs all resolve; cargo test,
fmt --check, doc --no-deps clean
2026-09-07 18:36:44 +00:00

6.9 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 channel BiStream → present the substrate-shaped data plane.
  • Errors: a failed open resolves a typed error — ChannelOpenError::CallFailed carrying the wire CallError; branch on establishment_reason() for channel: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, &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
  • open_reverse_channel — call the worker's open op on channel 0 (call_with_payload; optional auth_token), extract the worker-allocated channel_id. Caller identity rides the transport by default (alkcall 0.7.0 CF-005 (b): Connection::set_identity 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 open (an adopt failure is AdoptFailed-class, not an establishment error).
  • pump_against(accepted_halves) — spawn pump_bidi against 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() — raw AsyncRead/AsyncWrite halves; 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_datagram returns Some(bytes) per datagram — possibly empty (len=0 is a legal empty datagram) — and None only 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 of pump_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 (after take_halves, or a datagram session the caller drives directly): there is no session-owned pump — join completes immediately (nothing to await) and reaps only; the copy counts are (0, 0).
  • Drop — aborts the pump and sync-reaps the channel (teardown semantics of close, best-effort — Drop cannot await). Dropping without close/join never leaks the channel entry.
  • TunnelSession does not implement Clone. Aliasing a session would alias its teardown; multi-channel consumers hold a Vec<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_establisher whose 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: ReverseTunnel open_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)