--- status: draft last_updated: 2026-09-18 (the graduation: the direct op, listen metadata, the forwarded op; ADR-007/008 as amended: the correlation token) --- # alktunnels: Overview Arbitrary bidirectional tunnels over alkcall channels: TCP, UDP, unix sockets, and other stream or datagram substrates, all 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 ops on alkcall channels and provides the typed consumer sessions: - **The producer half** (open ops registered via `ChannelCore::register_openable_with_establisher`, alkcall ADR-047/049): the establisher validates params semantically, then dials (or binds/accepts for) the substrate, and returns the 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). Three ops on the one ALPN (ADR-002 Amendment 1): `channels/tunnel/sub` (the resource op), `channels/tunnel/direct` (dynamic-target egress, ADR-007), and `channels/tunnel/forwarded` (served by the listen-opener side; accept-as-open, ADR-008). Three establisher shapes: dial, listen (accept-first), and bind-first listen. - **The consumer half** (`TunnelSession` / `TunnelListener`, ADR-005/008): 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). `TunnelListener` is the listen-flow session (bound-address discovery + forwarded-connection receipt). The guiding insight (the OQ-TN-01 reframe): > A tunnel is a resource, not an address. Base params identify a > produced resource + substrate; the producer owns the backing. The > one additive exception is the direct op (ADR-007): a consumer may > ask the producer to dial an arbitrary target, and that is a > different capability, its own op and its own scope (`tunnel:direct`), never > implied by `tunnel:open`. Rich per-datagram addressing (SOCKS5 ATYP, > udpgw-style remotes) still enters only through the `-D`/dynamic > composition path, i.e. 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), or `channels/tunnel/direct` with `params = {substrate, target}` (ADR-007, dynamic egress). The producer's establisher (the open op's awaited phase, alkcall ADR-049) resolves the resource (or parses the target), 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. A bind-first listen open completes at bind time and may carry `"bound"` (the resolved address) and `"listen"` (the correlation token) in the reply (ADR-008 §2/Amendment 1). 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 ops; the `-R` composed flow rides the bind-first establisher + the forwarded op (ADR-008). ## Dependencies ``` alktunnels (default: wasm-clean) ├── alkcall 0.10.1 (core types, channels: ChannelCore/ChannelClient/manager, │ pump_bidi, the ServingConfig identity seam (CF-005/006), │ establisher reply projection + flavor-form discovery │ derivation + ChannelRelay/HubLegTemplate (review 008/ADR-051)) ├── 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 connect, unix sockets; unix per 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, though it is 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, i.e. halves-producing dial/listen functions (ADR-004). Unix ships (OQ-TN-14); stdio does NOT: a spawned process's stdio is alktty's pipe mode (exit codes, signals), not a tunnel | no | ## Module Map ``` src/ lib.rs:re-exports; crate docs params.rs:TunnelParams {resource, substrate} + open-op input schema (ADR-001); TunnelTarget {substrate, target} + direct-op schema (ADR-007); TunnelForwardedParams {listen, substrate, peer} (ADR-008 as amended); the three scope constants wire.rs:the data-plane codec: frame_datagram / DatagramReader (ADR-003) producer.rs:the open ops: tunnel_open_spec + direct/forwarded spec builders (scope-gated), establisher shapes (dial + listen + bind-first), pump handler (pump_bidi / accept dispatcher), register_tunnel_*_openable (ADR-004/007/008 shapes) consumer.rs:TunnelSession: open / open_direct / adopt / stream_halves / take_halves / send_datagram / recv_datagram / pump_against / close / join / Drop (ADR-005); TunnelListener: open / bound / forwarded-receipt / close / Drop (ADR-008) 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; amended with the `tunnel:direct`/`tunnel:forwarded` scopes + the hub re-produce policy sentence | | [007](decisions/007-direct-open-op.md) | The direct open op | `channels/tunnel/direct`: dynamic-target egress, `{substrate, target}` params, `tunnel:direct` scope, dial-only (the ssh `direct-tcpip` capability with structural ACL separation) | | [008](decisions/008-listen-metadata-forwarded-op.md) | Listen metadata + the forwarded op | Bind-first establisher (reply `bound` field = BND.ADDR fidelity; `listen` = the correlation token), `channels/tunnel/forwarded` accept-as-open (params carry `{listen, substrate, peer}` = reply#2 fidelity); this is the `forwarded-tcpip` shape; amended so that a producer-minted correlation token supersedes the channel-ID key | ## 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); the policy sentence is pinned: the hub's ACL governs the final dial) - **OQ-TN-13**: UDP truncation semantics (resolved, fail-loud, ADR-003) - **OQ-TN-14**: unix/stdio placement (resolved; unix in via `local`, stdio out: alktty's pipe mode owns process stdio) - **OQ-TN-15**: the direct op (resolved; ADR-007) - **OQ-TN-16**: listen metadata (resolved; ADR-008) ## 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/`)