--- status: draft last_updated: 2026-09-07 --- # alktunnels — Overview Arbitrary bidirectional tunnels over alkcall channels: TCP, UDP, unix sockets, and other stream or datagram substrates — in the `ssh -L` / `ssh -D` / `ssh -R` sense. A producer/consumer protocol crate riding alkcall channels the same way alktty does (`alk/tty` is the sibling precedent). This document covers the crate's purpose, the resource model in brief, dependencies, the ALPN, and the module map; component details are in the sibling documents ([wire.md](wire.md), [producer.md](producer.md), [consumer.md](consumer.md)). ## What `alktunnels` registers the `alk/tunnel` ALPN's open op on alkcall channels and provides the typed consumer session: - **The producer half** (a `channels/tunnel/sub` open op registered via `ChannelCore::register_openable_with_establisher`, alkcall ADR-047/049): the establisher validates params semantically (registry lookup of `{resource, substrate}` → backing), dials (or accepts for) the substrate, and returns the dialed handle via `Establishment::new(plan)`; the pump handler awaits `alkcall::channels::pump_bidi` over the channel and the substrate halves (ADR-050, R-02's lifetime contract). - **The consumer half** (`TunnelSession`, ADR-005): opens tunnel channels (`ChannelClient::open_channel` on the forward path; the reverse path's call + adopt equivalent), splits the channel `BiStream`, presents the substrate-shaped data plane (raw halves or datagram codec), and owns teardown (close/join/Drop). The guiding insight (the OQ-TN-01 reframe): > A tunnel is a resource, not an address. `params` identify a produced > resource + substrate; the producer owns the backing. Rich addressing > (SOCKS5 ATYP, per-datagram remotes) enters only through the > `-D`/dynamic composition path — inside the tunnel payload, never in > the wire. ## Why The crate's purpose is to be the tunnel library for downstream consumers, the same role alktty plays for terminal sessions: - A hub that runs agent workspaces exposes workers' services (postgres, redis, gitea HTTP) as named resources; a coordinator opens tunnels to them over alkcall channels — the VPN/SSH-tunnel service pattern without SSH. - A worker behind NAT dials the hub and serves its own open ops (the reverse flow, `ssh -R`); the hub opens tunnel channels toward it per local accept. Both POCs validated this end-to-end. - A `-D`-style consumer tunnels a SOCKS5 connection; the socks5 server (assembly layer) does dynamic target selection inside the tunnel payload. - The protocol layer is substrate-agnostic by construction — the bookkeeping (target addressing, direction, lifecycle) never hardcodes a substrate (AGENTS.md convention 7). TCP, UDP, unix sockets, and in-process pipes ride the same open op, pump shape, and session type. ## The Resource Model in Brief A tunnel channel's life (full detail in [wire.md](wire.md)): 1. **Open (the establishment phase).** The consumer calls `channels/tunnel/sub` with `params = {resource, substrate}` (ADR-001). The producer's establisher — the open op's awaited phase (alkcall ADR-049) — resolves the resource, dials the substrate, and returns the handle in the plan. Failure is a typed `channel:open_failed` (`dial_failed` / `unknown_resource` / `resource_shortage` / `handler_error` / `timeout`); the channel never exists on the opener's side afterward. 2. **Pump (the data plane).** Two pumps, one per direction, via `pump_bidi` (ADR-050): each shuts the opposite sink down on completion; half-close semantics fall out. Stream substrates ride raw pass-through (0 B tunnel overhead); UDP rides the mandatory `[len: u16 BE]` codec (ADR-003, F-2). 3. **Teardown.** The serving side is wrapper-managed (R-02); the adopting side's `TunnelSession` owns teardown (ADR-005, W3). EOF sentinels are the channels layer's; the codec never collides with them. Direction is not in the protocol: whoever can reach the target is the producer; whoever wants the bytes is the consumer; connection direction (who dialed the transport) is independent of tunnel direction (OQ-TN-03's hub model). `-L`, `-R`, and `-D` are assembly shapes over the same open op. ## Dependencies ``` alktunnels (default — wasm-clean) ├── alkcall 0.7.0 (core types, channels: ChannelCore/ChannelClient/manager, │ pump_bidi, ServingConfig identity seam — CF-005/006) ├── tokio (wasm-clean subset: rt, sync, io-util, macros, time) ├── bytes, futures, serde/serde_json, thiserror, tracing, async-trait └── (no backend deps — sockets/process live in feature-gated modules) alktunnels (local feature) — non-wasm by design └── adds: tokio/net (TCP dial, UDP bind/connect, unix sockets — unix pending OQ-TN-14) ``` Same posture as alktty: the default crate is protocol-only and wasm-clean (AGENTS.md convention 4); substrate I/O is feature-gated (ADR-004). The wire format carries no alkcall-internal types (alktty ADR-006's self-containment rule applies to the tunnel codec — trivial here, since the codec is a 2-byte length prefix). ## Feature Gates | Feature | Contents | Wasm | |---------|----------|------| | *(default)* | params, wire codec, open-op spec, establisher shapes, `TunnelSession` — protocol only | yes | | `local` | TCP/UDP/unix dial + listen helpers — halves-producing dial/listen functions (ADR-004; unix pending OQ-TN-14) | no | ## Module Map ``` src/ lib.rs — re-exports; crate docs params.rs — TunnelParams {resource, substrate} + open-op input schema (ADR-001) wire.rs — the data-plane codec: frame_datagram / DatagramReader (ADR-003) producer.rs — the open op: tunnel_open_spec (scope-gated), establisher shapes (dial + listen), pump handler (pump_bidi), register_tunnel_openable (ADR-004 shapes) consumer.rs — TunnelSession: open / adopt / stream_halves / take_halves / send_datagram / recv_datagram / pump_against / close / join / Drop (ADR-005) error.rs — TunnelError + the typed open-error surface (ADR-049 §4) local/ — the `local` feature module: real socket halves functions ``` Public API surface is `lib.rs` re-exports (AGENTS.md convention 16). ## Design Decisions All design decisions are documented as ADRs in [decisions/](decisions/). | ADR | Decision | Summary | |-----|----------|---------| | [001](decisions/001-open-params-layout.md) | Open-op params: resource + substrate | `{resource, substrate}` — the producer's stable name, not an address; wire-stable from the first consumer | | [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN | Substrate in params selects the framing; no ALPN split, no API bifurcation | | [003](decisions/003-codec-and-udp-framing.md) | Data-plane codec | Raw pass-through (stream) / mandatory `[len: u16 BE]` (UDP — F-2 mandate); `len=0` is a legal empty datagram | | [004](decisions/004-no-backend-trait.md) | No TunnelBackend trait | Halves functions at the assembly layer; listen is an establisher shape; trait re-evaluated at the 3-consumer threshold | | [005](decisions/005-consumer-session-owns-teardown.md) | Consumer session owns teardown | `TunnelSession` with close/join/Drop — the W3 gap closes structurally | | [006](decisions/006-access-control-posture.md) | Access-control posture | The open gate is the boundary: `tunnel:open` scope (stable once published), op-level ACL, ownership seam, no allowlists in v1 | ## Open Questions Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this document: - **OQ-TN-11**: resource naming collision domain + lifecycle (partially resolved) - **OQ-TN-12**: hub re-produce composition helper (deferred(scope)) - **OQ-TN-13**: UDP truncation semantics (resolved — fail-loud, ADR-003) - **OQ-TN-14**: unix/stdio substrate placement (open) ## References - Phase 0: `docs/research/` (findings, both POC summaries, survey) - Upstream ADRs: see [README.md](README.md) §References - Sibling: alktty (`/workspace/@alkdev/alktty/docs/architecture/`)