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)
172 lines
7.3 KiB
Markdown
172 lines
7.3 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-09-07
|
|
---
|
|
|
|
# alktunnels — Wire Format
|
|
|
|
The `alk/tunnel` wire surface. Two layers: the **open op** (JSON
|
|
params on the channels call plane — structured, schema-validated,
|
|
typed errors) and the **data plane** (bytes/datagrams inside the
|
|
channel `BiStream` — the codec, ADR-003). The channels layer strips
|
|
its own 8-byte header transparently (alknet ADR-093); the tunnel
|
|
protocol owns everything inside the `BiStream`.
|
|
|
|
Design decisions live in the ADRs ([001](decisions/001-open-params-layout.md),
|
|
[002](decisions/002-alpn-strategy.md), [003](decisions/003-codec-and-udp-framing.md));
|
|
this document is the normative WHAT.
|
|
|
|
## The Open Op (`channels/tunnel/sub`)
|
|
|
|
`Sub`-typed operation (alkcall ADR-047): the reply carries
|
|
`{ "channel_id": <u32> }` once; the data plane flows on the channel's
|
|
`BiStream` — never in the call response stream.
|
|
|
|
### Input (params)
|
|
|
|
```json
|
|
{
|
|
"resource": "postgres-primary",
|
|
"substrate": "tcp"
|
|
}
|
|
```
|
|
|
|
- `resource`: string, required — the producer's stable resource name
|
|
(NOT an address; the producer's registry maps it to its backing).
|
|
- `substrate`: string, required, enum `["tcp", "udp", "unix"]` — the
|
|
extensible discriminator (ADR-001); selects the data-plane framing.
|
|
The schema ships with all three values in v1 (unix has an
|
|
implementation path — OQ-TN-14). An older producer rejecting a
|
|
newer substrate value is the SSH "unknown channel type" posture —
|
|
loud, not silent.
|
|
- `input_schema` pins both as required strings; unknown substrate
|
|
values fail schema validation (typed error — loud, not silent).
|
|
- Extensibility: a new substrate is a new enum value, not a format
|
|
change. An older producer rejecting a newer substrate is the SSH
|
|
"unknown channel type" posture.
|
|
|
|
### Reply / errors
|
|
|
|
- Success: `{ "channel_id": <u32> }` — the channel is open, the
|
|
establishment phase (dial) succeeded, the pump handler is running.
|
|
- Failure: a typed `channel:open_failed` call error with
|
|
`details: { reason, message }` — `reason ∈ dial_failed` /
|
|
`unknown_resource` / `resource_shortage` / `handler_error` /
|
|
`timeout` (alkcall ADR-049 §3). The registry's ACL gate runs before
|
|
the wrapper (`FORBIDDEN` for a scope-less identity); the 256-channel
|
|
cap is `channel:too_many_channels`. **A failed open never returns a
|
|
`channel_id`** — no phantom channel (the SSH contract, POC-verified
|
|
from the consumer side).
|
|
- The scope gate: the open op's `AccessControl.required_scopes` is
|
|
`["tunnel:open"]` (the `TUNNEL_OPEN_SCOPE` constant). Caller
|
|
identity resolves in the alkcall 0.7.0 precedence order: payload
|
|
`auth_token` > `ServingConfig.identity` > transport identity (CF-005);
|
|
the establisher and pump handler receive the per-call opener
|
|
identity (CF-006).
|
|
|
|
## The Data Plane
|
|
|
|
One data stream per direction on one channel — the channel ID is the
|
|
flow key (no conn-id, no sub-demux). Two codecs by substrate
|
|
(ADR-003):
|
|
|
|
### Stream substrates (`tcp`, `unix`): raw pass-through
|
|
|
|
The halves are the tunnel. Zero tunnel-level framing; the only wire
|
|
overhead is the channels 8-byte chunk header. A zero-length read is
|
|
genuinely EOF (stream semantics).
|
|
|
|
Both pumps run `alkcall::channels::pump_bidi(channel, peer_read,
|
|
peer_write)` — the two-pump contract (alknet ADR-078, pinned upstream
|
|
ADR-050): when one direction's source EOFs, the pump shuts the
|
|
OPPOSITE sink down (the EOF sentinel crosses the mux; the peer sees a
|
|
clean half-close), and the helper completes when both pumps finish,
|
|
returning `(u64, u64)` copy counts. Copy errors are EOF-shaped
|
|
(abrupt close); there is no `Err` state.
|
|
|
|
### Datagram substrate (`udp`): mandatory length framing
|
|
|
|
Every datagram rides `[len: u16 BE][datagram]` on BOTH directions —
|
|
mandatory for correctness (the F-2 rationale lives in ADR-003; the
|
|
short form: raw pass-through cannot carry an empty datagram). Under
|
|
the codec:
|
|
|
|
- **Framing (`frame_datagram`):** `len: u16 BE` + payload. Datagrams
|
|
> 65535 bytes are rejected at frame time (`Oversize`) — never a
|
|
wire overflow (the u16 length field would wrap).
|
|
- **Decoding (`DatagramReader`):** incremental — datagrams split
|
|
across channel chunks, batch into single chunks, and survive
|
|
partial headers. The decoder buffers state across reads; it is the
|
|
only consumer-visible decode path.
|
|
- **`len = 0` is a legal empty datagram** (DNS-over-TCP-style
|
|
zero-payload probes). EOF is exclusively the channels-level
|
|
sentinel (`BiStream` EOF after draining); the codec never emits
|
|
zero-length reads, so the layers never collide. A `recv_datagram`
|
|
returns `Some(bytes)` per datagram (possibly empty), `None` only on
|
|
stream EOF.
|
|
- **Chunk-size independence:** datagram boundaries survive chunk
|
|
splitting, batching, and any channels-layer re-chunking — verified
|
|
end-to-end at 7-byte chunk splits (forward POC).
|
|
- **MTU discipline:** 1400-byte datagrams (a conservative MTU-safe
|
|
payload, not the theoretical 1472 max) ride the bounded-buffer path;
|
|
the 64-parked-chunks bound (alkcall early-arrival cap) is upstream
|
|
sizing, not tunnel policy.
|
|
- **Truncation:** a receive buffer smaller than the datagram fails
|
|
loudly at the adapter level (OQ-TN-13, resolved fail-loud per
|
|
ADR-003); silent truncation would corrupt the framing invariants.
|
|
|
|
### Sentinels and lifecycle (both codecs)
|
|
|
|
- **EOF:** the channels-level zero-length chunk, written by
|
|
`pump_bidi`'s shutdown-on-completion or by a channel teardown
|
|
(wrapper exit, `channel/close`, transport EOF → demux sender drop).
|
|
EOF is per-direction (half-close): one direction EOFs, the other
|
|
keeps pumping until its own EOF.
|
|
- **No mid-stream control frames.** Pump-phase failures are
|
|
EOF-shaped by design (alkcall ADR-049 §6); there is no tunnel-level
|
|
error frame, no KEEPALIVE (channels transport liveness is
|
|
upstream's), no flow-expiry frame (producer-side bookkeeping, and
|
|
idle-expiry mapping is a producer-registry concern, not wire).
|
|
- **No establishment frame.** The open op's reply IS the
|
|
establishment result (ADR-049); the pre-ADR-049
|
|
"JSON ack before data" hunch is superseded — the wire carries zero
|
|
tunnel-level control framing in v1.
|
|
- **BAST:** the binary framing (the UDP codec) carries its BAST
|
|
document — [bast.md](bast.md) (AGENTS.md convention 12; the
|
|
stream pass-through has no binary framing to describe).
|
|
|
|
## Normative byte diagrams
|
|
|
|
Stream substrate, one direction's wire view (inside the channel
|
|
`BiStream`, after the channels layer's 8-byte header per chunk):
|
|
|
|
```
|
|
<payload bytes> ... <EOF sentinel: channels-level length=0 chunk>
|
|
```
|
|
|
|
UDP substrate, one direction's wire view:
|
|
|
|
```
|
|
[len:u16 BE][datagram bytes] [len:u16 BE][datagram bytes] ...
|
|
len == 0: an empty datagram (legal payload, NOT EOF)
|
|
EOF: the channels-level sentinel, outside the codec
|
|
```
|
|
|
|
## Open Questions
|
|
|
|
- **OQ-TN-13**: resolved (fail-loud, ADR-003) — see the Truncation
|
|
bullet
|
|
- **OQ-TN-02**: resolved — see the ADR-003 mandate
|
|
|
|
## References
|
|
|
|
- [ADR-001](decisions/001-open-params-layout.md) (params),
|
|
[ADR-002](decisions/002-alpn-strategy.md) (ALPN),
|
|
[ADR-003](decisions/003-codec-and-udp-framing.md) (codec),
|
|
[ADR-005](decisions/005-consumer-session-owns-teardown.md) (session
|
|
teardown)
|
|
- Forward POC `docs/research/poc-summary.md` (codec + sentinel
|
|
layering, 17 tests); reverse POC
|
|
`docs/research/reverse-poc-summary.md` (F-2 mandate, W4 half-close)
|
|
- alkcall ADR-047 (open ops), ADR-049 (establishment/typed errors),
|
|
ADR-050 (`pump_bidi`); alknet ADR-071/093 (channels wire) |