Files
alktunnels/docs/architecture/overview.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

8.1 KiB

status, last_updated
status last_updated
draft 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, producer.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):

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

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

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