# ADR-002: Single `alk/tunnel` ALPN (Option A) ## Status Accepted (2026-09-07) ## Context alkcall ADR-004: one ALPN per protocol, `alk/` prefix. This crate owns the `alk/tunnel`-family ALPN(s). The open question (OQ-TN-07) was whether stream (TCP/unix) and datagram (UDP) tunnels get distinct ALPNs: - **Option A**: single `alk/tunnel` ALPN; the substrate is a `params` field, and per-substrate data framing (if any) is self-describing inside the channel. - **Option B**: `alk/tunnel` (stream) + `alk/tunnel-dgram` (datagram), so the wire framing differs per ALPN cleanly. Mechanically both are cheap: channels' `params` is ALPN-specific, and the open-handler registry dispatches per ALPN. The cost difference is consumer-side API shape (two session types vs one with a substrate enum) and the ALPN namespace (a published string is wire-stable). Prior art surveyed in Phase 0 (`ssh-socks5-survey.md`): - SSH uses ONE channel mechanism for all forwarding flavors — the channel type string (`direct-tcpip`, `forwarded-tcpip`, `direct-streamlocal`) is per-open metadata on one transport, not a separate transport per flavor. - SOCKS5 runs CONNECT and UDP ASSOCIATE over one control connection with a CMD discriminator. - tun2proxy's udpgw proves datagram framing self-describes over a stream (length-framed datagrams inside a TCP tunnel). - With OQ-TN-02 resolved as endpoint-at-open for base UDP resources, the substrate discriminator in `params` tells the establisher and pumps which framing to expect — exactly option A's shape. The discriminator is load-bearing (it selects the framing, ADR-003), so it must ride the open op; the ALPN carries no substrate information at all. ## Decision One ALPN: **`alk/tunnel`**. The substrate discriminator in `params` (ADR-001) selects the data-plane framing; the open op, establisher, pump handler, and consumer session type are shared across substrates (one `TunnelSession` with a substrate-shaped data plane, not two session types). - The open op id is `channels/tunnel/sub` (the `channels//sub` convention of alkcall ADR-047 — the open op is a `Sub`-typed operation: the reply carries `channel_id` once, the data plane flows on the channel's `BiStream`). - The channel's ALPN marker on the open-op spec is `alk/tunnel` (`ChannelOpenSpec::new("alk/tunnel")`) — the value the per-ALPN dispatch and the adopted-side manager record for observability. - If a future tunnel flavor needs structurally different framing from byte zero with no params-dependent dispatch, that is a NEW ALPN decided by a new ADR before its first consumer — never a change to `alk/tunnel`'s meaning. ALPN strings are wire-stable once published. ## Consequences - **One session type for consumers** (`TunnelSession` with a substrate-shaped data plane, ADR-005); no API bifurcation. - **One registration per producer** regardless of how many substrates it serves; the resource registry (OQ-TN-11) keys on `(resource, substrate)`. - **Framing is params-dependent, not ALPN-dependent.** The establisher validates `substrate` semantically (the registry lookup); the pump handler is framing-agnostic (raw pass-through halves — the codec lives on the consumer/producer edge for UDP, ADR-003). A mismatched `substrate` value fails at schema/establisher time, loudly. - **ALPN namespace hygiene:** `alk/tunnel` is published here. The `alknet/tunnel` spelling in the alknet ADR-071 table predates the `alknet/` → `alk/` prefix swap (alkcall v0.1.1) — docs and code must not perpetuate the old prefix. ## References - OQ-TN-07 (promoted, resolved by this ADR) - `docs/research/ssh-socks5-survey.md` §ALPN-relevant prior art - alkcall ADR-004 (ALPN convention), ADR-047 (open ops per ALPN) - AGENTS.md convention 17 (ALPN naming)