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)
268 lines
14 KiB
Markdown
268 lines
14 KiB
Markdown
---
|
||
status: complete
|
||
last_updated: 2026-09-07
|
||
---
|
||
|
||
# alktunnels: Reverse-Flow (`-R`) Tunnel POC Research Summary
|
||
|
||
**Status:** Research complete — the `-R` shape (the hub opens tunnel
|
||
channels *toward* a connect-side worker that serves its own open op)
|
||
validated end-to-end over alkcall 0.6.0 (2026-09-07 pass, 14 tests)
|
||
and re-validated over alkcall 0.7.0 (16 tests) after the CF-005/CF-006
|
||
identity remediation landed — the W1 `auth_token` workaround is dead;
|
||
the primary caller-identity path is transport identity. Clippy
|
||
`-D warnings` clean; fmt clean. The review 007 non-finding trace
|
||
("no upstream mechanism is missing") is confirmed by execution —
|
||
and the POC surfaced two real findings the forward POC could not
|
||
(one an upstream sharpness question — F-1, documented upstream in
|
||
0.7.0; one a spec-mandate — F-2).
|
||
**Date:** 2026-09-07
|
||
**Scope:** Validates the OQ-TN-10 #2 POC — the SSH `tcpip-forward`
|
||
template (register → per-accept channel-open → cancel) mapped onto
|
||
alkcall's both-sides serving semantics. Probes four wrinkles (W1..W4)
|
||
selected from the mechanism trace; findings F-1/F-2 below.
|
||
|
||
---
|
||
|
||
## Executive summary
|
||
|
||
A POC (`alktunnels-reverse-poc`, `/workspace/alktunnels-reverse-poc`)
|
||
validated the reverse flow end-to-end over alkcall 0.6.0 (re-validated
|
||
over 0.7.0 — see §W1):
|
||
|
||
1. **The worker (connect side, serving side)** — dials the transport,
|
||
`ChannelClient::from_connection_with_serving` +
|
||
`ChannelOperations::register_on` (generic channel ops on the
|
||
serving registry, so the hub can `channel/close`) + post-hoc
|
||
`register_openable_with_establisher` (W2). Its establisher dials
|
||
the resource target and returns the handle via
|
||
`Establishment::new(plan)` — R-01's plan flow, no handoff map.
|
||
Its pump handler awaits `alkcall::channels::pump_bidi` inline
|
||
(R-02/R-03). This is ADR-022 §2 both-sides semantics exercised
|
||
for real: connect side AND serving side.
|
||
2. **The hub (accept side, reverse-flow initiator)** —
|
||
`ChannelsAdapter` + install hook that captures the hub's channel-0
|
||
`CallConnection` and `ChannelManager`. Per local accept: call the
|
||
worker's open op (`call_with_payload` — the caller identity rides
|
||
the transport identity by default (0.7.0); the payload
|
||
`auth_token` is the optional hub-forwarding path), adopt the
|
||
worker-allocated channel ID, pump the accepted socket against the
|
||
adopted halves via `pump_bidi` spawned locally.
|
||
3. **The ADR-047 §5 orientation under test holds:** the reverse
|
||
channels are allocated by the worker (connect side — odd IDs) and
|
||
adopted by the hub (accept side). "Connection owner allocates"
|
||
exercised in the Pub-like orientation.
|
||
4. **16 tests (0.7.0 pass):** TCP round trip, 1 MiB backpressure,
|
||
half-close semantics, concurrent same-resource channels (the R-01
|
||
race-killer proven: two opens of one resource, distinct channels,
|
||
distinct dialed handles), UDP round trip, TCP+UDP concurrent,
|
||
typed establishment errors (`unknown_resource`, `dial_failed`),
|
||
the identity precedence chain (transport identity alone /
|
||
`ServingConfig.identity` override / token override / identity-less
|
||
fail-closed), CF-006 per-call opener witness, out-of-band
|
||
`channel/close`, worker outbound calls resolving while serving,
|
||
late-registration visibility, and two finding-pinning tests
|
||
(F-1 compiler-pinned, F-2 below).
|
||
|
||
## Wrinkle probes (W1..W4) — results
|
||
|
||
- **W1 identity on the serving path — CONFIRMED 2026-09-07 against
|
||
0.6.0; RESOLVED by alkcall 0.7.0 (ledger CF-005 + CF-006).**
|
||
Original finding: `from_connection_with_serving` built channel 0
|
||
internally; its `Connection::identity()` was empty and
|
||
`ServingConfig` had no auth capture point — the serving dispatcher
|
||
resolved the caller identity ONLY from the payload `auth_token` →
|
||
`ServingConfig.identity_provider`, and the establisher/pump handler
|
||
received the install-time `AuthContext`, never the per-call opener.
|
||
**The 0.7.0 remediation (validated from this POC's topology, 16
|
||
tests):**
|
||
- `ServingConfig.identity: Option<Identity>` — the explicit
|
||
override (remediation (a)).
|
||
- The transport `Connection::identity()` (set before dialing — the
|
||
mTLS/QUIC posture) propagates to channel 0 automatically
|
||
(remediation (b)). Validated: the POC's default harness now
|
||
authorizes the scope-gated open on transport identity ALONE — no
|
||
token payload.
|
||
- Precedence (alkcall cf005 tests + POC probes): payload token >
|
||
`ServingConfig.identity` > transport identity; none → fail-closed
|
||
`FORBIDDEN` (validated).
|
||
- CF-006 corollary: the establisher/pump handler now receive the
|
||
**per-call** opener identity (validated via an identity witness —
|
||
the establisher saw `hub`, and the override identity under (a)).
|
||
On hub-forwarded opens the establisher sees the end client, not
|
||
the hub.
|
||
Status: resolved — the POC's `auth_token` workaround is dead code;
|
||
the primary path is transport identity.
|
||
- **W2 post-hoc openable registration — works.** The worker registers
|
||
its openable AFTER `from_connection_with_serving` returns; the
|
||
dispatcher reads through the shared `Arc<OperationRegistry>` per
|
||
dispatch. Proven by every open succeeding + a direct
|
||
`registration()` assertion. No finding — the assembly order
|
||
(construct client → register ops → serve) is viable as-is.
|
||
- **W3 adopter self-reaping — confirmed asymmetry, POC-local fix.**
|
||
The serving side's channel is wrapper-managed (teardown cascades on
|
||
pump completion or out-of-band close). The ADOPTING side has no
|
||
such machinery: `adopt_channel` installs routing state nothing
|
||
awaits. The hub must hold its pump handle and reap
|
||
(`teardown_channel`) itself — `ReverseTunnel::join_and_reap`/`close`
|
||
in the POC. Spec note: the real crate's consumer half needs a
|
||
session type with the same ownership (the adopted channel + pump
|
||
handle + reaper). Not an upstream gap (the hub's lifecycle is
|
||
assembly-layer by design, OQ-TN-04) — but it is the reason
|
||
`TunnelSession` in the spec must own teardown explicitly.
|
||
- **W4 EOF sentinel propagation — works both ways.** Half-close
|
||
semantics test: local `shutdown()` → EOF sentinel into the mux →
|
||
worker pump sees the zero-length read → shuts the target write
|
||
down; the target's echo still flows back before the reverse leg
|
||
completes. `pump_bidi`'s shutdown-on-completion contract holds
|
||
across the reverse path unchanged.
|
||
|
||
## Findings
|
||
|
||
### F-1 — `ChannelPlan`'s `Send + Sync` bound constrains plan payloads (compiler-pinned)
|
||
|
||
`ChannelPlan = Arc<dyn Any + Send + Sync>` (ADR-049 amendment 2).
|
||
A dialed-socket handle whose halves are
|
||
`Box<dyn AsyncRead + Send + Unpin>` does NOT satisfy the bound —
|
||
`Arc<T>` requires `T: Send + Sync`, and `dyn AsyncRead` is not
|
||
`Sync` by default. The POC widened the halves to `+ Sync` (socket
|
||
halves are `Sync`, so dial-shaped payloads are unaffected).
|
||
Implications for Phase 1: tunnel plan payloads (target handles) must
|
||
be `Sync`, which is fine for socket-backed substrates but would
|
||
exclude non-Sync handles (e.g. process pipes as boxed trait objects)
|
||
without a wrapper. **Documented upstream 2026-09-07 (alkcall 0.7.0):
|
||
the `ChannelPlan` type doc now carries the `Send + Sync` payload
|
||
constraint.** No further action needed — the bound is correct (the
|
||
plan crosses the wrapper task boundary).
|
||
|
||
### F-2 — empty UDP datagrams collide with the EOF sentinel in raw pass-through (spec-mandate)
|
||
|
||
The raw pass-through pump shape (`tokio::io::copy` between the UdpHalf
|
||
adapter and the channel) cannot carry an empty datagram: `poll_recv`
|
||
returning zero bytes is `Ok(0)` from the adapter — indistinguishable
|
||
from EOF for `copy`, which treats it as end-of-stream and shuts the
|
||
pump down. An empty datagram and the zero-length EOF sentinel are the
|
||
SAME wire shape at the pump level. Pinned by test
|
||
(`reverse_udp_empty_datagram_finding` asserts the non-round-trip so
|
||
the conclusion is executable, not anecdotal).
|
||
Consequence: the forward POC's codec decision — `[len: u16 BE]`
|
||
framing for UDP — is now validated from a SECOND angle: not only does
|
||
it preserve boundaries, it is MANDATORY for correctness (an empty
|
||
datagram is 2 bytes under the codec, unambiguous with EOF). The
|
||
Phase 1 spec must state: UDP rides the length-framed codec, never raw
|
||
pass-through. Stream substrates (TCP, unix) keep raw pass-through —
|
||
a zero-length read there is genuinely EOF.
|
||
|
||
## The `-R` registration template (SSH tcpip-forward analogue)
|
||
|
||
SSH: global request `tcpip-forward` → per-accept `forwarded-tcpip`
|
||
channel opens toward the forwarder → `cancel-tcpip-forward`.
|
||
|
||
Mapped onto the validated surface (OQ-TN-03's role-follows-resource
|
||
model — no protocol-level direction):
|
||
|
||
| SSH | alktunnels reverse shape (validated) |
|
||
| --- | --- |
|
||
| `tcpip-forward` registration | hub binds the listener locally (OQ-TN-04: binding is assembly-layer; no advertisement op needed for the hub's OWN listeners) |
|
||
| server-side accept loop | hub accept loop (assembly layer) |
|
||
| per-accept `forwarded-tcpip` open | hub calls the worker's served open op per accept (`call_with_payload`) + `adopt_channel` |
|
||
| target dial | worker's establisher dials the resource (establishment phase — typed errors) |
|
||
| `cancel-tcpip-forward` | hub closes: pump teardown + `teardown_channel` (and/or `channel/close` out-of-band on the worker) |
|
||
| originator pair (informational) | not needed — ACL rides the channels open-op machinery |
|
||
|
||
Key validation: **no advertisement surface is needed in the base
|
||
crate.** The "please listen on your side" step of SSH's template
|
||
dissolves when the listener is the hub's own local resource — the hub
|
||
does not ask the worker to listen; the worker serves open ops, and
|
||
the hub opens channels whenever its own accepts fire. An advertisement
|
||
op only becomes necessary if the LISTENER lives on the far side
|
||
("worker, expose port 8080 on your end") — which is the worker
|
||
exposing a produced resource = the forward shape with the listener as
|
||
the resource (producer-side listen is an establisher variant, not a
|
||
new mechanism). This closes the last open thread of OQ-TN-03's
|
||
original question set (the `-R` advertisement/lifecycle surface).
|
||
|
||
## What was built
|
||
|
||
```
|
||
alktunnels-reverse-poc/
|
||
Cargo.toml — alkcall 0.6.0; tokio wasm-clean subset + net (POC-local)
|
||
src/
|
||
lib.rs — module docs; wrinkle overview
|
||
producer.rs — worker half: tunnel_open_spec, tunnel_establisher
|
||
(Establishment::new(plan) — R-01; per-call
|
||
identity witness — CF-006), make_tunnel_
|
||
pump_handler (pump_bidi inline — R-02/03),
|
||
register_tunnel_openable (post-hoc, W2)
|
||
producer/
|
||
params.rs — TunnelParams {resource, substrate} + the UdpHalf
|
||
adapter (Send + Sync — F-1)
|
||
consumer.rs — hub half: open_reverse_channel (call_with_payload
|
||
with optional auth_token — now the fallback
|
||
path; primary is transport identity, 0.7.0),
|
||
ReverseTunnel::open_and_pump (adopt +
|
||
pump_bidi), join_and_reap/close (W3),
|
||
bi_stream_from_halves
|
||
harness.rs — wire()/wire_with(): worker =
|
||
from_connection_with_serving (transport identity
|
||
+ ServingConfig identity/provider — the 0.7.0
|
||
precedence chain) + ChannelOperations::
|
||
register_on + post-hoc openable; hub = adapter +
|
||
capturing install hook (manager + CallConnection)
|
||
tests/
|
||
tunnel_poc.rs — 16 tests (see Executive summary + §W1)
|
||
```
|
||
|
||
## What the POC does NOT validate
|
||
|
||
1. **A real transport** — `tokio::io::duplex` stands in for
|
||
TCP/TLS/QUIC (unchanged from the forward POC scope).
|
||
2. **The advertisement op for far-side listeners** — resolved as
|
||
"not needed" (see the template table); if a deploy mode ever wants
|
||
"worker, expose a port on YOUR side," that is a producer-side
|
||
listen establisher over the same open op — spec-shaped, not POC-
|
||
shaped.
|
||
3. **Unix socket + stdio substrates** — OQ-TN-10 #3 (cheap; the pump
|
||
is substrate-agnostic and both POCs now confirm it). Phase 1
|
||
resolution: unix in via the `local` feature; stdio OUT — alktty's
|
||
pipe mode owns process stdio (OQ-TN-14).
|
||
4. ~~**W1 remediation**~~ — **resolved 2026-09-07 (alkcall 0.7.0)**:
|
||
the remediation IS now exercised from this POC's topology
|
||
(transport identity, `ServingConfig.identity` override, precedence,
|
||
fail-closed, CF-006 witness). Remaining un-exercised: fingerprint-
|
||
based identity resolution (`resolve_from_fingerprint` — the POC's
|
||
duplex transport has no TLS layer to fingerprint).
|
||
5. **Multi-hop (hub relay traversal)** — single hop; relay forwarding
|
||
(alkcall ADR-042) is transparent per leg.
|
||
6. **Wasm** — POC-local `tokio/net`; the real crate's protocol-only
|
||
core stays wasm-clean (unchanged conclusion).
|
||
|
||
## Verification
|
||
|
||
```
|
||
0.6.0 pass: 14 tests; clippy -D warnings clean; fmt clean.
|
||
0.7.0 pass: 16 tests (W1 remediation probes added, foreign-token
|
||
probe obsoleted by the precedence chain); clippy -D
|
||
warnings clean; fmt clean; repeat-run stable (3×).
|
||
```
|
||
|
||
## References
|
||
|
||
- alkcall 0.6.0 — review 007 remediation as consumed here: ADR-049
|
||
amendment 2 (`Establishment::new(plan)`, R-01), ADR-050
|
||
(`channels::pump_bidi`, R-03), R-02 JoinHandle contract.
|
||
- alkcall review 007 §Part B — the reverse-flow non-finding trace
|
||
(`from_connection_with_serving` + `register_on` + serving-side
|
||
allocation) — confirmed by execution.
|
||
- alkcall ADR-022 §2 — both-sides serving semantics (the worker's
|
||
shape). ADR-047 §5 — connection-owner allocation (the odd-ID
|
||
orientation). ADR-037/ADR-042 — channel ops, relay.
|
||
- alktunnels `docs/research/phase-0-findings.md` — OQ-TN-03 (direction
|
||
semantics — the advertisement thread this POC closes), OQ-TN-04
|
||
(no forced binding — the hub binds, the worker never does),
|
||
OQ-TN-09 (half-open residual — validated W4), OQ-TN-10 #2 (this
|
||
POC).
|
||
- alktunnels `docs/research/ssh-socks5-survey.md` §RFC 4254 §7.1 —
|
||
the `tcpip-forward` template mapped in the table above.
|
||
- forward POC (`poc-summary.md`) — the codec decision F-2 strengthens;
|
||
the UdpHalf adapter reused; the plan flow replaces `HandleHandoff`. |