- Rewrote the remaining em dashes outside docs/reviews/ (preserved as historical record, matching the alktty precedent): tests/ (313), fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in fences), docs/research + sdd_process + plans (525 plus 25 in fences), docs/architecture/overview.md fence labels (13), README, CHANGELOG, AGENTS.md, fuzz/README.md (176), and the .opencode/agents prompts (110) - Rewrote fenced content too: ASCII tree and module-map label separators, Rust snippet comments, and template placeholders; wire data, JSON/BAST examples, and transcripts survive verbatim - Rewrote 6 assert/expect message literals in tests/ and 2 format strings in fuzz/shared/src; no test data changed - Added Project Convention 18: no em dashes in prose (doc comments, markdown including docs/ and tasks/, agent prompts, commit messages); fence content rule, en-dash clause rule, and the no-spaced-hyphen prohibition follow alktty's convention 15 Follows6b20924(src doc comments) andcd0afe0(docs/architecture). Verification: cargo test (141 pass); cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/
194 lines
11 KiB
Markdown
194 lines
11 KiB
Markdown
---
|
|
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/`) |