--- status: draft last_updated: 2026-09-07 --- # alktunnels — Producer Half The producer half: the `channels/tunnel/sub` open op — its spec, its establisher (dial + listen shapes), its pump handler, and its registration. The producer is whoever can reach the tunnel target; connection direction is irrelevant (OQ-TN-03's hub model). The reverse POC validated the producer running on the connect side of its own connection (`from_connection_with_serving`). Design decisions: [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-004](decisions/004-no-backend-trait.md) (no backend trait). This document is the normative WHAT. ## The Open-Op Spec ```text operation id: channels/tunnel/sub op type: Sub (reply = one { channel_id } envelope; data plane on the BiStream) ALPN marker: alk/tunnel (ChannelOpenSpec) visibility: External input schema: { resource: string (required), substrate: enum [tcp, udp, unix] (required) } output schema: { channel_id: integer > 0 } access control: required_scopes: ["tunnel:open"] description: SHOULD carry a human-readable hint (round-trips through discovery, OQ-TN-08) ``` Registration is via `ChannelCore::register_openable_with_establisher` (alkcall ADR-049 §5): the registry schema-validates input, runs the ACL, allocates the channel (connection-owner rule, ADR-047 §5), runs the establisher as the awaited bounded establishment phase, then spawns the pump handler. The generic channel ops (`channel/close`, `channel/control`, `channel/resources/subscribe`) register separately via `ChannelOperations::register_on` — the producer (or its assembly layer) MUST register them on the serving registry so peers can manage channels out-of-band (the recipe alkcall review 007 §Part B verified — `alkcall/docs/reviews/ 007-establishment-follow-ups-review.md`; R-01 = the `Establishment` plan payload, R-02 = the `OpenHandler` lifetime contract). **Registration timing:** the openable may register AFTER `from_connection_with_serving` returns (the connect-side serving posture — the reverse POC's W2): the dispatcher reads through the shared `Arc` per dispatch. Late registration is a supported assembly shape, not a race. ## The Establisher The establisher is the open op's awaited, bounded establishment phase (alkcall ADR-049 §1/§2): validate params semantically, consult ownership if wired, dial/accept the substrate, and return `Establishment::new(plan)` — the dialed handle rides the typed-opaque plan (`ChannelPlan = Arc`; the `+ Sync` bound constrains plan payloads — socket-backed handles carry it naturally, reverse POC F-1, documented upstream). On failure: `EstablishmentError` (the typed reason codes) → the wrapper tears the channel down and replies `channel:open_failed`. **Two shapes** (both return the same plan-flow; ADR-004): ### 1. Dial establisher (the `-L`/forward shape) Registry lookup of `(resource, substrate)` → backing → dial → halves. The dial function is substrate code injected at assembly (ADR-004: the establisher closure closes over it); the protocol crate never sees a socket type. Failure mapping: | Producer condition | Typed reason | |---|---| | resource not in the registry (or wrong substrate) | `unknown_resource` | | dial refused / timed out / unreachable | `dial_failed` | | bind/connect resource exhaustion (fds, ports, slots) | `resource_shortage` | | establisher-internal failure | `handler_error` | | the bound exceeded | `timeout` (wrapper-level) | ### 2. Listen establisher (the producer-side listener variant) For a producer whose resource is a LISTENER (SSH `-R` far-side listener; a hub exposing its own port to a third party): the same open op, the same params; the establisher pops the next accepted connection from an assembly-owned listener queue and returns it as the plan payload. The listener lifecycle (bind, accept loop, cancel) is assembly code — the protocol never binds (OQ-TN-04). Error mapping: an empty queue during establishment is `resource_shortage`; a closed listener is `dial_failed`-class. No new wire surface — the listen variant is plan-flow with a different halves source. **Identity:** the establisher receives the **per-call opener identity** (the dispatch-resolved identity overlaid onto the install-time context — alkcall 0.7.0 CF-006). On hub-forwarded opens it sees the end client, not the hub. `auth` carries the opener's identity plus the transport-truthful fields (`alpn`, `remote_addr`, `tls_client_fingerprint`) — never rewritten. Consult it for ownership-scoped resources if the assembly layer wants `OwnershipProvider` checks here (OQ-TN-08's optional half). **Concurrency:** each open's establisher result flows to ITS pump handler — concurrent opens of the same resource are independent (the R-01 plan flow — alkcall review 007 R-01, the `Establishment::new(plan)` payload; the forward POC's resource-keyed handoff race is structurally gone, confirmed under concurrency by the reverse POC). ## The Pump Handler The `OpenHandler` (alkcall ADR-049/050 shape: `Fn(Value, Option, Connection, AuthContext) -> JoinHandle<()>`): 1. Downcast the plan to the concrete handle type (`Arc::downcast::` — establisher and handler agree on the type; alkcall never inspects it). 2. `conn.accept_bi()` — the channel's yield-once `BiStream`. 3. `pump_bidi(bidi, handle.read, handle.write)` — **awaited inline** inside the handler's task. The returned `JoinHandle` MUST track the data-plane lifetime (R-02): the wrapper awaits it and its completion triggers channel teardown. **Early return = teardown-at-birth** — both pumps see instant EOF with zero bytes; the "spawn-and-forget" shape is the one bug class the R-02 telemetry (yield-once flag + birth-teardown debug hint) diagnoses at runtime. Hand-rolling the two-pump shape is out (ADR-050). 4. UDP: the plan payload is the FRAMED adapter (ADR-003) — the pump is unchanged; the codec lives at the substrate/pump boundary. **The handler is substrate-agnostic by construction:** it cannot know whether the halves came from TCP, UDP, a unix socket, or an in-process pipe, and must not (AGENTS.md convention 7). ## Registration API (the spec surface) ```rust pub fn register_tunnel_openable( core: &ChannelCore, // the manager + policy pair registry: &ResourceRegistry, // assembly-owned: (resource, substrate) -> backing on_registry: &Arc, // the session's dispatch registry // (the per-session fork per alkcall ADR-047 §4 // amendment, 2026-09-03 — NOT the connection // overlay; Layer-2-registered open ops resolve // NOT_FOUND, review 004 F-01) auth: AuthContext, // the install-time context (per-call overlay is upstream's) dial: DialFn, // substrate halves function (ADR-004; `local` feature or assembly) ) -> Result<(), String> ``` - `ResourceRegistry` (assembly construct, OQ-TN-11): maps `(resource, substrate)` → backing. The POCs' `HashMap` shape is the candidate; the collision domain is the producer's registry (ADR-001 — two producers on one connection may expose the same resource name independently). - The scope constant `TUNNEL_OPEN_SCOPE = "tunnel:open"`; the open-op spec builder is public so assembly layers can compose variants (e.g. extra ACL fields) without forking the wire shape. - The pump handler factory is public for the same reason. ## Access control posture Tunnels reach local networks — the open gate is the security boundary (OQ-TN-08): - The registry runs `AccessControl` before the establisher (the ACL rides `register_openable`; free via alkcall). - `TUNNEL_OPEN_SCOPE` is the base gate; the assembly layer MAY add resource-scoped checks via the establisher (ownership on the resource name) — the protocol provides the seam, not the policy. - No target allowlists, no tunnel-specific policy machinery in v1 — the far side owns the resource (OQ-TN-08 resolution); discovery is the ACL-filtered ops listing. ## Open Questions - **OQ-TN-11**: resource naming collision domain + lifecycle (partially resolved — the per-producer registry is the leading candidate) - **OQ-TN-14**: unix/stdio substrate placement (open — backend module task, not a wire question) ## References - [wire.md](wire.md) (the open op's wire surface), [overview.md](overview.md) (module map) - Forward POC `docs/research/poc-summary.md`; reverse POC `docs/research/reverse-poc-summary.md` (both producer shapes ran) - alkcall ADR-047/049 (open ops + establishment), ADR-050 (`pump_bidi`), ledger CF-005/CF-006 (identity precedence + per-call opener)