From d789031863ca61c0634ba93232b8a85e652485e7 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sun, 20 Sep 2026 09:30:21 +0000 Subject: [PATCH] =?UTF-8?q?release:=20v0.2.0=20=E2=80=94=20the=20graduatio?= =?UTF-8?q?n=20(direct=20op=20+=20bind-first/forwarded=20metadata=20flow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README sync: the direct op (ADR-007), the bind-first establisher + the forwarded op (ADR-008), TunnelListener + the reply-field projection, the 0.2 summary section, the local bind-first helpers - CHANGELOG: Unreleased cut to 0.2.0 (the graduation + the alkcall 0.8.0 bump; compare links updated) - Cargo.toml: 0.1.0 -> 0.2.0 Verification: cargo test (135 pass default / 162 with local), clippy (all targets, -D warnings, both feature sets), fmt, doc, the wasm32-unknown-unknown check + clippy, and cargo publish --dry-run --allow-dirty all green --- CHANGELOG.md | 10 +++-- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 126 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4daf3..ff7e3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ this crate adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] -The graduation implementation (the direct op, listen metadata + the -forwarded op — ADRs 007/008) landed — see Added below. +## [0.2.0] - 2026-09-20 + +The graduation release: the direct op + the bind-first/forwarded +metadata flow (ADRs 007/008), over alkcall 0.8.0. Composed against by +alksocks (the SOCKS5 layer) and the planned `alknodes` assembly. ### Added @@ -218,5 +221,6 @@ complete (Phase 2, review 001 findings remediated). matrix). Wasm-clean default crate verified (`wasm32-unknown-unknown` check + clippy). -[Unreleased]: https://git.alk.dev/alkdev/alktunnels/compare/v0.1.0...HEAD +[Unreleased]: https://git.alk.dev/alkdev/alktunnels/compare/v0.2.0...HEAD +[0.2.0]: https://git.alk.dev/alkdev/alktunnels/compare/v0.1.0...v0.2.0 [0.1.0]: https://git.alk.dev/alkdev/alktunnels/releases/tag/v0.1.0 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 53547aa..13b75bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,7 +48,7 @@ dependencies = [ [[package]] name = "alktunnels" -version = "0.1.0" +version = "0.2.0" dependencies = [ "alkcall", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 71dcfa0..3946f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alktunnels" -version = "0.1.0" +version = "0.2.0" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index d593fad..2dfc1d2 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,14 @@ transport dependencies; the transport (QUIC, websocket, stdio, ...) is whatever alkcall runs over. Downstream crates compose on top of it: wire your substrate's sockets -into the producer's dial/accept closures (the `local` feature ships -reference helpers), and open typed sessions from the consumer side. +into the producer's dial/accept/bind closures (the `local` feature +ships reference helpers), and open typed sessions from the consumer +side. Dynamic-target egress (`ssh -W`/SOCKS5 CONNECT shape) rides the +[`direct op`](https://docs.rs/alktunnels/latest/alktunnels/params/constant.OP_TUNNEL_DIRECT.html) +(ADR-007); the composed `-R` flow (bind + peer metadata) rides the +bind-first establisher + the +[`forwarded op`](https://docs.rs/alktunnels/latest/alktunnels/params/constant.OP_TUNNEL_FORWARDED.html) +(ADR-008). ## Quick start @@ -76,6 +82,38 @@ let accept: AcceptFn = /* pops from the queue (or your own source) */; register_tunnel_listen_openable(&core, ®istry, &op_registry, auth, accept)?; ``` +### Producer — bind-first listen shape (`-R` with address + peer fidelity) + +The same open op with a bind-first establisher: establishment BINDS +(the reply carries the real OS-chosen `bound` address + a correlation +token), then every local accept opens the `channels/tunnel/forwarded` +op toward the listen opener with the real peer address. + +```rust +use alktunnels::producer::{ + register_tunnel_bindfirst_openable, BindFirstInjections, +}; + +let injections = BindFirstInjections { + // Binds the listener, returns it + the OS-chosen bound address. + // Behind `local`: alktunnels::local::local_bind_first() (tcp/unix). + bind, + // Per accept: yields (peer, halves) from the opaque listener. + // Behind `local`: alktunnels::local::local_bindfirst_accept_loop(). + accept_loop, + // Opens the forwarded op on the channel-0 call plane + adopts. + open_forwarded, +}; +register_tunnel_bindfirst_openable(&core, ®istry, &op_registry, auth, injections, None)?; + +// The forwarded op's opener-side serving half (on the consumer's +// connection): correlate the token, pump against the local socket. +register_tunnel_forwarded_openable( + &core, &op_registry, auth, + /* ForwardedHandler: (params, BiStream) -> pump task */, +)?; +``` + ### Consumer — open a tunnel channel (the `-L` path) ```rust @@ -135,6 +173,45 @@ let session = TunnelSession::adopt( session = session.pump_against(accepted_local_stream).await; ``` +### Consumer — dynamic target (the direct op, `ssh -W`/CONNECT shape) + +```rust +use alktunnels::params::SubstrateAddr; +use alktunnels::TunnelSession; + +// `tunnel:direct` is a SEPARATE grant from `tunnel:open` (never +// implied by it) — the caller states which capability it holds. A +// malformed target fails the schema gate; an old producer rejects +// the unknown op loudly (NOT_FOUND). +let mut session = TunnelSession::open_direct( + &client, + SubstrateAddr::HostPort { host: "example.internal".into(), port: 5432 }, + Substrate::Tcp, +).await?; +``` + +### Consumer — the listen flow (bind-first reply fields) + +```rust +use alktunnels::TunnelListener; + +// The same `channels/tunnel/sub` open op — the establisher shape is +// the producer's per-registration choice, invisible here. A bind-first +// producer's reply carries `bound` (the real OS-chosen address) and a +// correlation token; both are `None` for dial/accept-first producers +// or old producers (the lenient, additive posture). +let listener = TunnelListener::open(&client, params).await?; +let bound = listener.bound().cloned(); // Option — informational, NOT addressing +let token = listener.listen(); // Option — the forwarded op's correlation key + +// Teardown: `close` sends the EOF sentinel (the far accept dispatcher +// tears its listener down), then reaps; `Drop` is the sync best-effort +// form. Forwarded sessions arrive as separate adopted `TunnelSession`s +// on the manager — the assembly correlates them via the token; the +// listener does not own them. +listener.close().await; +``` + ## Architecture Two roles, symmetric per the alk convention (avoid "server"/"client": @@ -142,8 +219,9 @@ both sides of a connection can be either; role follows the resource): | Role | Via `alk/channels` | |------|--------------------| -| **Producer** (owns the backing: local ports, sockets, in-process services) | [`register_tunnel_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_openable.html) / [`register_tunnel_listen_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_listen_openable.html) — the open op (`channels/tunnel/sub`) with a **dial establisher** (target dialing) or a **listen establisher** (pops an accepted connection); the pump handler bridges the channel via [`alkcall::channels::pump_bidi`](https://docs.rs/alkcall/latest/alkcall/channels/pump/fn.pump_bidi.html) | -| **Consumer** (wants the bytes) | [`TunnelSession`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html) — [`open`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html#method.open) (forward) / [`adopt`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html#method.adopt) (reverse), substrate-shaped data planes, teardown ownership | +| **Producer** (owns the backing: local ports, sockets, in-process services) | [`register_tunnel_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_openable.html) / [`register_tunnel_listen_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_listen_openable.html) / [`register_tunnel_bindfirst_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_bindfirst_openable.html) — the open op (`channels/tunnel/sub`) with a **dial establisher** (target dialing), an **accept-first listen establisher** (pops an already-accepted connection), or a **bind-first establisher** (binds; the reply carries `bound` + token, per-accept forwarded opens carry the peer); the pump handler bridges the channel via [`alkcall::channels::pump_bidi`](https://docs.rs/alkcall/latest/alkcall/channels/pump/fn.pump_bidi.html) | +| **Producer** (dynamic-target egress) | [`register_tunnel_direct_openable`](https://docs.rs/alktunnels/latest/alktunnels/producer/fn.register_tunnel_direct_openable.html) — the direct op (`channels/tunnel/direct`, ADR-007) with its own scope (`tunnel:direct`); the target rides the params, no registry lookup | +| **Consumer** (wants the bytes) | [`TunnelSession`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html) — [`open`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html#method.open) (forward) / [`open_direct`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html#method.open_direct) (dynamic target) / [`adopt`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelSession.html#method.adopt) (reverse), substrate-shaped data planes, teardown ownership; [`TunnelListener`](https://docs.rs/alktunnels/latest/alktunnels/consumer/struct.TunnelListener.html) — the listen flow with the `bound`/`listen` reply fields | One connection hosts many tunnels (one per channel); sessions are independent. Both roles are callable from either connection side — @@ -151,9 +229,9 @@ independent. Both roles are callable from either connection side — machines (OQ-TN-03; the reverse POC validated the worker-serves / hub-initiates shape). -### A tunnel is a resource, not an address +### A tunnel is a resource, not an address (with one additive exception) -The open-op params are wire-stable (ADR-001) and deliberately +The base open-op params are wire-stable (ADR-001) and deliberately address-free: ```json @@ -165,6 +243,20 @@ never learns an address. Rich addressing (SOCKS5 ATYP, per-datagram remotes) enters only through the `-D`/dynamic composition path, speaking its protocol inside the tunnel payload — never in the wire. +The exception is the direct op (ADR-007) — a *different capability* +on its own op + scope, so the ACL separation is structural: + +```json +{ "substrate": "tcp", "target": { "host": "10.0.0.7", "port": 5432 } } +``` + +Scope-gated separately (`tunnel:direct` ≠ `tunnel:open` ≠ +`tunnel:forwarded` — never implied by each other); dial-only (no +listen/direct-BIND form); the same `DialFn` the base op uses. The +composed `-R` metadata flow rides the forwarded op +(`channels/tunnel/forwarded`, `{listen, substrate, peer}` — the peer +is producer-asserted metadata, not transport truth). + ### Data plane - **Stream substrates (`tcp`, `unix`)**: raw pass-through — the @@ -191,12 +283,29 @@ The default crate (no features) compiles to `wasm32-unknown-unknown` feature (real sockets) needs a real OS and is non-wasm by design; enabling it on wasm is a compile error. +## Version 0.2 — the graduation + +0.2 rides alkcall 0.8.0 and graduates the proxy surface +(`docs/research/tunnels-graduation.md`, ADRs 007/008): + +- **The direct op** (`channels/tunnel/direct`, scope `tunnel:direct`) + — dynamic-target egress; `TunnelSession::open_direct`. +- **The bind-first establisher + the forwarded op** + (`channels/tunnel/forwarded`, scope `tunnel:forwarded`) — the + composed `-R` metadata flow: `bound` + correlation token in the + listen reply, `{listen, substrate, peer}` per accept; + `TunnelListener` + `parse_listen_reply`/`listen_reply_fields`. +- **alkcall 0.7.1 → 0.8.0** — `Establishment::with_reply_field` + + `open_channel_with_reply` (the bind-first carriers) and + flavor-form op ids in discovery (hub relay wraps them as channels); + bonus: in-tree `ChannelRelay`/`HubLegTemplate` (alkcall ADR-051). + ## Features | Feature | Contents | wasm | |---------|----------|------| -| *(default)* | params, wire codec, open-op spec, establishers (dial + listen), `TunnelSession`, registration helpers — protocol only | yes | -| `local` | [`dial_tcp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.dial_tcp.html) / [`bind_tcp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.bind_tcp.html) / [`connect_udp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.connect_udp.html) (the framed `UdpHalf`) / [`dial_unix`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.dial_unix.html) / [`local_dial`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.local_dial.html), the TCP accept loop | no | +| *(default)* | params, wire codec, open-op specs (`sub`/`direct`/`forwarded`), establishers (dial + accept-first listen + bind-first), `TunnelSession`, `TunnelListener`, registration helpers — protocol only | yes | +| `local` | [`dial_tcp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.dial_tcp.html) / [`bind_tcp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.bind_tcp.html) / [`connect_udp`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.connect_udp.html) (the framed `UdpHalf`) / [`dial_unix`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.dial_unix.html) / [`local_dial`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.local_dial.html), the TCP accept loop; the bind-first halves: [`bind_tcp_first`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.bind_tcp_first.html) / [`bind_unix_first`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.bind_unix_first.html) + [`local_bind_first`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.local_bind_first.html) + [`local_bindfirst_accept_loop`](https://docs.rs/alktunnels/latest/alktunnels/local/fn.local_bindfirst_accept_loop.html) | no | Stdio is intentionally out of scope — a spawned process's stdio is alktty's pipe mode (exit codes + signals are tty semantics); remote