Files
glm-5.3-flash cd0afe0497 docs(architecture): eliminate em dashes from the spec docs
Rewrote every prose em dash under docs/architecture/ with natural
grammar (sentence splits, colons, semicolons, parentheses, commas);
wire-format fences, JSON/BAST examples, and byte diagrams untouched.

Verified: grep -rln '—' docs/architecture/ returns only
overview.md's fenced dependency/module-map blocks (12 dashes),
left verbatim per the fenced-code rule; all other files at zero.
2026-11-03 14:02:07 +00:00

165 lines
9.7 KiB
Markdown

---
status: draft
last_updated: 2026-09-16 (the graduation ADRs 007/008 + the amendments)
---
# alktunnels: Architecture
Arbitrary bidirectional tunnels over alkcall channels: the
`alk/tunnel`-family producer/consumer protocol crate. This directory
holds the architecture spec docs and the ADRs. It is the sibling of
alktty's architecture (`/workspace/@alkdev/alktty/docs/architecture/`):
same substrate (alkcall channels), same shape (a producer
open-handler + a typed consumer session), different service (tunnels:
raw byte/datagram transport, not terminal sessions).
## Documents
| Document | Status | Description |
|----------|--------|-------------|
| [overview.md](overview.md) | draft | Crate purpose, the resource model in brief, dependencies, ALPN, feature gates, module map |
| [wire.md](wire.md) | draft | The wire format: open-op params JSON (ADR-001; the direct op's `{substrate, target}` per ADR-007), the `bound` reply field + the forwarded op (ADR-008), the data-plane codec (raw pass-through for stream substrates; mandatory `[len: u16 BE]` framing for UDP per ADR-003), sentinels, half-close semantics |
| [producer.md](producer.md) | draft | The producer half: the `channels/tunnel/sub` + `channels/tunnel/direct` open ops, three establisher shapes (dial, accept-first listen, bind-first listen), the pump handler, the forwarded-op opener side, registration, scope gating |
| [consumer.md](consumer.md) | draft | The consumer half: `TunnelSession` (stream + datagram variants, `open`/`open_direct`), `TunnelListener` (the listen flow), the reverse-flow initiator, teardown ownership (ADR-005/008) |
| [bast.md](bast.md) | draft | The BAST (Binary Abstract Syntax Tree) document for the data-plane's binary framing (the UDP `[len: u16 BE]` codec, ADR-003); the stream pass-through has no binary framing to describe |
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-TN-01..16; the Phase 0 ledger promoted) |
## Applicable ADRs
alktunnels-native ADRs (001..008). Upstream ADRs (alkcall/alknet) are
referenced by their own numbers and are NOT ported; the alkcall
originals at `/workspace/@alkdev/alkcall/docs/architecture/decisions/`
remain authoritative for the shared surfaces (establishment, channels
wire format, `pump_bidi`, identity seam).
| ADR | Title | Status |
|-----|-------|--------|
| [001](decisions/001-open-params-layout.md) | Open-Op Params Layout: Resource + Substrate | Accepted; amended (params layout is per-op; the direct op's params ride ADR-007) |
| [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN (Option A) | Accepted; amended (flavor-form op ids: `…/sub`, `…/direct`, `…/forwarded` on the one ALPN) |
| [003](decisions/003-codec-and-udp-framing.md) | Data-Plane Codec: Raw Pass-Through (Stream) / Mandatory Length Framing (UDP) | Accepted |
| [004](decisions/004-no-backend-trait.md) | No TunnelBackend Trait: Halves Functions at the Assembly Layer | Accepted |
| [005](decisions/005-consumer-session-owns-teardown.md) | The Consumer Session Type Owns Teardown | Accepted |
| [006](decisions/006-access-control-posture.md) | Access-Control Posture: the Open Gate Is the Security Boundary | Accepted; amended (`tunnel:direct`/`tunnel:forwarded` scopes; the hub re-produce policy sentence) |
| [007](decisions/007-direct-open-op.md) | The Direct Open Op: Dynamic-Target Egress | Accepted |
| [008](decisions/008-listen-metadata-forwarded-op.md) | Listen Metadata: Bind-First Establisher and the Forwarded Op | Accepted |
## Key Design Principles
1. **A tunnel is a resource, not an address (with one additive
exception).** Base params identify a produced resource + substrate
discriminator; the producer owns the backing (local port, docker
container, in-process service, unix socket). Rich addressing
enters through `-D`/dynamic composition, which speaks its protocol
inside the tunnel payload. The exception (ADR-007): the direct op
lets a consumer ask the producer to dial an arbitrary target. That
is a different capability on its own op and scope (`tunnel:direct`),
never implied by `tunnel:open`. See
[ADR-001](decisions/001-open-params-layout.md),
[ADR-007](decisions/007-direct-open-op.md), and
[wire.md](wire.md).
2. **Role follows the resource; connection direction is independent.**
Whoever can reach the target is the producer (registers openable
channels); whoever wants the bytes is the consumer. `-L` and `-R`
are the same open op with the entry point on different machines;
no protocol-level direction (the hub-owns-the-connection model,
OQ-TN-03). The composed `-R` flow rides the bind-first establisher
+ the forwarded op (ADR-008). Validated by the reverse-flow POC:
the worker serves on the connect side; the hub initiates per
accept. See [producer.md](producer.md) and
[consumer.md](consumer.md).
3. **Raw pass-through for stream substrates; mandatory length framing
for UDP.** A tunnel has one data stream per direction: no
sub-demux key, no tunnel-level header on stream substrates (0 B
tunnel overhead; the channels 8-byte header is the only cost).
UDP rides `[len: u16 BE]` framing, which is mandatory for correctness
(F-2; the rationale and the executable pin live in ADR-003). See
[ADR-003](decisions/003-codec-and-udp-framing.md) and
[wire.md](wire.md).
4. **The two-pump shape is `alkcall::channels::pump_bidi`; it is never
hand-rolled.** The producer's pump handler and the consumer-side
pump both consume the upstream helper (ADR-050); each pump shuts
down the opposite sink on completion (half-close semantics fall
out). The `OpenHandler`'s returned `JoinHandle` tracks the
data-plane lifetime (early return = teardown-at-birth, R-02). See
[producer.md](producer.md).
5. **No backend trait.** Stream substrates need "produce boxed halves
for a resource"; that is a function, not a trait (the UDP POC's OQ-TN-05
finding). The inversion point is the assembly layer wiring
substrate halves into the establisher/pump; substrate types stay
in feature-gated backend modules. The listen variant (producer
accepts instead of dialing) is an establisher shape, not a trait.
See [ADR-004](decisions/004-no-backend-trait.md).
6. **The consumer session type owns teardown.** One session = one
channel = one tunnel (the channel ID is the flow key). The adopted
side has no wrapper awaiting its pump (reverse POC W3): the
session must hold the pump handle and reap the channel on
completion/close. See [ADR-005](decisions/005-consumer-session-owns-teardown.md)
and [consumer.md](consumer.md).
7. **Backpressure and limits are inherited, not redefined.** Bounded
per-channel buffers, the 256-channel cap, monotonic IDs, and the
zero-length-sentinel EOF convention are alkcall channels
invariants; the tunnel crate consumes them and does not build a
second demux/mux (AGENTS.md convention 10).
8. **Security posture: the open gate is the boundary.** Scope-gated
opens (`TUNNEL_OPEN_SCOPE = "tunnel:open"`,
`TUNNEL_DIRECT_SCOPE = "tunnel:direct"`,
`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"` (each stable once
published; ADR-006 + Amendment 1), alkcall's op-level ACL via
`register_openable`; caller identity resolves in the 0.7.0
precedence order (token > `ServingConfig.identity` > transport);
the establisher/pump handler see the per-call opener identity. No
target allowlists in the protocol (an assembly establisher MAY
enforce them, but that is policy, not protocol). Hub re-produce: the hub's
ACL governs the final dial (the hub is the network boundary). See
[ADR-006](decisions/006-access-control-posture.md),
[ADR-007](decisions/007-direct-open-op.md), and
[producer.md](producer.md).
9. **Wasm-clean default crate.** The default crate (protocol only)
compiles to `wasm32-unknown-unknown`; socket/platform I/O lives in
feature-gated backend modules (AGENTS.md conventions 4/14), same
as alktty.
## Relevant Open Questions
All open questions are tracked in
[open-questions.md](open-questions.md). The Phase 0 OQ ledger
(OQ-TN-01..10, `docs/research/phase-0-findings.md`) is promoted there;
the questions affecting the spec set are summarized in each doc's Open
Questions section.
## References
- Phase 0 research: `docs/research/phase-0-findings.md` (OQ ledger),
`docs/research/poc-summary.md` (UDP POC),
`docs/research/reverse-poc-summary.md` (reverse-flow POC),
`docs/research/ssh-socks5-survey.md` (SSH/SOCKS5/udpgw prior art)
- Upstream: alkcall ADR-047 (openable ALPNs are operations), ADR-049
(establishment phase + amendment 2 plan payload), ADR-050
(`pump_bidi`), ADR-022 §2 (both-sides serving), ADR-047 §5
(connection-owner allocation), ledger CF-005/CF-006 (identity seam);
alknet ADR-078 (two-pump contract), ADR-071/093 (channels wire
format); alkcall ADR-042 (hub relay)
- Upstream reviews/ledger (the spec cites these IDs):
`/workspace/@alkdev/alkcall/docs/reviews/007-establishment-follow-ups-review.md`
(R-01 = the `Establishment` plan payload, R-02 = the `OpenHandler`
lifetime contract, R-03 = `pump_bidi`) and
`/workspace/@alkdev/alkcall/docs/reviews/consumer-findings-ledger.md`
(CF-005/CF-006 = the identity seam, resolved in 0.7.0)
- The graduation research (the two new ADRs implement it):
`docs/research/tunnels-graduation.md`; the upstream alkcall asks
(review 008) **landed in alkcall 0.8.0**: flavor-form discovery
derivation + the establisher → reply `bound` projection, plus the
in-tree `ChannelRelay`/`HubLegTemplate` (alkcall ADR-051)
- Sibling precedent: alktty (`/workspace/@alkdev/alktty`), i.e. the
producer/consumer-on-channels shape, wasm-clean default,
feature-gated local backend, BAST convention
- POC code: `/workspace/alktunnels-udp-poc` (forward),
`/workspace/alktunnels-reverse-poc` (reverse)