docs: reverse-flow POC summary — -R shape validated end-to-end
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
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
---
|
||||
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`.
|
||||
Reference in New Issue
Block a user