diff --git a/AGENTS.md b/AGENTS.md index 1382355..ecb54ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,14 +167,22 @@ just spawned implementation agents. `alkcall::core`. Do not vendor copies into this crate. alkcall is v0.5.x — breaking changes are expected at this major-zero stage; this is an early consumer, so we find and fix issues upstream - rather than working around them. Pin `alkcall = "0.6.0"` and bump + rather than working around them. Pin `alkcall = "0.7.0"` and bump deliberately. The 0.6.0 establishment surface (ADR-049 + amendment 2) is load-bearing for this crate: tunnel opens use `register_openable_with_establisher` so a refused target dial is a typed `channel:open_failed` call error, never a phantom channel, and the establisher returns the dialed handle via - `Establishment::new(plan)` (typed-opaque `ChannelPlan`) — no - side-channel handoff. + `Establishment::new(plan)` (typed-opaque `ChannelPlan` — its + `Send + Sync` bound constrains plan payloads; socket handles carry + `+ Sync`) — no side-channel handoff. The 0.7.0 identity surface + (ledger CF-005/CF-006) is load-bearing for the serving side: a + connect-side serving op resolves the caller identity in + precedence order — payload `auth_token` > + `ServingConfig.identity` > transport identity (set via + `Connection::set_identity` before dialing) — and the establisher/ + pump handler receive the per-call opener identity, not the + install-time context. 12. **BAST document for the wire format** — when the tunnel wire format gains binary framing (if any beyond pass-through), it carries a @@ -286,18 +294,20 @@ non-backend module changes. channel 0; `params` carries the tunnel target) - alkcall ADR-042 — hub relay (byte-for-byte data-channel forwarding with ID rewrite — tunnels traverse relays transparently) + - alkcall ledger CF-005/CF-006 (0.7.0) — the connect-side serving + identity seam: caller identity precedence (token > + `ServingConfig.identity` > transport), per-call opener identity + on the open-op hooks (the reverse-flow POC's W1, resolved) - alknet ADR-085 — workspace scope: `alknet/tunnel` was flagged "POC-validated, minimal spec needed, not yet specced"; this crate is that spec -- What the POC does NOT settle (open work for Phase 0/1): - - UDP and Unix-socket substrates (the POC only exercised TCP) — the - pump pattern is expected to generalize, but datagram boundary - preservation and addressing bookkeeping are unspecced - - The "no forced local binding" requirement (SSH `-R`/dynamic flows) - is not covered by the POC at all - - Target addressing format (what a tunnel `params` looks like) — - alknet ADR-071 §ALPN table noted `alknet/tunnel` as `[0, 1]` data - in/out only, but the addressing scheme was never decided +- What the POCs settled (Phase 0 complete — 2026-09-07): both POCs + ran clean (forward UDP POC + reverse-flow POC; see + `docs/research/poc-summary.md` and `reverse-poc-summary.md`). The + remaining Phase 1 work is the spec: OQ promotion, params JSON ADR, + ALPN ADR, codec ADR (UDP framing mandatory per F-2), and the + producer/consumer API surface (the consumer session type owns + teardown — reverse POC W3). - If a TODO references a design direction that an ADR has since decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0a2b1d0..909ad12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ dependencies = [ [[package]] name = "alkcall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44500900b82904d2a870e444051fc041a042e42ff7190803bbc5b3973f4feeb" +checksum = "5162a10144df65b03b887e30f77eb30d956ca1ca460a3f81b409128cbcae3199" dependencies = [ "async-trait", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 4ba11f4..8c004bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ name = "alktunnels" default = [] [dependencies] -alkcall = "0.6.0" +alkcall = "0.7.0" tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros", "time"] } bytes = "1" futures = "0.3" diff --git a/docs/research/phase-0-findings.md b/docs/research/phase-0-findings.md index 3598040..3d7d7d7 100644 --- a/docs/research/phase-0-findings.md +++ b/docs/research/phase-0-findings.md @@ -773,11 +773,15 @@ Phase 0 may need (in rough priority order, per the SDD process's closes: no advertisement op needed — the listener is the hub's own local resource; a far-side listener is a producer-side listen establisher over the same open op. Findings: F-1 (plan payloads - must be `Send + Sync`), F-2 (UDP raw pass-through cannot carry - empty datagrams — the length-framed codec is mandatory, second - validation of the codec decision). W1 (connect-side serving path - resolves identity only via payload `auth_token`) filed for the - alkcall ledger. + must be `Send + Sync` — documented upstream in 0.7.0), F-2 (UDP + raw pass-through cannot carry empty datagrams — the length-framed + codec is mandatory, second validation of the codec decision). W1 + (connect-side serving path resolves identity only via payload + `auth_token`) filed for the alkcall ledger — **RESOLVED by alkcall + 0.7.0 (CF-005/CF-006)**; the POC re-validated over 0.7.0 (16 + tests): transport identity authorizes alone, `ServingConfig. + identity` overrides, identity-less fails closed, and the + establisher sees the per-call opener identity. 3. **Unix socket + stdio bridge POC** — cheap; validates "substrate agnostic" beyond IP substrates. 4. **Two-pump helper extraction spike** — OQ-TN-06, only after 1–3. @@ -880,11 +884,12 @@ Candidate reading for the research specialist (to be expanded): - [x] Targeted POC(s) run + summary (OQ-TN-10) — UDP POC DONE (2026-09-06, `poc-summary.md`); reverse-flow POC DONE (2026-09-07, `reverse-poc-summary.md` — the `-R` shape validated - end-to-end over alkcall 0.6.0; findings F-1/F-2; W1 filed for - the upstream ledger). Remaining items (#3 unix/stdio, #4 helper - spike) are spec-scope: #3 folds into the real crate's tests - (pump agnosticism confirmed twice), #4 resolved by alkcall 0.6.0 - (ADR-050) + end-to-end over alkcall 0.6.0, re-validated over 0.7.0 with the + CF-005/CF-006 identity remediation; findings F-1/F-2; W1 filed + upstream and RESOLVED). Remaining items (#3 unix/stdio, #4 + helper spike) are spec-scope: #3 folds into the real crate's + tests (pump agnosticism confirmed twice), #4 resolved by + alkcall 0.6.0 (ADR-050) - [x] Two-pump helper convergence input gathered (OQ-TN-06) — the POC gives both shapes (producer `pump_halves`, consumer `take_halves` + copy): same shape modulo channel side; the diff --git a/docs/research/reverse-poc-summary.md b/docs/research/reverse-poc-summary.md index 9413d35..590f80c 100644 --- a/docs/research/reverse-poc-summary.md +++ b/docs/research/reverse-poc-summary.md @@ -7,11 +7,15 @@ last_updated: 2026-09-07 **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 +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, one a spec-mandate). +(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 @@ -23,7 +27,8 @@ 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: +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` + @@ -38,42 +43,56 @@ validated the reverse flow end-to-end over alkcall 0.6.0: 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'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. **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). +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, 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). +- **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` — 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` per @@ -111,10 +130,10 @@ 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). +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) @@ -171,22 +190,27 @@ alktunnels-reverse-poc/ src/ lib.rs — module docs; wrinkle overview producer.rs — worker half: tunnel_open_spec, tunnel_establisher - (Establishment::new(plan) — R-01), make_tunnel_ + (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 - + auth_token — W1), ReverseTunnel::open_and_pump - (adopt + pump_bidi), join_and_reap/close (W3), + 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(): worker = from_connection_with_serving - (TokenIdProvider — W1) + ChannelOperations:: + 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 — 14 tests (see Executive summary) + tunnel_poc.rs — 16 tests (see Executive summary + §W1) ``` ## What the POC does NOT validate @@ -200,9 +224,12 @@ alktunnels-reverse-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. +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 @@ -211,8 +238,10 @@ alktunnels-reverse-poc/ ## Verification ``` -14 tests pass; clippy --all-targets -D warnings clean; fmt clean. -Repeat-run stable (3× clean). +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