- Cargo.toml: 0.4.1 -> 0.5.0 (minor, not patch: the release adds public API — ForwardedGate, ForwardedPlan, register_tunnel_forwarded_openable_with_establisher — additive only; the base registration stays byte-for-byte, the wire delta is empty) - CHANGELOG: Unreleased cut to 0.5.0 (the establisher-variant entry + the release paragraph; the compare links updated) - README: the forwarded-op serving snippet shows the recommended gate shape (register_tunnel_forwarded_openable_with_establisher) beside the base one Review 003 W1 absorbed in-repo; W2/W3 are the alkssh/alksocks repos' adoption waves. Verification: cargo test (default + --all-features), fuzz corpus replay (fuzz/shared), clippy --all-targets -D warnings (native + wasm32), wasm32 check, fmt --check, cargo doc --no-deps, taskgraph validate (33 tasks), cargo publish --dry-run --allow-dirty all green
22 KiB
Changelog
All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.
Unreleased
0.5.0 - 2026-11-03
The correlation-gate release: the forwarded op's serving half gains an
establishment phase (review 003 W1 absorbed in-repo, ADR-008 Amendment
2), prompted by alkssh's consumer input on the composed -R
deployment's failure vocabulary. Additive public API only (the minor
bump, not a patch); the wire delta is empty and the base registration
stays byte-for-byte. The rest of the wave is doc-only: the em-dash
prose sweep across repo docs, spec docs, doc comments, and task files
(no code-path change), and review 003's doc trail closure (W2/W3 are
the sibling repos' adoption waves and remain booked there).
Added
- The forwarded-op establisher variant (
register_tunnel_forwarded_openable_with_establisher+ForwardedGate+ForwardedPlan, ADR-008 Amendment 2): the serving side ofchannels/tunnel/forwardedgains an establishment phase: the assembly injects a correlation gate (ForwardedGate, a sync closure over the parsedTunnelForwardedParams) and the crate wraps it into the alkcallOpenEstablisherdance, so a stale/missinglistentoken fails the open as a typedchannel:open_failedwithdetails.reason == "unknown_resource"BEFORE any channel is born (no phantom channel, extending ADR-049's posture to the forwarded op's serving side; the wire vocabulary is unextended). An establisher parse failure is likewise typed (handler_error), an improvement over the base shape's success-then-silent-birth-teardown for malformed ingress. Gate order pinned: the registry runs thetunnel:forwardedACL before the establisher (pinned by test with a gate-invocation witness). The baseregister_tunnel_forwarded_openablestays byte-for-byte compatible as the compatibility + residual-race-fallback posture (the narrow gate-pass→handler-runcorrelation-missrace keeps teardown-at-birth); the establisher variant is the recommended shape and new assemblies take it by default. The wire delta is empty. Five gate tests pin the typed miss/no-phantom/no-handler, the unchanged gate-pass serving flow (full byte round-trip), the residual-race reap, the scope-gate ordering, and the malformed-params delta.
0.4.1 - 2026-10-22
The dependency-patch release: the alkcall substrate bumped 0.10.0 →
0.10.1 (the adoption-window ordering fix). No alktunnels API change,
no wire change; the only file-level contacts with the bump are
Cargo.toml/Cargo.lock.
Changed
- alkcall 0.10.0 → 0.10.1 (crates.io). The bug-fix release
(review 013, routed from alkgit's wave-2 gate): the
adopt_channelinstall→drain window is now one atomic section, so a chunk routed by the demux inside the window can no longer be delivered ahead of earlier-parked early arrivals (the observed downstream shape was pkt-line desync in alkgit's open-op integration test under multi-process load), and a related loss variant (a chunk parked after the drain removed the queue, parked forever) is closed. Relevant in principle to this crate's reverse-path adopt flow (the bind-first establisher surfaces channels through the same adopt path), though no change in this crate's own surfaces was needed. No API, wire-format (EventEnvelope, the 8-byte chunk header), or documented-cap behavior change upstream: the fix surface is private; the behavior only strengthens. 136 tests pass; wasm-clean verified (check + clippy onwasm32-unknown-unknown).
0.4.0 - 2026-10-19
The dependency-major release: the alkcall substrate bumped 0.9.0 →
0.10.0 (the gateway ownership-seam release). No alktunnels API
change, no wire change, and zero source changes; the only file-level
contacts with the bump are Cargo.toml/Cargo.lock. Minor rather
than patch for the 0.3.0 reason: the semver-incompatible dependency
bump plus this crate's public re-exports of alkcall types
(ChannelOpenError; Establishment/EstablishmentError shapes in
the producer API) make it semver-visible to downstream consumers.
Changed
- alkcall 0.9.0 → 0.10.0 (crates.io). The gateway
ownership-seam release (review 012):
GatewayDispatchgainswith_ownership_provider/with_static_ownership_fallback, and the previously-silent static fallback becomes loud: an unwired spine refuses (an error envelope,FORBIDDEN, pre-dispatch) an op whose spec carriesaccess_control.resource_type. None of it is on this crate's paths: alktunnels does not use thegatewayfeature orGatewayDispatch; tunnel opens serve viaChannelCore::register_openable/register_openable_with_establisherregistry dispatch on the native wire, which 0.10.0 leaves untouched. All of this crate's alkcall contact points (register_openable/register_openable_with_establisher,Establishment::new,OpenHandler,ChannelClient::open_channel_with_reply,ManagerError::ChannelExists,ChannelOpenError) are unchanged between 0.9.0 and 0.10.0. 136 tests pass; wasm-clean verified (check + clippy onwasm32-unknown-unknown).
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), andop/registerforward-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 genericop/registerannouncements), so no behavior change here. Of the 0.9.0 breaking list,Establishmentdropping itsDebugderive was the only surface this crate touched: twobindfirst_establisherunit tests'expect_errcalls and one integration test's untypedpanic!("{other:?}")fall-through restructured (the error type itself remainsDebug; theOkarm now formats viapanic!("got Ok")).ManagerError::InvalidAdopt(additive),RelayError::PubTypedOpenremoval, and theregister_imported/register_imported_allreturn 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), andsession_pumps(the wave-2 stateful target: op sequences over a live channels stack per exec, theTunnelSessiondata planes mirrored, no-wedge + shutdown-on-completion + lossless p2c + EOF-sentinel teardown + thependingbacklog bound asserted). Invariant logic lives infuzz/shared/: a stable-toolchain crate replayed as plaincargo test, so the committed corpora (96 + 8 seeds, plus the campaign-promotedseed-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 UDPpendingcount (the datagrams decoded from a chunk beyond the one returned), the wave-2 harness's asserted backlog bound made caller-visible;Nonefor 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 thedatagram_codectarget.
Changed
- alkcall 0.8.0 → 0.8.1 (crates.io). The bug-fix release: a
duplicate
adopt_channel/open_channelno longer destroys the live channel it collided with (found by alkcall's ownmanager_routingfuzz 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 ofTunnelSession:TunnelListener::open(ridesChannelClient::open_channel_with_reply, alkcall 0.8.0's U-2 surface: the full reply arrives and the bind-firstbound/listenfields project through the lenientlisten_reply_fields; adopt-free construction, since the client already adopted the channel),bound()/listen()(the Option posture:Nonefor 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 viashutdown, then the reap) andDrop(the held write half drops → the mux pump's implicit-EOF chunk → the far accept dispatcher's teardown signal; sync reap, so nothing leaks). The heldBiStream's write half stays IDLE and private (a listen channel has no data-plane role; pumping one is meaningless). NoClone(compile-asserted). Forwarded sessions are NOT owned by the listener; they arrive as ordinary adopted-sideTunnelSessions on the opener's manager (adoptgains nothing, pinned by test); the assembly correlates them via thelisten()token (the receipt queue is assembly-side bookkeeping). -
The
localfeature's bind-first substrate half (ADR-008 §1, shape 3), the real-socket sources behind the committedBindFn/BindFirstAcceptLoopcontracts (behindlocal; the default crate stays wasm-clean):bind_tcp_first(port 0 resolved vialocal_addr, theboundreply field's data) +TcpListenerHalves::accept_pair(one accept →(peer, halves); the peer ridesaccept()'s return, the forwarded op'speercarrier); unix bind-first parity shipped (OQ-TN-14):bind_unix+UnixListenerHalves(local_addr, the queue-feedingaccept_loop, the bind-firstaccept_pair, where an unnamed (autobind) peer fails loud: the forwardedpeerparam requires an address) +bind_unix_first; the assembly convenienceslocal_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) andlocal_bindfirst_accept_loop()(downcasts the plan's opaque listener to the module's wrappers; a foreign listener type fails loud ashandler_error). The accept-FIRST shapes (accept_loop+AcceptQueue) are untouched. The direct-opDialFnreuse is verified + pinned:local_dialis shared unchanged (the UDP framed wrap happens inside the closure, review 001 U-1); aTunnelSession::open_direct+Substrate::Udptest 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 taking the parsed params + the channelBiStream; 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_biFIRST (R-02 accepted-flag telemetry), then wrapper→assembly bridge, the injected handler'sJoinHandleawaited inline); +register_tunnel_forwarded_openable(register_openable, so no establisher, the data plane starts immediately; scope gatetunnel:forwarded, ingress; a separate grant fromtunnel:open/tunnel:direct, never implied by either). Pluslisten_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 withTunnelListener). 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, and 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+listenreply fields viaEstablishment::with_reply_field→ theBindFirstPlanplan payload; the token mint is the pinned discipline: non-zero- seededAtomicU32, per-open fresh, per-producer unique, token 0 never minted);BindFirstAcceptLoop(the type-erased accept closure; it receives the opaque listener, yields(peer, halves)per accept);ForwardedOpener(the per-accept forwarded-open caller, the call plane:call_with_payloadon channel 0 +manager.adopt_channel, the reverse POC's shape);make_bindfirst_accept_handler(the dispatcher:accept_bifirst per R-02, the idle listen-stream EOF cancels the loop and tears the listener down, per-acceptpump_biditasks tracked by the handler'sJoinHandle, meaning 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:openscope gate as the dial/listen variants, the per-registration establisher posture; the injected seams rideBindFirstInjections {bind, accept_loop, open_forwarded}+ the optional CF-006 identity witness). The opener-side forwarded-op serving shape (register_openable, no establisher) landed withtunnels/forwarded-receipt. -
The forwarded op + the bind-first params surface (ADR-008 as amended): the wire-stable types pin first (the ordering
tunnels/paramsused for the base op):OP_TUNNEL_FORWARDED/TUNNEL_FORWARDED_SCOPEconstants;TunnelForwardedParams {listen, substrate, peer}(deny-unknown;listenis the producer-minted correlation token, positive by wire contract, i.e. schemaminimum: 1+ a serde-level pin) with a per-substratepeersub-schema (malformed peers fail the registry's schema gate before the opener-side handler);tunnel_forwarded_spec()(Sub-typed, External,alk/tunnelALPN marker, no establisher, so the forwarded data plane starts immediately);TunnelListenReply+parse_listen_reply(the typed projection of the bind-first listen open's additivebound/listenreply fields: unknown reply fields tolerated, malformed ones fail loud via the newTunnelError::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 sshdirect-tcpip/ SOCKS5 CONNECT shape), riding the samealk/tunnelALPN:SubstrateAddr(the untagged substrate-shaped address object, also the forwarded op'speershape) +TunnelDirectParams {substrate, target}with a per-substrateinput_schema(malformed targets fail the registry's schema gate asINVALID_INPUTbefore the establisher),tunnel_direct_spec(),direct_establisher(+ witness variant; no registry lookup, sounknown_resourcecan never fire; targets render to the same backing-string form the registry backings use and ride the SAME injectedDialFn),register_tunnel_direct_ openable(same pump handler + plan-flow as the base op), andTunnelSession::open_direct(separate constructor;tunnel:directis a separate grant fromtunnel: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-firstboundcarrier) and flavor-form open-op ids in discovery (ADR-047 amendment 3:channels/tunnel/direct-shaped ops reconstruct theirchannel_openmarker 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, withboundflowing 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. -
BindFirstPlangainssubstrate: Substrate(review 002 M-1 remediation); the accept dispatcher's silentunwrap_or(Substrate::Tcp)fallback (the secondTunnelParamsre-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 stampTcpinto 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 externalBindFirstPlanconsumer yet). Pinned byforwarded_opens_carry_plan_stamped_substrate(a UDP bind-first flow whose forwarded opens carrysubstrate: udpfrom 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/subopen op on thealk/tunnelALPN; params{resource, substrate}(deny_unknown_fields; ADR-001); thetunnel:openscope gate (ADR-006). - Data-plane codec (ADR-003): raw pass-through for stream
substrates (
tcp,unix); mandatory[len: u16 BE]boundary- preserving framing forudp(len = 0a legal empty datagram, never EOF; oversize rejected at frame time; truncation fails loud per OQ-TN-13). The BAST contract:docs/architecture/bast.md. - Producer half (ADR-004):
ResourceRegistry,tunnel_establisher(dial shape) andlisten_establisher(the-Rfar-side accept shape) over alkcall's establishment surface (ADR-049),make_tunnel_pump_handler(thepump_biditwo-pump contract, alkcall ADR-050), and theregister_tunnel_openable/register_tunnel_listen_openableregistration helpers. - Consumer half (ADR-005):
TunnelSessionwithopen(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_channelfor the hub-call path. localfeature (non-wasm):dial_tcp,bind_tcp+TcpListenerHalvesaccept loop,connect_udpwrapped in the FRAMEDUdpHalfadapter (the codec composes at the substrate boundary, per review 001 U-1),dial_unix, and the all-substratelocal_dialclosure. 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, theAcceptQueuelost-wakeup regression, the identity-precedence matrix). Wasm-clean default crate verified (wasm32-unknown-unknowncheck + clippy).