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.
239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-09-18 (`open_direct`, `TunnelListener`, the forwarded-op serving side; ADR-007/008 as amended: the correlation token)
|
|
---
|
|
|
|
# alktunnels: Consumer Half
|
|
|
|
The consumer half: `TunnelSession`, the typed client for tunnel
|
|
channels, plus `TunnelListener` (the listen-flow session, ADR-008)
|
|
and the forwarded-op serving side. The consumer is whoever wants the
|
|
bytes; it opens tunnel channels toward the producer (forward path), or
|
|
per local accept toward a connect-side serving producer (the reverse
|
|
flow). Both are the same session type; role follows the resource, not
|
|
the connection (OQ-TN-03). Design decisions:
|
|
[ADR-005](decisions/005-consumer-session-owns-teardown.md) (session
|
|
owns teardown), [ADR-003](decisions/003-codec-and-udp-framing.md)
|
|
(codec), [ADR-007](decisions/007-direct-open-op.md) (the direct op),
|
|
[ADR-008](decisions/008-listen-metadata-forwarded-op.md) (listen
|
|
metadata + the forwarded op); this document is the normative WHAT.
|
|
|
|
## Opening a Tunnel
|
|
|
|
### Forward path (`-L`/direct, the consumer dials the producer's transport)
|
|
|
|
```rust
|
|
let session = TunnelSession::open(&channel_client, params).await?;
|
|
```
|
|
|
|
- `params`: `{resource, substrate}` (ADR-001), i.e. the produced resource
|
|
+ substrate discriminator.
|
|
- Flow: `ChannelClient::open_channel("channels/tunnel/sub", params,
|
|
"alk/tunnel")` → adopt the returned channel ID → split the channel
|
|
`BiStream` → present the substrate-shaped data plane.
|
|
- Errors: a failed open resolves a typed error:
|
|
`ChannelOpenError::CallFailed` carrying the wire `CallError`;
|
|
branch on `establishment_reason()` for `channel:open_failed`'s
|
|
reason (dial_failed / unknown_resource / resource_shortage /
|
|
handler_error / timeout), or the pre-establishment codes
|
|
(`FORBIDDEN`, `channel:too_many_channels`). **A failed open never
|
|
yields a session**; no phantom session, mirroring the
|
|
no-phantom-channel property (POC-verified).
|
|
|
|
### Direct path (dynamic-target egress, ADR-007)
|
|
|
|
```rust
|
|
let session = TunnelSession::open_direct(&client, target, substrate).await?;
|
|
```
|
|
|
|
- A separate constructor from `open`, not a params enum: two scopes
|
|
are two capabilities (`tunnel:direct` vs `tunnel:open`), and the
|
|
caller states which one it holds. The session type, data planes,
|
|
and teardown API are identical.
|
|
- `target`: the substrate-shaped JSON object (`{host, port}` /
|
|
`{path}`; ADR-007 §2); domain-form hosts stay unresolved (the
|
|
producer resolves).
|
|
- Flow: `open_channel("channels/tunnel/direct", {substrate, target},
|
|
"alk/tunnel")` → same adopt/split/data-plane flow as `open`.
|
|
- Errors: the same typed surface minus `unknown_resource` (no
|
|
registry lookup); malformed targets are the schema/
|
|
`invalid_input`-class failure. An old producer rejects the unknown
|
|
op loudly (`NOT_FOUND`, the SSH posture).
|
|
|
|
### Reverse path (`-R`, the consumer initiates toward a connect-side serving producer)
|
|
|
|
The hub/accept-side shape (the reverse POC's `ReverseTunnel`, the
|
|
seed of this path). Construction differs from the forward path only
|
|
in how the channel is obtained; the session type and teardown API
|
|
are identical:
|
|
|
|
```rust
|
|
// Per local accept (assembly-owned listener):
|
|
let channel_id = open_reverse_channel(&hub_call, ¶ms, auth_token).await?;
|
|
let session = TunnelSession::adopt(hub_manager, channel_id, substrate, TUNNEL_ALPN).await?;
|
|
let pumped = session.pump_against(accepted_halves).await; // or drive the halves directly
|
|
```
|
|
|
|
- `open_reverse_channel`: call the worker's open op on channel 0
|
|
(`call_with_payload`; optional `auth_token`), extract the
|
|
worker-allocated `channel_id`. Caller identity rides the transport
|
|
by default (alkcall 0.7.0 CF-005 (b): `Connection::set_identity`
|
|
before dialing); the payload `auth_token` is the optional
|
|
hub-forwarding path (precedence: token > `ServingConfig.identity` >
|
|
transport).
|
|
- `TunnelSession::adopt(manager, channel_id, substrate, alpn)`:
|
|
adopt the worker-allocated ID (connection-owner rule, ADR-047 §5:
|
|
the SERVING side allocates; the initiator adopts; early-arrival
|
|
parking covers the adoption race), install the session's data plane
|
|
from the adopted halves. The substrate parameter is required; a
|
|
datagram session cannot exist without it (the data plane variant is
|
|
substrate-shaped). `adopt` takes the same typed-error surface as
|
|
`open` (an adopt failure is `AdoptFailed`-class, not an
|
|
establishment error).
|
|
- `pump_against(accepted_halves)`: spawn `pump_bidi` against the
|
|
accepted local connection and hold the pump handle (ADR-005).
|
|
|
|
The session type and its teardown API are identical on both paths:
|
|
one API, `-L` and `-R` alike.
|
|
|
|
## The Listen Flow: `TunnelListener` (ADR-008)
|
|
|
|
The listen-flow sibling of `TunnelSession`: the session that OPENS a
|
|
bind-first listen tunnel and owns its teardown (ADR-005's ownership
|
|
posture extended to the listen flow). The forwarded connections it
|
|
receives arrive as ordinary adopted-side `TunnelSession`s on the
|
|
opener's manager (`adopt` gains nothing).
|
|
|
|
```rust
|
|
let listener = TunnelListener::open(&client, params).await?; // bind-first: fast establishment
|
|
let bound = listener.bound(); // the resolved OS-chosen address (Option; absent when unknown)
|
|
let token = listener.listen(); // the correlation token (the producer's; from the `listen` reply field)
|
|
let sessions = listener.forwarded(); // the forwarded sessions, as they arrive
|
|
listener.close(self); // or Drop; tears the listen channel + far listener down
|
|
```
|
|
|
|
- **`TunnelListener::open`**: the same `channels/tunnel/sub` open
|
|
with `{resource, substrate}`; the establisher shape (bind-first vs
|
|
accept-first) is the producer's per-registration choice, invisible
|
|
to the constructor. What differs: bind-first replies carry
|
|
`"bound"` (ADR-008 §2: the resolved address; informational, never
|
|
a dial target) and `"listen"` (ADR-008 Amendment 1: the
|
|
correlation token the producer minted).
|
|
- **`bound()`**: `Option`-shaped: `Some` when the producer supplied
|
|
the field (bind-first), `None` otherwise (accept-first, dial
|
|
establishers, or old producers). reply#1-class fidelity (BND.ADDR).
|
|
- **`listen()`**: `Option<u32>`: the correlation token (bind-first
|
|
producers). The opener-side correlation registry, token → waiting
|
|
session, is assembly-side bookkeeping (consumer.md's posture:
|
|
the protocol provides the op and params, not the wait state
|
|
machine); `TunnelListener` hands the assembly the key.
|
|
- **The forwarded side:** each accepted connection at the far
|
|
listener arrives as a producer-initiated
|
|
`channels/tunnel/forwarded` open that the opener SERVES
|
|
(`tunnel:forwarded` scope), its `listen` param matching the token.
|
|
The opener-side assembly correlates the open to the waiting
|
|
session, adopts the allocated channel (`TunnelSession::adopt`),
|
|
and pumps against the local BIND client's halves.
|
|
- **`peer` fidelity:** the forwarded op's params carry the accepted
|
|
connection's real remote address (reply#2-class fidelity; the
|
|
alksocks POC's sentinel retires). Producer-asserted metadata, not
|
|
transport truth.
|
|
- **Teardown:** dropping/`close`ing the listener EOFs the listen
|
|
channel; the producer-side accept loop cancels and tears the far
|
|
listener down (ADR-008 §1's handler contract). Forwarded sessions
|
|
already adopted own their own teardown (per-session, ADR-005).
|
|
|
|
## The Data Plane (substrate-shaped)
|
|
|
|
- **Stream variant (`tcp`, `unix`):** `stream_halves()`: raw
|
|
`AsyncRead`/`AsyncWrite` halves; the halves ARE the tunnel (raw
|
|
pass-through, ADR-003). For owned access (spawning local pumps),
|
|
`take_halves()` consumes the session's halves.
|
|
- **Datagram variant (`udp`):** `send_datagram(&[u8])` /
|
|
`recv_datagram() -> Option<Bytes>` over the mandatory
|
|
`[len: u16 BE]` codec (ADR-003). `recv_datagram` returns
|
|
`Some(bytes)` per datagram (possibly empty; `len=0` is a legal
|
|
empty datagram), and `None` only on stream EOF (the channels-level
|
|
sentinel; the codec never collides with it, F-2). A >65535-byte
|
|
send is rejected at frame time (`Oversize`). `take_halves()` on a
|
|
datagram session yields the RAW halves; the wire bytes carry the
|
|
codec framing (ADR-005's N-3 amendment: one `take_halves` semantic
|
|
for every substrate; keep the session for codec-applied I/O, or
|
|
compose the framed adapter at your own boundary).
|
|
|
|
## Lifecycle and Teardown (the point of ADR-005)
|
|
|
|
One session = one channel = one tunnel. The session owns:
|
|
|
|
- **The adopted channel entry**: nothing upstream awaits the
|
|
adopter's pump (the W3 gap); the session closes it structurally.
|
|
- **The session-owned pump handle** (when the session spawned one,
|
|
i.e. the reverse path's `pump_against`).
|
|
|
|
Teardown API:
|
|
|
|
- `close(self)`: abort the pump (if session-owned), tear down the
|
|
adopted channel (`ChannelManager::teardown_channel`), consume the
|
|
session. The ungraceful path.
|
|
- `join(self)`: await pump completion (both directions finished;
|
|
half-close semantics fall out of `pump_bidi`: one direction EOFs,
|
|
the opposite sink shuts down, the other keeps pumping until its own
|
|
EOF; W4-validated), THEN reap the adopted channel. Returns the
|
|
`(u64, u64)` copy counts for observability. The graceful path.
|
|
**Pump-less sessions** (after `take_halves`, or a datagram session
|
|
the caller drives directly): there is no session-owned pump, so
|
|
`join` completes immediately (nothing to await) and reaps only;
|
|
the copy counts are `(0, 0)`.
|
|
- `Drop`: aborts the pump and sync-reaps the channel (teardown
|
|
semantics of `close`; best-effort, since `Drop` cannot await). Dropping
|
|
without close/join never leaks the channel entry.
|
|
- **`TunnelSession` does not implement `Clone`.** Aliasing a session
|
|
would alias its teardown; multi-channel consumers hold a
|
|
`Vec<TunnelSession>` (or the assembly layer does).
|
|
|
|
## Out-of-band close
|
|
|
|
A peer-initiated `channel/close` (the generic channel op on the
|
|
producer's serving registry) tears the SERVING side down; the
|
|
consumer's session still owns its adopted entry, so `close`/`join` reaps
|
|
it. The reverse POC pinned this split (the close ran on the worker;
|
|
the hub reaped its own entry, W3). EOF propagation is symmetric:
|
|
either side's teardown reaches the other as the channels-level
|
|
sentinel.
|
|
|
|
## Both-Sides Sanity
|
|
|
|
The consumer and producer halves compose on one connection in both
|
|
roles (alkcall ADR-022 §2): a worker dials the hub (connect side),
|
|
serves its open ops (serving side), and its own outbound calls still
|
|
resolve while serving (reverse POC: worker_outbound_calls_still_
|
|
resolve_while_serving). The consumer half adds nothing to the
|
|
connection's serving posture: a pure consumer (`from_connection`)
|
|
never dispatches inbound requests; a reverse-flow initiator needs no
|
|
serving registry of its own.
|
|
|
|
## Open Questions
|
|
|
|
- **OQ-TN-12**: hub re-produce composition (deferred; a hub proxy
|
|
re-exporting a consumed resource would add a
|
|
`register_openable_with_establisher` whose establisher opens a
|
|
consumer channel; no concrete consumer yet. The re-produce policy
|
|
sentence is pinned: the hub's ACL governs the final dial, per ADR-006
|
|
Amendment 1)
|
|
- **OQ-TN-13**: UDP truncation (resolved, fail-loud, ADR-003)
|
|
- **OQ-TN-15/16**: resolved (ADR-007/008: `open_direct`,
|
|
`TunnelListener`, the forwarded op)
|
|
|
|
## References
|
|
|
|
- [producer.md](producer.md) (the serving side's shapes),
|
|
[wire.md](wire.md) (the data plane)
|
|
- Reverse POC `docs/research/reverse-poc-summary.md` (the seed shape:
|
|
`ReverseTunnel` open_and_pump / close / join_and_reap; W3, W4)
|
|
- `docs/research/tunnels-graduation.md` (the graduation research)
|
|
- Upstream asks (review 008): `alkcall/docs/reviews/
|
|
008-tunnels-graduation-upstream-review.md`; landed in alkcall
|
|
0.8.0
|
|
- alkcall ADR-047 §5 (allocation; the initiator adopts), ADR-050
|
|
(`pump_bidi`), ledger
|
|
CF-005/CF-006 (identity on the reverse path) |