POC at /workspace/alktunnels-reverse-poc (14 tests, clippy/fmt clean, repeat-run stable) over alkcall 0.6.0: - The review 007 non-finding trace confirmed by execution: the hub (accept side) opens tunnel channels toward a connect-side worker serving its own open op (from_connection_with_serving + ChannelOperations::register_on + post-hoc openable registration) - ADR-047 §5 exercised in the Pub-like orientation: the worker (connect side) allocates odd IDs; the hub adopts - R-01 plan flow under concurrency: two same-resource opens, distinct channels, distinct dialed handles — the forward POC's handoff race is structurally gone - OQ-TN-03's last open thread closed: no advertisement op needed — the listener is the initiator's own local resource; a far-side listener is a producer-side listen establisher over the same open op - W4 EOF sentinel propagation validated (half-close semantics); W3 adopter self-reaping pinned as a TunnelSession spec requirement; W2 post-hoc registration viable Findings: - F-1: ChannelPlan's Send+Sync bound constrains plan payloads (socket halves carry + Sync; spec should pin the constraint) - F-2: empty UDP datagrams collide with the EOF sentinel in raw pass-through (pinned by an executable test) — the length-framed codec is mandatory for UDP; raw pass-through stays stream-substrate-only. Second validation of the codec decision; OQ-TN-02 mandate strengthened W1 (connect-side serving path resolves caller identity only via payload auth_token) filed upstream as alkcall consumer-findings ledger CF-005 (alkcall 0d287a9). Verification: cargo test 14/14, clippy --all-targets -D warnings, fmt --check — all clean, repeat-run stable
237 lines
12 KiB
Markdown
237 lines
12 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. 14 tests pass; 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, one a spec-mandate).
|
||
**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:
|
||
|
||
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` + `auth_token`), 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. **14 tests:** 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`,
|
||
`FORBIDDEN` ×2), 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, upstream question.**
|
||
`from_connection_with_serving` builds channel 0 internally; its
|
||
`Connection::identity()` is empty and `ServingConfig` has no auth
|
||
capture point. The serving dispatcher resolves the caller identity
|
||
ONLY from the payload `auth_token` →
|
||
`ServingConfig.identity_provider` (verified: scoped token → open
|
||
proceeds; absent/foreign token → `FORBIDDEN`). The establisher and
|
||
pump handler receive the connection-establishment-time `AuthContext`
|
||
closed over at `register_openable` — NOT the per-call opener.
|
||
Consequences: (a) a scope-gated reverse-flow open op is satisfiable
|
||
today only via payload tokens (hub-side: attach
|
||
`auth_token` to `call_with_payload`); (b) transport-level
|
||
authentication (mTLS/QUIC peer identity) never reaches the
|
||
open-op ACL on the connect-side serving path. This is the same
|
||
asymmetry the accept side resolved with the install hook's
|
||
`channel0_conn.set_identity`. Candidate follow-ups: `ServingConfig`
|
||
gaining an identity capture, or documented token-only posture.
|
||
→ alkcall review ledger (W1).
|
||
- **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. The spec should pin "plan payloads are
|
||
`Send + Sync`" as a documented constraint of the establisher API.
|
||
No upstream change 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), 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
|
||
+ auth_token — W1), ReverseTunnel::open_and_pump
|
||
(adopt + pump_bidi), join_and_reap/close (W3),
|
||
bi_stream_from_halves
|
||
harness.rs — wire(): worker = from_connection_with_serving
|
||
(TokenIdProvider — W1) + ChannelOperations::
|
||
register_on + post-hoc openable; hub = adapter +
|
||
capturing install hook (manager + CallConnection)
|
||
tests/
|
||
tunnel_poc.rs — 14 tests (see Executive summary)
|
||
```
|
||
|
||
## 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).
|
||
4. **W1 remediation** — only the token path is exercised; whether
|
||
alkcall should grow a serving-side identity capture is an upstream
|
||
question (W1), not resolved here.
|
||
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
|
||
|
||
```
|
||
14 tests pass; clippy --all-targets -D warnings clean; fmt clean.
|
||
Repeat-run stable (3× clean).
|
||
```
|
||
|
||
## 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`. |