docs: resolve OQ-TN-03 via hub-owns-the-connection model

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.
This commit is contained in:
2026-09-05 20:24:13 +00:00
parent 7e387ac05f
commit 4df3c618d4
+123 -17
View File
@@ -65,6 +65,71 @@ from zero. It inherits:
about what the tunnel protocol frames *inside* the channel, not about what the tunnel protocol frames *inside* the channel, not
about the transport. about the transport.
## Prior art: the hub-owns-the-connection model (`OQ-TN-01`, `OQ-TN-03`, `OQ-TN-04`, `OQ-TN-08`)
Discussed 2026-09-05. The standing stance across the alk* crates: **the
hub owns the connection and explicitly proxies to expose resources for
others.** A produced resource is not a socket — it is an ACL-scoped
*virtual resource* on the side that can reach the real target. Applied
to tunnels, this dissolves the SSH direction model:
- **Role follows the resource.** Whoever can reach the target is the
producer (registers openable tunnel channels via
`ChannelCore::register_openable`, dials/serves the target); whoever
wants the bytes is the consumer (opens channels via `ChannelClient`).
This holds for both SSH `-L` and `-R`: in both, the entry-point side
opens the channel (consumer) and the target side handles the open
(producer) — the only difference is which machine hosts the entry
point, which is assembly-layer wiring, not protocol.
- **The "exposed port" is a virtual port.** The far side may *think* it
exposed a port on the remote end; what actually exists is an
ACL-scoped resource in the connection/identity registry — the same
shape as operation registration, not a bind. Example: a self-hosted
gitea (HTTP in a docker container, really binding a port) is tunneled
to the hub; the hub proxies the tunnel to consumers whose ACL grants
access to the gitea HTTP service. A SOCKS5 service is the same shape,
further downstream.
- **The hub proxy is a producer wrapping a consumer.** The proxy is an
assembly-layer construct. On leg 1 (gitea side ↔ hub): gitea side is
producer, hub is consumer. On leg 2 (hub ↔ service consumer): the hub
re-produces the resource ACL-scoped — producer wrapping its leg-1
consumer role. Data path per hop is the same two-pump shape; the
consumer opens a channel naming the resource, the producer's open
handler dials the target. This means the hub *terminates and
re-produces* rather than relaying byte-for-byte — unlike alkcall
ADR-042's transparent data-channel relay — because ACL must apply
per hop. (ADR-042 relays remain valid for transports that don't
need per-hop ACL; the hub's tunnel proxy is the explicit-ACL path.)
- **`-D` (dynamic/SOCKS) composes.** A SOCKS5 server at some assembly
layer is just a consumer that opens channels with per-connection
dynamic targets, gated by the producer's target policy. Not a base
crate concern (same conclusion as the original half-answer, now with
the mechanism named).
- **No forced binding, everywhere (OQ-TN-04 resolved).** Binding is
always assembly-layer and optional, on either side. The protocol
never binds: a producer "produces a resource" (which may or may not
correspond to a local bind — a docker container's port, a unix
socket, an in-process service); a consumer "consumes it." The SSH
mental image of "expose a port" is an illusion the assembly layer
may create locally; the protocol only ever carries
produce/consume bookkeeping.
**Model, one sentence:** a producer produces a resource (a stream of
basically anything, if TCP and UDP are supported); a consumer consumes
it; direction, bind, and ACL scoping are assembly-layer concerns
wrapped around that pair.
**Residue (folded into other OQs):**
- How does a consumer *discover* produced resources — call-level
listing op, out-of-band config, or resource names? → OQ-TN-01
(params/resource naming).
- Dynamic-target policy for `-D`-style opens (which identities may open
channels to which targets) → OQ-TN-08.
- Whether the hub proxy needs anything from this crate (a
re-produce helper? typed open-handler composition?) or composes from
the public producer/consumer surface as-is → spec question for
Phase 1, tracked here as part of OQ-TN-05.
## Prior art: tun2proxy UDP gateway (`OQ-TN-01`, `OQ-TN-02`, `OQ-TN-07`, `OQ-TN-10`) ## Prior art: tun2proxy UDP gateway (`OQ-TN-01`, `OQ-TN-02`, `OQ-TN-07`, `OQ-TN-10`)
`/workspace/tun2proxy/src/udpgw.rs` implements a UDP gateway over a TCP `/workspace/tun2proxy/src/udpgw.rs` implements a UDP gateway over a TCP
@@ -157,7 +222,13 @@ extensible; exact shape TBD. Strengthened 2026-09-05: the tun2proxy UDP
gateway (§Prior art) validates SOCKS5 ATYP addressing (v4/v6/domain) as gateway (§Prior art) validates SOCKS5 ATYP addressing (v4/v6/domain) as
in-band prior art, and surfaces a fork — for UDP, remote addressing is in-band prior art, and surfaces a fork — for UDP, remote addressing is
per-datagram (SOCKS5-style) rather than fixed-at-open like TCP. The per-datagram (SOCKS5-style) rather than fixed-at-open like TCP. The
params design must account for both modes. params design must account for both modes. Reframed 2026-09-05 by the
hub model (§Prior art: the hub-owns-the-connection model): `params`
names a *produced resource* (an ACL-scoped virtual resource), not a
raw socket spec — so the design must also decide resource naming
(stable names vs target addresses) and discovery (call-level listing
op vs out-of-band config). SOCKS5 ATYP remains the in-band transport
encoding candidate underneath the resource naming.
### OQ-TN-02: Datagram substrates (UDP) — boundary preservation ### OQ-TN-02: Datagram substrates (UDP) — boundary preservation
@@ -189,11 +260,30 @@ flow lifecycle (idle timeout + keepalive) is packet-level. Remaining:
whether alktunnels fixes the UDP endpoint at open (per-channel, TCP- whether alktunnels fixes the UDP endpoint at open (per-channel, TCP-
like) or carries per-datagram addresses (udpgw-like), and whether a like) or carries per-datagram addresses (udpgw-like), and whether a
u16 conn-id vocabulary is right for channels (vs the channel ID u16 conn-id vocabulary is right for channels (vs the channel ID
itself doing the demux and one channel per UDP flow). A targeted POC itself doing the demux and one channel per UDP flow). Note (2026-09-05,
(OQ-TN-10 #1) is likely still +EV for the chosen shape. hub model §Prior art): if UDP resources are produced like any other
resource, endpoint-at-open aligns naturally with resource naming
(OQ-TN-01); per-datagram addressing matches the `-D`/dynamic-target
composition path instead. A targeted POC (OQ-TN-10 #1) is likely still
+EV for the chosen shape.
### OQ-TN-03: Direction semantics (`-L` / `-R` / dynamic) ### OQ-TN-03: Direction semantics (`-L` / `-R` / dynamic)
**Status: resolved 2026-09-05** by the hub-owns-the-connection model
(§Prior art: the hub-owns-the-connection model). There is no
protocol-level direction: role follows the resource. Whoever can reach
the target is the producer (registers openable channels); whoever wants
the bytes is the consumer (opens channels). SSH `-L` and `-R` are the
same producer/consumer pair with the entry point on different machines —
assembly-layer wiring, not protocol. The "exposed port" is a virtual,
ACL-scoped resource on the producing side; the hub proxy is a producer
wrapping a consumer. `-D`/SOCKS composes as a consumer opening channels
with per-connection dynamic targets, gated by the producer's target
policy (OQ-TN-08) — not a base-crate concern. Original question retained
below for context.
Considerations (original):
SSH has three forwarding flavors; the crate must model them without SSH has three forwarding flavors; the crate must model them without
"server/client" framing: "server/client" framing:
@@ -242,11 +332,12 @@ channel" without assuming either endpoint is a bound socket. Substrate
modules (behind feature flags) own actual `bind()` calls; the protocol modules (behind feature flags) own actual `bind()` calls; the protocol
owns bookkeeping only. owns bookkeeping only.
**Status:** open — mostly a spec-level requirement to encode in the **Status:** open — mostly resolved (2026-09-05, see §Prior art: the
architecture docs and API shapes rather than a research question. Half- hub-owns-the-connection model): binding is always assembly-layer and
answer: already agreed as a requirement (AGENTS.md convention 9); what's optional, on either side; the protocol never binds. What remains is the
missing is the concrete API surface (who calls what to start a tunnel in concrete API surface who calls what to start a tunnel in each mode
each mode). (produce-with-dial, produce-without-dial/accept-style, consume). This is
now a spec-shape task, not a research question.
### OQ-TN-05: Backend inversion point — is there a `TunnelBackend` trait? ### OQ-TN-05: Backend inversion point — is there a `TunnelBackend` trait?
@@ -265,6 +356,12 @@ so the question:
listener variant. listener variant.
- Backpressure/limits come from channels (AGENTS.md convention 10); the - Backpressure/limits come from channels (AGENTS.md convention 10); the
backend trait must not add a second layer of them. backend trait must not add a second layer of them.
- New sub-question from the hub model (2026-09-05, §Prior art): the
hub's tunnel proxy *re-produces* a resource it consumes (producer
wrapping a consumer). Does the proxy need a composition helper from
this crate, or does it assemble from the public producer/consumer
surface as-is? If a helper is warranted, it may share shape with the
substrate dial/listen trait — which would argue for the trait.
**Status:** open — needs a survey of what backends would actually **Status:** open — needs a survey of what backends would actually
implement (local TCP? docker exec? ssh -w?) before deciding trait vs implement (local TCP? docker exec? ssh -w?) before deciding trait vs
@@ -339,7 +436,14 @@ path gets `AccessControl` wiring for free via
alktty scoped its gate. Half-answer (hunch): scope-gate for the open alktty scoped its gate. Half-answer (hunch): scope-gate for the open
plus an open-handler-level target policy hook; ownership for plus an open-handler-level target policy hook; ownership for
*registered/listened* tunnels (which are persistent resources), not for *registered/listened* tunnels (which are persistent resources), not for
ephemeral dials. ephemeral dials. Strengthened 2026-09-05 by the hub model (§Prior art):
the "virtual port" IS an ACL-scoped registered resource — so
`register_openable` per-resource registration is the primary gate
shape, and ownership naturally attaches to produced resources
(persistent), not to ephemeral dials. Remaining: the dynamic-target
policy hook for `-D`-style opens (which identities may open channels to
which targets when targets arrive per-channel), and whether that policy
lives in `AccessControl` or in the open handler's params validation.
### OQ-TN-09: Lifecycle, teardown, and error reporting ### OQ-TN-09: Lifecycle, teardown, and error reporting
@@ -423,14 +527,16 @@ Candidate reading for the research specialist (to be expanded):
SSH/SOCKS5 addressing survey still open SSH/SOCKS5 addressing survey still open
- [ ] Recommendation: addressing format sketch (OQ-TN-01) — SOCKS5 - [ ] Recommendation: addressing format sketch (OQ-TN-01) — SOCKS5
ATYP validated as in-band encoding prior art; per-channel vs ATYP validated as in-band encoding prior art; per-channel vs
per-datagram fork unresolved per-datagram fork unresolved; resource naming + discovery now
- [ ] Recommendation: datagram strategy (OQ-TN-02) + ALPN strategy in scope (hub model)
dependent on it (OQ-TN-07) — framing mechanics de-risked by - [x] Direction model resolved (OQ-TN-03) — hub-owns-the-connection
tun2proxy prior art; endpoint-at-open vs per-datagram addressing model: role follows the resource, no protocol-level direction;
fork remains `-D` composes at the assembly layer
- [ ] Recommendation: direction model (-L/-R/-D) (OQ-TN-03) + API - [x] No-forced-binding requirement encoded (OQ-TN-04) — binding is
surface sketch satisfying no-forced-binding (OQ-TN-04) always assembly-layer and optional; remaining work is the
- [ ] Decision input: backend trait vs no-trait (OQ-TN-05) concrete produce/consume API surface sketch (spec task)
- [ ] Decision input: backend trait vs no-trait (OQ-TN-05), now
including the hub re-produce composition question
- [ ] Targeted POC(s) run + summary (OQ-TN-10) — UDP first, reverse - [ ] Targeted POC(s) run + summary (OQ-TN-10) — UDP first, reverse
flow second flow second
- [ ] Open questions promoted to Phase 1 - [ ] Open questions promoted to Phase 1