# Changelog All notable changes to this crate are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this crate adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] ## [0.3.0] - 2026-10-14 The dependency-major release: the alkcall substrate bumped 0.8.1 → 0.9.0 (the stub-sweep release). No alktunnels API change, no wire change; the only code contact with the 0.9.0 breaking surface was test-side (`Establishment` no longer derives `Debug` — three `expect_err`/`panic!` sites restructured), which is why this is a minor bump rather than a patch: the semver-incompatible dependency bump plus this crate's public re-exports of alkcall types make it semver-visible to downstream consumers (matching the 0.7.1 → 0.8.0 ride on 0.2.0). ### Changed - **alkcall 0.8.1 → 0.9.0** (crates.io). The stub-sweep release: the marked-pub open contract (S-01), `channel/control` (S-02), `channel/resources/subscribe` (S-03), and `op/register` forward-through (S-04) replace the four always-failing stubs. None of the four stubs were on this crate's paths (no marked-pub registration, no control-handle subscription, no resource enumerator use, no generic `op/register` announcements), so no behavior change here. Of the 0.9.0 breaking list, `Establishment` dropping its `Debug` derive was the only surface this crate touched — two `bindfirst_establisher` unit tests' `expect_err` calls and one integration test's untyped `panic!("{other:?}")` fall-through restructured (the error type itself remains `Debug`; the `Ok` arm now formats via `panic!("got Ok")`). `ManagerError::InvalidAdopt` (additive), `RelayError::PubTypedOpen` removal, and the `register_imported`/`register_imported_all` return change are untouched by this crate's API use. 136 tests pass; wasm-clean verified. ## [0.2.1] - 2026-09-28 The hardening release: fuzzing adopted from the alkcall pattern (`fuzz/` workspace, excluded from the published package) and the alkcall substrate bumped 0.8.0 → 0.8.1. No API break, no wire change. ### Added - **Fuzzing (`fuzz/` workspace, `docs/research/fuzzing.md`)** — three cargo-fuzz targets over this crate's own attacker-reachable surfaces (the envelope/chunk/manager layer is upstream and fuzzed there per alkcall 0.8.1): `datagram_codec` (the ADR-003 codec: no-panic, infallible decode, size bound, F-2 sentinel layering, re-frame round-trip, retained-bytes bound), `params_parse` (the serde params/reply parsers: no-panic on raw bytes and bounded structured values, clean typed failure, round-trips), and `session_pumps` (the wave-2 stateful target: op sequences over a live channels stack per exec, the `TunnelSession` data planes mirrored, no-wedge + shutdown-on-completion + lossless p2c + EOF-sentinel teardown + the `pending` backlog bound asserted). Invariant logic lives in `fuzz/shared/` — a stable-toolchain crate replayed as plain `cargo test`, so the committed corpora (96 + 8 seeds, plus the campaign-promoted `seed-900`) stay executable without nightly. `fuzz/` is excluded from the workspace and from the published package. Campaigns (detached, fork mode): ~25M+228k execs across waves 1–2, zero crashes/hangs/OOMs; all findings were harness-model defects, no crate bugs. - **`TunnelSession::datagram_backlog()`** — the UDP `pending` count (the datagrams decoded from a chunk beyond the one returned), the wave-2 harness's asserted backlog bound made caller-visible; `None` for stream sessions or after the halves were taken. - **`DatagramReader::retained_bytes()`** — the decoder-state byte count (leftover buffer + the in-flight partial datagram); post-feed bounded by one frame (≤ `MAX_DATAGRAM_LEN` + 2), pinned by the `datagram_codec` target. ### Changed - **alkcall 0.8.0 → 0.8.1** (crates.io). The bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer destroys the live channel it collided with (found by alkcall's own `manager_routing` fuzz target) — directly relevant to this crate's reverse-path adopt flow. No alktunnels API or wire change in this bump; the full suite passes unchanged. ## [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 - **`TunnelListener` — the listen-flow consumer session (ADR-008 §4)** — the listen-flow sibling of `TunnelSession`: `TunnelListener::open` (rides `ChannelClient::open_channel_with_reply`, alkcall 0.8.0's U-2 surface — the full reply arrives and the bind-first `bound`/ `listen` fields project through the lenient `listen_reply_fields`; adopt-free construction — the client already adopted the channel), `bound()`/`listen()` (the Option posture — `None` for dial/accept-first producers and old producers), and the listen channel's teardown ownership (ADR-005's posture extended to the listen flow): `close` (the graceful EOF sentinel via `shutdown`, then the reap) and `Drop` (the held write half drops → the mux pump's implicit-EOF chunk → the far accept dispatcher's teardown signal; sync reap — never leaks). The held `BiStream`'s write half stays IDLE and private (a listen channel has no data-plane role; pumping one is meaningless). No `Clone` (compile-asserted). Forwarded sessions are NOT owned by the listener — they arrive as ordinary adopted-side `TunnelSession`s on the opener's manager (`adopt` gains nothing, pinned by test); the assembly correlates them via the `listen()` token (the receipt queue is assembly-side bookkeeping). - **The `local` feature's bind-first substrate half (ADR-008 §1, shape 3)** — the real-socket sources behind the committed `BindFn`/`BindFirstAcceptLoop` contracts (behind `local`; the default crate stays wasm-clean): `bind_tcp_first` (port 0 resolved via `local_addr` — the `bound` reply field's data) + `TcpListenerHalves::accept_pair` (one accept → `(peer, halves)` — the peer rides `accept()`'s return, the forwarded op's `peer` carrier); unix bind-first parity shipped (OQ-TN-14): `bind_unix` + `UnixListenerHalves` (`local_addr`, the queue-feeding `accept_loop`, the bind-first `accept_pair` — an unnamed (autobind) peer fails loud: the forwarded `peer` param requires an address) + `bind_unix_first`; the assembly conveniences `local_bind_first()` (tcp/unix dispatch; **udp fails loud — a UDP socket has no accept, the bind-first shape is not defined there, never a fake listener**) and `local_bindfirst_accept_loop()` (downcasts the plan's opaque listener to the module's wrappers; a foreign listener type fails loud as `handler_error`). The accept-FIRST shapes (`accept_loop` + `AcceptQueue`) are untouched. The direct-op `DialFn` reuse is verified + pinned: `local_dial` is shared unchanged (the UDP framed wrap happens inside the closure — review 001 U-1); a `TunnelSession::open_direct` + `Substrate::Udp` test drives a REAL UDP echo socket through the direct op's establisher path. - **The forwarded op's SERVING side + the lenient reply projection (ADR-008 §3 as amended)** — the listen-opener half, the receipt counterpart to the bind-first producer (the opener SERVES the op; the producer CALLS it per accept): `ForwardedHandler` (the injected assembly handler — the parsed params + the channel `BiStream`; the assembly owns the pump, since the local halves are assembly state) + `make_forwarded_op_handler` (the built-in wrapper: deny-unknown params parse (a malformed open fails LOUD → the wrapper returns early → teardown-at-birth reaps the channel — no phantom, the correct shape for a malformed ingress), `accept_bi` FIRST (R-02 accepted-flag telemetry), then wrapper→assembly bridge, the injected handler's `JoinHandle` awaited inline); + `register_tunnel_forwarded_openable` (`register_openable` — no establisher, the data plane starts immediately; scope gate `tunnel:forwarded`, ingress — a separate grant from `tunnel:open`/`tunnel:direct`, never implied by either). Plus `listen_reply_fields` (the lenient, infallible session-surface projection of the bind-first reply — unknown extra reply fields tolerated, a malformed shape logs loud and surfaces both-`None`; shared with `TunnelListener`). Correlation-miss teardown semantics documented + test-pinned: the assembly holds the map, its error path drops the stream, the channel reaps (the producer's accept loop drops the accepted connection on the open's failure — the shapes mirror). - **The bind-first establisher + accept dispatcher (shape 3, ADR-008 as amended)** — the `-R`/ingress establisher whose establishment phase BINDS (the address-first-vs-accept-first deadlock dissolves; the accept wait lives in the teardown-governed pump phase): `BindFn` (the substrate bind function — the platform boundary, the protocol never binds) + `BoundListener` (the opaque listener + the real OS-chosen bound address); `bindfirst_establisher` (registry namespace gate → token mint → bind → `bound` + `listen` reply fields via `Establishment::with_reply_field` → the `BindFirstPlan` plan payload; the token mint is the pinned discipline — non-zero- seeded `AtomicU32`, per-open fresh, per-producer unique, token 0 never minted); `BindFirstAcceptLoop` (the type-erased accept closure — receives the opaque listener, yields `(peer, halves)` per accept); `ForwardedOpener` (the per-accept forwarded-open caller — the call plane: `call_with_payload` on channel 0 + `manager.adopt_channel`, the reverse POC's shape); `make_bindfirst_accept_handler` (the dispatcher: `accept_bi` first per R-02, the idle listen-stream EOF cancels the loop and tears the listener down, per-accept `pump_bidi` tasks tracked by the handler's `JoinHandle` — a forwarded pump never outlives its listener; forwarded-open failures are per-accept isolated — log + drop, the loop continues, v1 no retry); `register_tunnel_bindfirst_openable` (same spec + `tunnel:open` scope gate as the dial/listen variants — the per-registration establisher posture; the injected seams ride `BindFirstInjections {bind, accept_loop, open_forwarded}` + the optional CF-006 identity witness). The opener-side forwarded-op serving shape (`register_openable`, no establisher) landed with `tunnels/forwarded-receipt`. - **The forwarded op + the bind-first params surface (ADR-008 as amended)** — the wire-stable types pin first (the ordering `tunnels/params` used for the base op): `OP_TUNNEL_FORWARDED` / `TUNNEL_FORWARDED_SCOPE` constants; `TunnelForwardedParams {listen, substrate, peer}` (deny-unknown; `listen` is the producer-minted correlation token, positive by wire contract — schema `minimum: 1` + a serde-level pin) with a per-substrate `peer` sub-schema (malformed peers fail the registry's schema gate before the opener-side handler); `tunnel_forwarded_spec()` (`Sub`-typed, External, `alk/tunnel` ALPN marker, no establisher — the forwarded data plane starts immediately); `TunnelListenReply` + `parse_listen_reply` (the typed projection of the bind-first listen open's additive `bound`/`listen` reply fields — unknown reply fields tolerated, malformed ones fail loud via the new `TunnelError::MalformedReply`); `BindFirstPlan` (the opaque assembly-typed listener + the token — the plan payload the bind-first establisher/accept-dispatcher task consumes). - **The direct op (`channels/tunnel/direct`, ADR-007)** — dynamic- target egress (the ssh `direct-tcpip` / SOCKS5 CONNECT shape), riding the same `alk/tunnel` ALPN: `SubstrateAddr` (the untagged substrate-shaped address object, also the forwarded op's `peer` shape) + `TunnelDirectParams {substrate, target}` with a per-substrate `input_schema` (malformed targets fail the registry's schema gate — `INVALID_INPUT` — before the establisher), `tunnel_direct_spec()`, `direct_establisher` (+ witness variant; no registry lookup — `unknown_resource` can never fire; targets render to the same backing-string form the registry backings use and ride the SAME injected `DialFn`), `register_tunnel_direct_ openable` (same pump handler + plan-flow as the base op), and `TunnelSession::open_direct` (separate constructor — `tunnel:direct` is a separate grant from `tunnel:open`, never implied by it; the session type, data planes, and teardown API are identical). An old producer rejects the unknown op loudly (`NOT_FOUND`). ### Changed - **alkcall 0.7.1 → 0.8.0** (crates.io). The two upstream asks the graduation ADRs depend on landed (alkcall review 008): the establisher → reply projection (`Establishment::with_reply_field`, ADR-049 amendment 3 — the bind-first `bound` carrier) and flavor-form open-op ids in discovery (ADR-047 amendment 3 — `channels/tunnel/direct`-shaped ops reconstruct their `channel_open` marker through discovery, so hub relay wraps them as channels, not plain forwarding stubs). Plus the bonus: `ChannelRelay` + `HubLegTemplate` (ADR-051) — the hub re-produce machinery the graduation's re-produce story assumed would be assembly-layer is now in-tree, with `bound` flowing end-to-end per-hop-truthful (ADR-051 §6, ADR-008 §6's sentence realized). No alktunnels API or wire change in this bump; 71/91 tests pass, wasm-clean verified. - **`BindFirstPlan` gains `substrate: Substrate` (review 002 M-1 remediation)** — the accept dispatcher's silent `unwrap_or(Substrate::Tcp)` fallback (the second `TunnelParams` re-parse of wire input) is removed: the establisher stamps the parsed substrate into the plan (one parse, one source of truth) and the dispatcher reads it from the plan it already downcast. A drift can no longer silently stamp `Tcp` into a UDP listen tunnel's forwarded opens (a raw-vs-framed data-plane mismatch on the opener side). Plan-payload change is pre-consumer (no external `BindFirstPlan` consumer yet). Pinned by `forwarded_opens_carry_plan_stamped_substrate` (a UDP bind-first flow whose forwarded opens carry `substrate: udp` from the plan). ## [0.1.0] - 2026-09-10 The initial release: the v1 tunnel protocol implementation over alkcall 0.7.x channels, spec-complete (Phase 1) and implementation- complete (Phase 2, review 001 findings remediated). ### Added - **Wire surface** (one-way-door stable from this release): the `channels/tunnel/sub` open op on the `alk/tunnel` ALPN; params `{resource, substrate}` (`deny_unknown_fields`; ADR-001); the `tunnel:open` scope gate (ADR-006). - **Data-plane codec** (ADR-003): raw pass-through for stream substrates (`tcp`, `unix`); mandatory `[len: u16 BE]` boundary- preserving framing for `udp` (`len = 0` a legal empty datagram, never EOF; oversize rejected at frame time; truncation fails loud — OQ-TN-13). The BAST contract: `docs/architecture/bast.md`. - **Producer half** (ADR-004): `ResourceRegistry`, `tunnel_establisher` (dial shape) and `listen_establisher` (the `-R` far-side accept shape) over alkcall's establishment surface (ADR-049), `make_tunnel_pump_handler` (the `pump_bidi` two-pump contract, alkcall ADR-050), and the `register_tunnel_openable` / `register_tunnel_listen_openable` registration helpers. - **Consumer half** (ADR-005): `TunnelSession` — `open` (forward) / `adopt` (reverse), substrate-shaped data planes (`stream_halves`, `take_halves`, `send_datagram`/`recv_datagram`), pump ownership (`pump_against`), teardown ownership (`close`/`join`/`Drop`); `open_reverse_channel` for the hub-call path. - **`local` feature** (non-wasm): `dial_tcp`, `bind_tcp` + `TcpListenerHalves` accept loop, `connect_udp` wrapped in the FRAMED `UdpHalf` adapter (the codec composes at the substrate boundary — review 001 U-1), `dial_unix`, and the all-substrate `local_dial` closure. Stdio intentionally out of scope (alktty's pipe mode owns process stdio — OQ-TN-14). - **Identity seam** (alkcall 0.7.0 CF-005/CF-006): payload-token > `ServingConfig.identity` > transport identity precedence proven end-to-end; the per-call opener identity reaches the establisher. - **Test suite**: 71 tests default / 91 with `local`, including the review 001 gates (real-socket UDP framing probes, the `AcceptQueue` lost-wakeup regression, the identity-precedence matrix). Wasm-clean default crate verified (`wasm32-unknown-unknown` check + clippy). [Unreleased]: https://git.alk.dev/alkdev/alktunnels/compare/v0.3.0...HEAD [0.3.0]: https://git.alk.dev/alkdev/alktunnels/compare/v0.2.1...v0.3.0 [0.2.1]: https://git.alk.dev/alkdev/alktunnels/compare/v0.2.0...v0.2.1 [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