- Cargo.toml: alkcall 0.8.1 -> 0.9.0 (crates.io, the stub-sweep
release) and 0.2.1 -> 0.3.0 (breaking dep bump + public alkcall
re-exports -> minor, matching the 0.7.1 -> 0.8.0 ride on 0.2.0)
- fuzz/shared/Cargo.toml: alkcall 0.9.0 (path-linked alktunnels must
see one alkcall version)
- src/producer.rs + tests/producer_direct_op.rs: the only code contact
with the 0.9.0 breaking surface — Establishment no longer derives
Debug; two expect_err calls -> .err().expect, one untyped
panic!("{other:?}") fall-through -> typed Err/Ok arms
- CHANGELOG: 0.3.0 section + compare links
- README: the 0.3.0 note
- docs/architecture/overview.md: dep tree alkcall 0.9.0
Verification: cargo test (136 default / 163 with local), fuzz/shared
corpus replay (0 pass, empty target set — no regressions), clippy
--all-targets -D warnings (native + wasm32-unknown-unknown), fmt
--check, wasm32 check, and cargo publish --dry-run --allow-dirty all
green
17 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.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 — 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 — never 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— 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 — 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— 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 — 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 — 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— 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 — 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 — 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 —INVALID_INPUT— before the establisher),tunnel_direct_spec(),direct_establisher(+ witness variant; no registry lookup —unknown_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 — 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):
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_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 — 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).