The Phase 0 OQ ledger (OQ-TN-01..10) promoted into
docs/architecture/open-questions.md (now OQ-TN-01..14, with four
new Phase 1 residues). Six ADRs and four spec docs, all Draft, all
decision-shaped per the SDD process (ADRs carry the WHY; specs carry
the WHAT and reference by number):
ADRs:
- 001 open-params-layout: params = {resource, substrate} — the
producer's stable name, not an address; wire-stable from the first
consumer; unknown substrates fail loudly at schema time
- 002 alpn-strategy: single alk/tunnel ALPN (option A); substrate in
params selects the framing; prior art (SSH/SOCKS5/udpgw) gives no
reason for the split
- 003 codec-and-udp-framing: raw pass-through (stream) / mandatory
[len: u16 BE] (UDP) — F-2 mandate recorded (the codec is mandatory
for correctness, not cosmetics); len=0 = legal empty datagram;
OQ-TN-13 resolved fail-loud (truncation)
- 004 no-backend-trait: halves functions at the assembly layer;
listen is an establisher shape; hub re-produce deferred(OQ-TN-12);
trait re-evaluated at the alknet ADR-078 convergence threshold
- 005 consumer-session-owns-teardown: TunnelSession with
open/adopt/stream_halves/take_halves/pump_against/close/join/Drop —
the W3 adopter gap closes structurally; Drop is the best-effort
fallback
- 006 access-control-posture: the open gate is the boundary;
TUNNEL_OPEN_SCOPE = tunnel:open (stable once published); op-level
ACL; ownership seam; no allowlists in v1; identity = 0.7.0's
precedence chain
Spec docs:
- overview.md: purpose, resource model, deps (alkcall 0.7.0,
wasm-clean default, local feature), module map
- wire.md: the open op (params/reply/typed errors), the data plane
by substrate, sentinels + half-close, byte diagrams
- producer.md: spec, establisher (dial + listen shapes), pump
handler (pump_bidi inline, R-02), registration API, ACL posture
- consumer.md: TunnelSession (forward open + reverse adopt
construction), data plane, teardown API (close/join/Drop incl.
pump-less join semantics)
- bast.md: the BAST doc for the UDP codec (convention 12's trigger
fired — the framing IS binary beyond pass-through)
- open-questions.md: OQ-TN-01..14; 01..10 promoted (faithful to the
phase-0 ledger's final states), 11 partially resolved (collision
domain = per-producer registry per ADR-001; lifecycle open),
12 deferred(scope), 13 resolved fail-loud (ADR-003), 14 open
Verified by an architecture-reviewer pass (2 criticals, 6 majors,
10 minors — all fixed: ADR-001/003-vs-OQ decision-state contradictions
resolved; the Layer-2 mislabel corrected to the ADR-047 §4 per-session
fork; alktty/alknet ADR misattributions fixed; codec placement pinned
to the establisher (pump stays substrate-agnostic); reverse-path
construction named (TunnelSession::adopt + pump_against); security
posture promoted to ADR-006; BAST doc written; F-2 rationale deduped;
README tables completed; channel_id > 0; MTU wording fixed; ADR-047 §5
attributions corrected; impacts lines added to unresolved OQs; review/
ledger pointer paths added to README references).
Verification: doc set internal cross-refs all resolve; cargo test,
fmt --check, doc --no-deps clean
alkcall 0.7.0 resolved every open downstream-consumer finding,
including this crate's reverse-flow POC W1 (filed as ledger CF-005,
with the CF-006 corollary):
- CF-005 (a): ServingConfig.identity — the explicit override
- CF-005 (b): transport Connection::identity() propagates to
channel 0 — set_identity before dialing is the mTLS/QUIC path
- CF-006: the establisher/pump handler receive the per-call opener
identity (dispatch-resolved), not the install-time context
- CF-007: ADR-016 code list completed (doc-only)
- F-1 (POC finding): the ChannelPlan type doc now carries the
Send + Sync payload constraint
alktunnels updates:
- Cargo.toml: alkcall 0.6.0 -> 0.7.0 (lockfile resolves 0.7.0)
- AGENTS.md convention 11: pin note updated; the 0.7.0 identity
surface marked load-bearing (precedence order + per-call opener)
- AGENTS.md architecture context: CF-005/CF-006 row added; the
'What the POC does NOT settle' list replaced with the accurate
settled state (Phase 0 complete; Phase 1 = the spec)
- reverse-poc-summary: W1 marked RESOLVED with the remediation
validated from the POC's topology (16 tests over 0.7.0: transport
identity alone authorizes, ServingConfig.identity overrides,
identity-less fails closed, CF-006 witness); F-1 marked documented
upstream; what-the-POC-does-not-validate and verification blocks
updated
- phase-0-findings OQ-TN-10: reverse POC entry notes the 0.7.0
re-validation and W1 resolution
The reverse POC (/workspace/alktunnels-reverse-poc, 8b35818) re-
validated: the default harness now authorizes on transport identity
alone — the 0.6.0-era auth_token workaround is dead code.
Verification: cargo test, clippy --all-targets -D warnings,
fmt --check, wasm32 check, doc --no-deps — all clean
POC at /workspace/alktunnels-reverse-poc (14 tests, clippy/fmt clean,
repeat-run stable) over alkcall 0.6.0:
- The review 007 non-finding trace confirmed by execution: the hub
(accept side) opens tunnel channels toward a connect-side worker
serving its own open op (from_connection_with_serving +
ChannelOperations::register_on + post-hoc openable registration)
- ADR-047 §5 exercised in the Pub-like orientation: the worker
(connect side) allocates odd IDs; the hub adopts
- R-01 plan flow under concurrency: two same-resource opens, distinct
channels, distinct dialed handles — the forward POC's handoff race
is structurally gone
- OQ-TN-03's last open thread closed: no advertisement op needed —
the listener is the initiator's own local resource; a far-side
listener is a producer-side listen establisher over the same open op
- W4 EOF sentinel propagation validated (half-close semantics);
W3 adopter self-reaping pinned as a TunnelSession spec requirement;
W2 post-hoc registration viable
Findings:
- F-1: ChannelPlan's Send+Sync bound constrains plan payloads
(socket halves carry + Sync; spec should pin the constraint)
- F-2: empty UDP datagrams collide with the EOF sentinel in raw
pass-through (pinned by an executable test) — the length-framed
codec is mandatory for UDP; raw pass-through stays
stream-substrate-only. Second validation of the codec decision;
OQ-TN-02 mandate strengthened
W1 (connect-side serving path resolves caller identity only via
payload auth_token) filed upstream as alkcall consumer-findings
ledger CF-005 (alkcall 0d287a9).
Verification: cargo test 14/14, clippy --all-targets -D warnings,
fmt --check — all clean, repeat-run stable
17 tests pass (7 codec + 10 integration). The producer/consumer
shape, ADR-049 establishment integration, codec decision, and typed
establishment errors all validated over alkcall 0.5.0 channels.
Key findings folded into the ledger:
- OQ-TN-05 resolved for stream substrates: no backend trait —
pump_halves generic over boxed halves (TCP into_split, UDP
UdpHalf adapter); remaining thread is only the hub re-produce
composition question
- OQ-TN-06 convergence test satisfied: producer pump and consumer
pump are the same shape modulo channel side; extraction decidable
in Phase 1
- New finding for the alkcall ledger: OpenHandler JoinHandle
semantics are load-bearing and undocumented — a handler that
returns before the data plane starts tears the channel down at
birth (the POC found this by hanging; the pump must be awaited
inline). Plus: Establishment should carry the channel plan
(the reserved extension point) to kill the establisher handoff
side-channel
- UdpHalf truncation semantics flagged for the Phase 1 ADR
(recommend fail-loud over silent truncation)
POC crate: /workspace/alktunnels-udp-poc (db224cc).
Structural rationale: alktty's 5-byte header is a sub-demux key for
five logical streams sharing one BiStream; a tunnel has exactly one
data stream per direction, so no type byte. TCP needs no length
prefix (channels already length-prefixes every chunk). UDP needs
boundary re-framing only, and u16 suffices (UDP max payload 65507).
Wire overhead table: tunnel TCP = 8 B/chunk total (channels header
only), tunnel UDP = 10 B/datagram, vs alktty's 13 B. udpgw
calibration: 5 B + per-datagram address.
Trade-offs accepted: no in-band control path for TCP channels ever
(post-ADR-049 this is clean; escape hatch = new ALPN, not a wire
change); sentinel collision does not exist (UDP len=0 = empty
datagram, legal; EOF is the channels-level sentinel on the BiStream
at a different layer); future multi-endpoint UDP gateway framing
lives inside datagram payloads (udpgw precedent).
Residual for the Phase 1 codec ADR: empty-datagram semantics; u16 BE
over varint (leaned, MTU-bounded anyway).
Research specialist survey (docs/research/ssh-socks5-survey.md):
SSH forwarding model (direct-tcpip/forwarded-tcpip payloads, open-
failure reason codes, tcpip-forward registration lifecycle), SOCKS5
(ATYP, CONNECT, UDP ASSOCIATE), error-vocabulary comparison table,
endpoint-at-open vs per-datagram analysis, and an explicit anti-
prior-art list (SSH window updates, packet-size negotiation, dual
channel numbering, SOCKS5 FRAG/auth/BND semantics, udpgw
KEEPALIVE/pooling — all mechanisms channels already owns or the
substrate change obsoletes).
Key corrections to phase-0 findings:
- OQ-TN-02 category error fixed: SSH has NO UDP forwarding at all;
the earlier '-D UDP associate over SSH' line was wrong (SSH -D
carries only the SOCKS5 TCP control connection). Real UDP-over-
stream prior art: tun2proxy udpgw + SOCKS5's own UDP relay.
- OpenSSH -D is protocol-level indistinguishable from -L (one
direct-tcpip channel per SOCKS CONNECT) — direct confirmation of
the -D = 'tunnel a socks5 connection' resolution.
OQ ledger updates:
- OQ-TN-01: minimal params shape supported ({resource, substrate});
direct-streamlocal is the extensibility template; ATYP not needed
in base params
- OQ-TN-02: RESOLVED as split by path — endpoint-at-open for base UDP
resources; per-datagram addressing only inside the -D payload;
channel ID replaces CONN_ID (no second demux); KEEPALIVE drops
- OQ-TN-07: option A (single alk/tunnel ALPN) strengthened
- OQ-TN-09: reason-code vocabulary from SSH's four codes + detail
string; udpgw ERR bit flagged as the counterexample to avoid
- OQ-TN-10: reverse-flow POC gains the tcpip-forward template
- Checklist: SSH/SOCKS5 survey item checked off
Worktree/branch for repo-dependent POCs; standalone crate in the
global workspace for self-contained POCs (alknet-channels-poc
precedent); findings always land in docs/research/.
- OQ-TN-08 residue updated with the upstream posture: we own alkcall,
upstream asks happen early (alktty/alkhttp precedent — both required
upstream fixes/additions; only 3 downstream dependents now, each
resolved issue makes future dependents cheaper). Ops-listing
metadata is expected to be a small upstream alkcall change, not a
workaround.
- OQ-TN-09: control-frame hunch accepted (self-contained frame,
alktty ADR-006 shape; dial errors are channel-closing, byte EOFs
stay per-direction); frame vocabulary follows the alktty pattern;
concrete set = Phase 1 ADR.
- New prior art section: the alktty stream-splitting pattern
(split BiStream -> ChunkReader/ChunkWriter -> pump to handle halves)
proposed as the general tunnel pattern — 'tty without the five
stream types'. Comparison table (tty vs TCP vs UDP codecs/handles/
pumps); implications for OQ-TN-09 (ctrl frames via stream types),
OQ-TN-05 (handle = boxed AsyncRead/AsyncWrite halves; UDP flow
table is the real design surface), OQ-TN-06 (helper convergence
test against pump_session may be nearly free). Open: codec choice
(reuse 5-byte / stripped / raw + UDP framing).
- Checklist updated accordingly.
Untangles the 'host owns the resource and proxies on top' posture for
tunnels:
- Protocol-crate levels assume the resource is owned by the other side
of the connection; alkcall's existing op-level ACL applies as-is. No
new policy layer, target allowlists, or tunnel-specific ownership
machinery. Proxy/overlay is a downstream (assembly-layer) concern
(hub workers expose tunnels; hub overlays them per-ACL).
- Discovery resolved: openable channels are operations (alkcall
ADR-047), so the existing bidirectional ACL-filtered ops listing IS
tunnel-resource discovery. A consumer learns a socks5/postgres/redis
tunnel is available from the same listing it already uses for ops.
- -D simplifies to 'just tunnel a socks5 connection': socks5 server
lives on the producing side; target selection happens in the socks5
protocol, not tunnel params; the dynamic-target policy residue
dissolves (socks5 resource ACL governs reachability).
Residue for Phase 1: per-resource metadata in the ops listing
(substrate type, name/description) — an alkcall ADR-047 interaction,
not a new mechanism. OQ-TN-01 discovery residue and the hub-model
residue list cross-linked accordingly.
Four corrections from discussion:
- ALPN prefix: alknet/ -> alk/ swap happened in alkcall v0.1.1; docs
must not perpetuate the old prefix (consumers would bake it in)
- XY problem: except in the -D/dynamic composition case, params only
identify a produced resource + substrate discriminator (tcp/udp/
extensible); the producer owns where the resource comes from (most
likely a local port); UDP must be structurally supported even
though TCP dominates expected use
- self-contained JSON open-op object accepted as the params path
(alktty NegotiateRequest precedent)
- russh has no UDP channel support at all — no direct-udp prior art;
noted in OQ-TN-02 and the survey list; SOCKS5 relevance scoped to
the -D composition path
Residue for OQ-TN-01: resource naming shape, discovery mechanism,
exact JSON layout (Phase 1 ADR before first consumer).
Captures the standing hub stance as prior art: the hub owns the
connection and explicitly proxies to expose resources for others.
Role follows the resource — whoever reaches the target is the producer
(registers openable channels), whoever wants bytes is the consumer.
SSH -L/-R collapse to the same producer/consumer pair with the entry
point on different machines (assembly-layer wiring); the exposed port
is a virtual ACL-scoped resource (register_openable shape), not a
bind; the hub proxy is a producer wrapping a consumer, terminating
and re-producing per hop so ACL applies per hop (distinct from
ADR-042 transparent relay); -D composes as a consumer opening
channels with per-connection dynamic targets.
OQ-TN-03 resolved. OQ-TN-04 mostly resolved (binding always
assembly-layer, optional, either side). OQ-TN-01 reframed: params
name a produced resource — resource naming + discovery added. OQ-TN-08
strengthened: register_openable per-resource registration is the
primary gate; remaining residue is the dynamic-target policy hook.
OQ-TN-05 gains the hub re-produce composition sub-question. OQ-TN-02
cross-linked: endpoint-at-open aligns with resource naming; per-
datagram addressing matches the -D composition path.
Documents the previously-undocumented UDP gateway prior art discussed
with the alknet-channels POC agent (/workspace/tun2proxy/src/udpgw.rs):
per-datagram length framing over a stream (LEN|FLAGS|CONN_ID|[SOCKS5
addr]|DATA), SOCKS5 ATYP addressing in-band, CONN_ID flow multiplexing,
keepalive/ERR flag packets, MTU cap, idle expiry. Folds implications
into OQ-TN-01 (addressing fork: per-channel vs per-datagram), OQ-TN-02
(framing mechanics production-proven; remaining fork documented),
OQ-TN-07 (single-ALPN option strengthened), OQ-TN-09 (flag-packet
vocabulary maps to establishment/error frame question), OQ-TN-10 (UDP
POC scope narrowed to channels-layer fit), and the survey checklist.
Also records the transport story: TCP vs QUIC is the alkcall layer's
concern; the tunnel crate is transport-agnostic.
Captures the ten open design questions from the setup discussion:
addressing format, UDP datagram semantics, -L/-R/-D direction model,
no-forced-binding API surface, backend inversion point, two-pump
helper extraction, ALPN strategy, access control scope, lifecycle/
error reporting, and candidate targeted POCs. Includes the settled
foundation (POC-validated prior art), a prior-art survey list, and
the Phase 0 convergence checklist.