Phase 1 architecture for alknet-channels (multiplexing proxy on alknet/channels). Grounded in the completed de-risk POC (28 tests) and the landed ADR-070 (BidiStreamSource trait + Connection::from_source). ADRs: - 071: 9-byte chunk wire format (generalizes TTY's 5-byte) - 072: channel 0 pre-negotiated as alknet/call (no special control plane) - 073: channel lifecycle operations on the call protocol — channel/open, close, control, resources/subscribe; direction field pinned; subscribe from day one (not poll-for-v1 — StreamingHandler machinery exists) - 074: ChannelBidiStreamSource implements BidiStreamSource (ADR-070); into_sub_streams() typed accessor for TTY; accept_bi() generic path - 075: ChannelsAdapter + ChannelManager; REQ-CH-01..04 wire invariants - 076: bounded-buffer backpressure (1 MiB), 256-channel cap, monotonic IDs - 077: TTY inside channels uses sub-streams, not own wire format; amends ADR-052 scope to direct-connect TTY; channels feature on tty - 078: two-pump shutdown-on-completion contract (handler-level) - 079: hub relay translates channel 0, byte-forwards data channels - 080: ChannelClient (QUIC-only); AlknetClient core extraction deferred (OQ-55) Spec docs: overview, channels-wire, channels-connection, channels-adapter, channel-operations, channel-client. OQ-56 (full windowing) and OQ-57 (two-pump helper extraction) are genuine deferred(scope) deferrals with concrete blocking conditions; the contracts are decided, only the extensions are deferred. Hedging audit converted three research hedges into decisions: resources/subscribe (not poll), server-assigned IDs (not if-zero-RTT), bounded-buffer (not if-HOL-becomes-a-problem).
9.1 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-07-12 |
alknet-channels
A multiplexing proxy: a ProtocolHandler on alknet/channels that
decomposes a single bidirectional transport stream into N logical channels,
each carrying a different ALPN. Channel 0 is pre-negotiated as alknet/call
(ADR-072); every other channel is opened dynamically via call operations on
channel 0 and routed through the same HandlerRegistry as top-level
connections. The channels layer is a re-framing proxy — it converts between
"one transport stream carrying N channels" (the wire) and "N independent
AsyncRead + AsyncWrite handles" (what handlers see) — and it does no
protocol work itself.
Documents
| Document | Status | Description |
|---|---|---|
| overview.md | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates |
| channels-wire.md | draft | The 9-byte chunk format ([channel_id:u32 be][stream_type:u8][length:u32 be][payload]), stream types, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
| channels-connection.md | draft | ChannelBidiStreamSource (implements BidiStreamSource — ADR-070/074), into_sub_streams() typed destructure, recursive composition |
| channels-adapter.md | draft | ChannelsAdapter (ProtocolHandler on alknet/channels), ChannelManager, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
| channel-operations.md | draft | channel/open, channel/close, channel/control, channel/resources/subscribe — call-protocol operations on channel 0, ACL flow, direction semantics, the hub relay contract (ADR-079) |
| channel-client.md | draft | ChannelClient — the client side of a channels connection, QUIC-only initially, bidirectionality preserved |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 071 | channels Wire Format — 9-Byte Chunk Header | The chunk format; one-way door |
| 072 | Channel 0 Is Pre-Negotiated alknet/call |
Channel 0 = call protocol, no special control plane |
| 073 | Channel Lifecycle Operations on the Call Protocol | channel/open/close/control/resources/subscribe; direction semantics; subscribe not poll |
| 074 | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel BidiStreamSource impl; into_sub_streams() accessor |
| 075 | ChannelsAdapter and ChannelManager | The read/demux + reassemble/allocate split; REQ-CH-01..04 contracts |
| 076 | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel cap, monotonic IDs with wrap |
| 077 | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); amends ADR-052 scope |
| 078 | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer |
| 079 | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
| 080 | ChannelClient — the Client Side of a Channels Connection | ChannelClient in alknet-channels, QUIC-only; AlknetClient deferred (OQ-55) |
| 070 | BidiStreamSource Trait | The Connection extension point ChannelBidiStreamSource implements |
| 065 | Connection::from_stream |
The transport-agnostic Connection the channels layer rides on |
| 052 | alknet-tty Wire Format | The 5-byte format the 9-byte format generalizes (amended by ADR-077 — scoped to direct TTY) |
| 049 | StreamingHandler for Subscriptions | The machinery channel/resources/subscribe uses |
| 032 | Forwarded-For Identity | The auth chain for hub-relayed channel opens |
| 003 | Crate Decomposition | alknet-channels depends on alknet-core only; no handler-depends-on-handler |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | ChannelClient is decided (ADR-080); AlknetClient core extraction stays deferred — blocked on a second transport's client, not a second client |
| OQ-56 | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-076); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" |
| OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) | The shutdown-on-completion contract is decided (ADR-078); the helper extraction is blocked on a second two-pump handler existing (shape convergence) |
Key Design Principles
-
Streams are streams. A TTY session, an SSH channel, a forwarded TCP connection, a QUIC bidi stream — they're all
AsyncRead + AsyncWritehandles. The differences are only in how they're opened (negotiation viachannel/openon channel 0) and what multiplexing layer carries them (the 9-byte chunk format). Once normalized, every channel is an ALPN routed through the sameHandlerRegistry. See overview.md and ADR-071. -
Channel 0 is
alknet/callpre-negotiated, not a special control plane. The call protocol runs on channel 0 exactly as on a top-levelalknet/callconnection. Channel lifecycle operations (channel/open,channel/close,channel/control,channel/resources/subscribe) are call operations on channel 0'sOperationRegistry, gated by the existingAccessControl::check. No new auth machinery, no new framing. See ADR-072, ADR-073. -
The channels layer is a re-framing proxy, not a protocol engine. It converts between "one transport stream carrying N channels" (the wire) and "N independent
AsyncRead + AsyncWritehandles" (what handlers see). It does no ALPN-specific parsing, no auth, no transport coupling. This makes it WASM-compatible and transport-agnostic by construction. See channels-adapter.md and ADR-075. -
channel/resources/subscribeis aSubscription, not a polledQuery. The call protocol hasStreamingHandler/invoke_streaming(ADR-049, implemented and tested). The first consumer (the hub aggregating worker resources) needs live updates. Polling would be built and immediately reworked. See ADR-073. -
Bidirectional open. Either side can open a channel to the other, just like the call protocol's operation overlay. The
directionfield onchannel/openpins who is the ALPN-server vs ALPN-client. See ADR-073 §Direction semantics. -
Wire-level invariants are contracts, not implementation details. The POC surfaced five invariants (REQ-CH-01..04, plus REQ-CH-06 for close ordering) that hang channels silently if underspecified: shutdown emits a zero-length sentinel; transport close drops all senders; the mux supports dynamic registration; unknown
channel_idis lenient-dropped; bounded-buffer backpressure doesn't deadlock; data chunks flush beforechannel/close. See channels-wire.md and channels-adapter.md. -
The hub translates, not transparently forwards. The hub terminates channel 0 on both legs, runs
AccessControl::check, and re-issueschannel/openon the spoke leg withforwarded_for(ADR-032). Data channels are byte-forwarded withchannel_idrewrite. This preserves the auth model. See ADR-079.
References
docs/research/alknet-channels/phase-0-findings.md— Phase 0 research (vision, hub motivation, wire format, negotiation, internals, DPs, OQs)docs/research/alknet-channels/poc-summary.md— the de-risk POC (28 tests, three validated targets, REQ-CH-01..06 wire-level invariants surfaced; REQ-CH-07 is a cosmetic clippy item, not a wire invariant)docs/research/alknet-channels/poc-plan.md— the POC plan/workspace/alknet-channels-poc/— the POC codebasedocs/research/alknet-tty/phase-0-findings.md— the TTY crate's chunk format (the seed of the channels generalization)docs/research/alknet-ssh/phase-0-findings.md— SSH's channel multiplexer (the prior art for N-channel multiplexing)docs/architecture/crates/hub/README.md— the hub crate (the primary consumer; the relay implementation's home)