Split the last open substrate-placement question: - Unix: ships with the local feature v1 (dial_unix — same halves shape as TCP; the wire enum already carried unix per ADR-001; the params task's schema list includes all three values) - Stdio: OUT of scope — a spawned process's stdin/stdout/stderr IS alktty's pipe mode (LocalTtyBackend + tokio::process + Stdio::piped, alktty tty-local.md): three multiplexed logical streams + the exit-code control chunk (alktty ADR-004) + signal forwarding (REQ-TTY-02). A stdio bridge here would be alktty's runner mode with the terminal stripped out — a strictly worse duplicate that also drops the semantics that matter (a byte tunnel has neither exit codes nor signals). Remote command execution composes via alktty on the same channels substrate. Updated: open-questions.md OQ-TN-14 (resolved), overview.md feature gate + deps + OQ summary, producer.md OQ ref, OQ-TN-10 promotion (#3 split), phase-0-findings + both POC summaries' resolution notes, params task (schema enum includes unix), local-socket-halves task (unix ships, stdio does NOT — with the composition rationale), oq-tn-14-tracker task repurposed (boundary-maintenance: re-opens only if a consumer needs stdio-without-process-semantics — which would need its own ADR, or if the alktty/alktunnels boundary needs sharpening). Verified: taskgraph valid (12 tasks, no cycles)
167 lines
8.2 KiB
Markdown
167 lines
8.2 KiB
Markdown
---
|
|
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 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)
|
|
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 placement (resolved — unix in via
|
|
`local`, stdio out: alktty's pipe mode owns process stdio)
|
|
|
|
## 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/`) |