release: v0.2.0 — the graduation (direct op + bind-first/forwarded metadata flow)
- 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
This commit is contained in:
1 parent
20834d297e
commit
d789031863
4 files changed
+126
-13
No files matched your search
+7
-3
@@ -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
|
||||
Generated
+1
-1
@@ -48,7 +48,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alktunnels"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"alkcall",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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<SubstrateAddr> — informational, NOT addressing
|
||||
let token = listener.listen(); // Option<u32> — 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
|
||||
|
||||
Reference in new issue
Block a user