Files
alktunnels/docs/architecture/overview.md
T
glm-5.3-flash f315b0e0ca docs+tasks: ADR-008 Amendment 1 (correlation token); decompose the graduation into tasks/tunnels
Verification pass against the landed alkcall 0.8.0 surface surfaced one
design gap: ADR-008 §3 pinned the forwarded op's correlation key as the
listen channel's ID, but (1) the serving-side OpenHandler/establisher
surface never exposes the allocated channel ID, and (2) the in-tree
generic ChannelRelay forwards open-op params untouched (it rewrites
only the reply's channel_id), so a spoke's forwarded open would carry
an id the end opener never allocated. Amended (wire still unconsumed —
alksocks is docs-only — so pre-consumer and legal):

- ADR-008 Amendment 1: the bind-first establisher mints a fresh u32
  correlation token per open — carried in the plan (for the accept
  dispatcher) and contributed as the additive `listen` reply field
  (the same projection carrier `bound` rides); the forwarded op's
  params rename `channel` -> `listen` with token semantics. Tokens
  ride reply fields + params through hubs untouched (relay-transparent
  by construction, alkcall ADR-051). Consumer-minted tokens and a new
  upstream handler surface were considered and rejected (rationale in
  the amendment).
- Spec docs synced: wire.md (the `bound`+`listen` reply section, the
  forwarded params), producer.md (bind-first shape + forwarded op),
  consumer.md (TunnelListener gains `listen()`; correlation registry
  posture), overview.md, OQ ledger (OQ-TN-16 updated),
  tunnels-graduation.md.

Task decomposition (tasks/tunnels/, Phase 2 of the graduation — the
alkcall prerequisites are gone):
- direct-op — params/spec/establisher/open_direct (ADR-007)
- bindfirst-params — forwarded-op spec, TunnelListenReply, the
  BindFirstPlan + token mint (wire types first, the params.md
  ordering precedent)
- bindfirst-producer — bind-first establisher, accept dispatcher,
  register_tunnel_bindfirst_openable
- forwarded-receipt — the opener-side serving op + reply-field reader
- tunnel-listener — the listen-flow consumer session (ADR-008 §4)
- local-bind-halves — the `local` TCP bind + accept-pairs loop,
  unix parity decision, direct-op DialFn reuse pin

Dependency order: direct-op and bindfirst-params are independent
roots; bindfirst-producer -> {bindfirst-params}; forwarded-receipt ->
bindfirst-params; tunnel-listener -> {bindfirst-params,
forwarded-receipt}; local-bind-halves -> bindfirst-producer.

Verification: cargo test 71 passed; clippy --all-targets -D warnings;
fmt --check; wasm32-unknown-unknown check + clippy clean (docs + task
files only — no code change).
2026-09-18 14:26:21 +00:00

11 KiB


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 — 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, producer.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 — served by the listen-opener side — channels/tunnel/forwarded (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 — 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 — 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):

  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.8.0   (core types, channels: ChannelCore/ChannelClient/manager,
│                    pump_bidi, 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 — 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 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/.

ADR Decision Summary
001 Open-op params: resource + substrate {resource, substrate} — the producer's stable name, not an address; wire-stable from the first consumer
002 Single alk/tunnel ALPN Substrate in params selects the framing; no ALPN split, no API bifurcation
003 Data-plane codec Raw pass-through (stream) / mandatory [len: u16 BE] (UDP — F-2 mandate); len=0 is a legal empty datagram
004 No TunnelBackend trait Halves functions at the assembly layer; listen is an establisher shape; trait re-evaluated at the 3-consumer threshold
005 Consumer session owns teardown TunnelSession with close/join/Drop — the W3 gap closes structurally
006 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 — tunnel:direct/tunnel:forwarded scopes + the hub re-produce policy sentence
007 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 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) — the forwarded-tcpip shape; amended — producer-minted correlation token supersedes the channel-ID key

Open Questions

Open questions are tracked in 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 §References
  • Sibling: alktty (/workspace/@alkdev/alktty/docs/architecture/)