Files
alktunnels/docs/research/reverse-poc-summary.md
T
glm-5.3-flash 603f398f6d 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
2026-09-07 10:06:42 +00:00

12 KiB
Raw Blame History

status, last_updated
status last_updated
complete 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_tokenServingConfig.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 transporttokio::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.