- 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
13 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.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).