Files
alktunnels/docs/architecture/overview.md
T
glm-5.3-flash 0d6b23b7cf docs: spec the graduation — ADR-007 (direct op), ADR-008 (listen metadata + forwarded op)
The alksocks graduation research (tunnels-graduation.md) is decided;
the ADRs are normative now:

- ADR-007: the direct open op — channels/tunnel/direct on the one
  alk/tunnel ALPN, params {substrate, target} (self-describing JSON
  target; ATYP bytes rejected as not schema-describable), scope
  tunnel:direct (never implied by tunnel:open), dial-only.
- ADR-008: listen metadata — bind-first listen establisher (reply
  gains additive 'bound' = BND.ADDR fidelity; the BIND deadlock +
  10s-bound findings dissolve) + the forwarded op
  channels/tunnel/forwarded (accept-as-open, params carry
  {channel, substrate, peer} = reply#2 fidelity). Accept-first stays.
- ADR-001/002/006 amendments: params layout is per-op; flavor-form
  op ids on the one ALPN; per-flavor scopes + the hub re-produce
  policy sentence (the hub's ACL governs the final dial).
- wire/producer/consumer/overview/README updated for the new ops,
  the three establisher shapes, TunnelListener, open_direct.
- OQ-TN-15/16 promoted + resolved; upstream alkcall asks filed at
  alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md
  (flavor-form discovery derivation; the establisher → reply
  projection).

Verification: cargo test (22 passed), clippy -D warnings, fmt --check
(docs-only change; wasm check unaffected)
2026-09-16 08:31:56 +00:00

10 KiB


status: draft last_updated: 2026-09-16 (the graduation: the direct op, listen metadata, the forwarded op — ADR-007/008)

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" in the reply (ADR-008 §2 — the resolved address).
  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.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 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 {channel, substrate, peer} (ADR-008);
                      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), channels/tunnel/forwarded accept-as-open (params carry {channel, substrate, peer} = reply#2 fidelity) — the forwarded-tcpip shape

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/)