Complete the ADR-011 §Amendment (extraction, not wholesale-copy-plus- gate) across every doc that stated the pre-amendment posture: - ADR-011: add the two extraction-recipe exclusions the amendment missed (the socks4 module — upstream's separate default-off cargo feature, out of RFC 1928 scope; the legacy Config/Authentication auth layer — ungated but consumed only by the cut region) and the dependency-surface adjustments (log -> tracing, anyhow dropped, async-trait dropped with its legacy-layer uses) - ADR-009: amend — the vendor's net gate dissolves; features collapse to local + composed; local carries tokio/net + socket2 itself - vendor.md: replace the contradictory old "four carried changes" section with the two-change reading + extraction-time adjustments; extend the cut table (socks4 row, legacy auth layer) - overview.md: dependency diagram + feature table drop the net feature; module map reflects two carried changes - README.md: principles 7/8 + doc/ADR tables updated - ADR-002/012, open-questions.md (OQ-SK-04), phase-0.md current-state sections, AGENTS.md conventions 4/17 + stale wasm-caveat sentence, POC findings posture notes: aligned with the extraction Verification: repo-wide grep for stale "four carried"/default-on net/ implies-net statements returns only amendment-context matches; extraction recipe re-verified against POC #4's gate map and the fast-socks5 v1.0.0 source (async-trait usage confined to the cut legacy layer; AuthMethod<T> traits are native async-fn-in-trait).
1156 lines
66 KiB
Markdown
1156 lines
66 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-09-20 (consolidation rewrite: current-state form, one
|
|
canonical statement per decision, reasoning trails trimmed to their
|
|
Phase 1 weight; no decisions changed)
|
|
---
|
|
|
|
# alksocks — Phase 0 (Exploration)
|
|
|
|
This document captures Phase 0 (Exploration) for the `alksocks` crate:
|
|
vision, guiding principles, prior art, open questions (OQ-SK-01..NN), and
|
|
the converged recommendation. Phase 0's objective per `docs/sdd_process.md`:
|
|
*capture vision and guiding principles; research options; validate
|
|
approaches; converge on a recommended approach.* It is the input to Phase 1
|
|
(Architecture), where the Architect will produce `docs/architecture/`
|
|
specs, ADRs, and the open-questions tracker.
|
|
|
|
Drafted 2026-09-12, emerging from the initial setup discussion. The crate
|
|
is the sibling of alktty (`alk/tty`) and alktunnels (`alk/tunnel`) on the
|
|
alkcall substrate; where alktunnels realizes the `-L`/`-R` forwarding
|
|
flavors, alksocks realizes the `-D` flavor (dynamic / SOCKS5 proxy) —
|
|
the one alktunnels explicitly deferred to composition.
|
|
|
|
Phase 0 converged 2026-09-14 and is POC-complete: every numbered POC on
|
|
the register either passed or was dropped with the scope decision
|
|
(§OQ-SK-07). The remaining open questions are Phase 1 ADR decisions,
|
|
each with its inputs collected (§What Phase 0 could NOT answer).
|
|
|
|
## Vision and guiding principles
|
|
|
|
**One sentence:** a SOCKS5 (RFC 1928) producer/consumer protocol crate on
|
|
alkcall channels — an arbitrary-egress proxy service that never binds a
|
|
port unless explicitly configured to, carrying the RFC conversation inside
|
|
a channels data channel like any other produced resource. The RFC 1928
|
|
implementation is the vendored fast-socks5 subset (the inline vendor,
|
|
§Layering) — the crate owns the protocol code in-tree.
|
|
|
|
**Scope (resolved 2026-09-14, post-POC): socks + channels, nothing
|
|
else.** The original three-component split serves different purposes and
|
|
was at risk of being conflated; two of the three are this crate:
|
|
|
|
1. **Producer half** — a side exposes the SOCKS5 service as a produced,
|
|
ACL-scoped resource (registers openable channels; the open handler
|
|
runs the RFC state machine; target dials happen there or further
|
|
downstream). This is the `-D` service over channels.
|
|
2. **Consumer half** — a side opens SOCKS5 channels against a produced
|
|
service and speaks RFC 1928 inside them. *Optionally* it may expose
|
|
the service locally: bind a real `socks5://127.0.0.1:port` endpoint
|
|
and relay each accepted connection into a channel (the ssh `-D`
|
|
local-exposure shape, and the path tun2proxy/curl/browser point at).
|
|
|
|
3. ~~**SOCKS client wrapper for QUIC runtimes** — `AsyncUdpSocket`
|
|
implementations (quinn and noq) so downstream clients can route QUIC
|
|
through any third-party SOCKS5 server (the iroh privacy posture: a
|
|
client that doesn't want to leak its real IP to relays needs a SOCKS
|
|
hop ahead of the QUIC dial).~~ **Moved to the alknet rewrite** — it is
|
|
alknet's own ancestor (ADR-090 + the quinn-proxy POC live there), it
|
|
neither produces nor consumes channels, and alknet is where downstream
|
|
clients will actually consume it. The transfer took OQ-SK-06 and POC
|
|
#3 with it. This crate's client-side story is the channels-native
|
|
consumer session (POC #1) plus the wrapper-aware associate session
|
|
(POC #2) and the optional front doors — both POC-validated.
|
|
|
|
Components 1+2 form the producer/consumer protocol pair (channels in,
|
|
RFC 1928 inside). Keep them structurally separate in the crate layout.
|
|
|
|
Guiding principles, inherited from the alk* family:
|
|
|
|
1. **"ALPN as a service," not "a server."** The SOCKS5 service is a
|
|
produced, ACL-scoped resource on a channels connection — the same
|
|
shape as an alktty terminal or an alktunnels TCP tunnel. Kernel
|
|
binds exist only as explicit, optional assembly-layer shapes on
|
|
*either side* — the producer may serve from a real listener (a
|
|
feature-gated local backend, component 1's optional shape), and the
|
|
consumer may expose the resource on a local port (component 2's
|
|
optional shape, the ssh `-D` front door). The protocol itself never
|
|
binds; the binding decision belongs to the caller.
|
|
2. **The `-D` conclusion, realized.** alktunnels' Phase 0 settled that
|
|
`-D` composes at the assembly layer: "`-D` is just tunnel a socks5
|
|
connection"; target selection lives in the SOCKS5 protocol at the
|
|
producing side, governed by the same op-level ACL as the resource.
|
|
This crate is that conclusion's realization as a first-class protocol
|
|
crate, not an assembly-layer afterthought.
|
|
3. **The protocol layer is substrate-agnostic and generic over the
|
|
stream.** The vendored fast-socks5 typestate API
|
|
(`Socks5ServerProtocol<T, states::*>`) is generic over
|
|
`T: AsyncRead + AsyncWrite + Unpin`, and its interception points
|
|
(`run_tcp_proxy`, `run_udp_proxy_custom`, `transfer`) accept any such
|
|
`T`. The channels adapter feeds the state machine a `BiStream`; a
|
|
local backend feeds it a `TcpStream`. The wrapper must not leak
|
|
either substrate into the protocol layer (alktty `TtyBackend` /
|
|
alktunnels pump-halves inversion-point precedent).
|
|
4. **Producer/consumer vocabulary.** Both sides of a channels connection
|
|
can initiate; a producer registers openable SOCKS5 channels
|
|
(`ChannelCore::register_openable`), a consumer opens them and speaks
|
|
RFC 1928 inside. RFC 1928's own client/server roles keep their RFC
|
|
names *inside* the protocol layer, but crate-level docs and API use
|
|
producer/consumer. Avoid "SOCKS5 server" as a crate-level noun.
|
|
5. **Extract-and-improve from alknet (producer half only).** The server
|
|
side is the `-D` capability alktunnels deferred; the client side
|
|
(alknet ADR-090's `Socks5UdpSocket`, the quinn-proxy POC) stays in
|
|
alknet per the scope resolution above. The crates share conclusions,
|
|
not code paths.
|
|
6. **The vpn-like endgame composition.** A SOCKS5 server + tun2proxy is
|
|
the last step (ish) of "vpn-like without actually being a vpn": a
|
|
user tunnels a produced SOCKS5 resource to a local port (component
|
|
2's optional shape), then points tun2proxy (`/workspace/tun2proxy`
|
|
— it takes `--proxy socks5://user:pass@host:port` natively,
|
|
`src/args.rs`) at it, and the whole host's traffic rides the alk*
|
|
egress. This crate is the SOCKS5 leg of that composition; alktunnels
|
|
carries the tunnel; tun2proxy closes the loop. (The udpgw framing
|
|
alktunnels' UDP format is based on is the same prior art
|
|
OQ-SK-03's datagram codec draws on.)
|
|
|
|
## What is already settled
|
|
|
|
The foundation is POC-validated and ADR-pinned upstream; this crate does
|
|
not start from zero. It inherits:
|
|
|
|
- **The channels data path** — demux→Connection→handler→mux, validated
|
|
by the alknet-channels POC and production alktty/alktunnels. The SOCKS5
|
|
payload is raw bytes inside a channels data channel's `BiStream`;
|
|
channels strips its 8-byte header transparently (alknet ADR-093 /
|
|
alkcall ADR-035). SOCKS5 CONNECT is one `BiStream` per session — the
|
|
RFC conversation *is* the stream; no sub-demux, no framing, no flow
|
|
key (one channel per SOCKS5 session).
|
|
- **The two-pump data plane** — the CONNECT proxy loop is the canonical
|
|
two-pump shape; `alkcall::channels::pump_bidi` (ADR-050) is pinned
|
|
upstream and alktunnels-validated. Use it; do not hand-roll.
|
|
- **The establishment story** — session opens use
|
|
`register_openable_with_establisher` (alkcall ADR-049 + amendments):
|
|
the establisher runs as an awaited, bounded establishment phase; a
|
|
refused session is a typed `channel:open_failed` call error
|
|
(`reason ∈ dial_failed / unknown_resource / resource_shortage /
|
|
handler_error / timeout`), never a phantom channel. The negotiate/
|
|
auth half of the RFC conversation still runs in-stream over the
|
|
`BiStream` — establishment covers only the producer's accept policy,
|
|
not the RFC handshake. The establisher can project reply fields
|
|
(alkcall 0.8.0, `Establishment::with_reply_field(s)`, ADR-049
|
|
amendment 3; consumable via `ChannelClient::open_channel_with_reply`)
|
|
— the carrier alktunnels' bind-first `bound` address rides; a SOCKS5
|
|
establisher that ever needs to return open-time metadata has the same
|
|
seam (additive; absent fields leave the reply byte-identical).
|
|
- **The producer/consumer model** — producer registers openable channels
|
|
(authorization for free via `AccessControl`); consumer opens them via
|
|
`ChannelClient` (alkcall ADR-037, ADR-043). Connection direction is
|
|
independent of service direction.
|
|
- **The relay/hub story** — a SOCKS5 resource traverses alkcall hub
|
|
relays like any produced resource; the hub's terminate-and-re-produce
|
|
proxy applies per hop, and a SOCKS5 resource is exactly the "further
|
|
downstream" shape alktunnels described. No socks5-specific relay
|
|
work. The relay machinery is in-tree — alkcall 0.8.0 ships
|
|
`ChannelRelay` + `HubLegTemplate` (ADR-051), so a hub leg is an
|
|
install-template composition, not assembly-layer hand-rolling;
|
|
relayed replies carry extra fields through per-hop-truthfully (the
|
|
bind-first `bound` flows end-to-end), and flavor-form ops (the direct
|
|
op's `channels/tunnel/direct` shape) re-produce as channels, not
|
|
forwarding stubs.
|
|
- **The identity seam** — alkcall's CF-005/CF-006 (0.7.0): the per-call
|
|
opener identity arrives on the open-op hooks. Auth mapping (identity →
|
|
SOCKS5 credentials/ACL, if any) happens there, not via in-band SOCKS
|
|
auth, unless RFC-facing username/password is needed for vanilla SOCKS5
|
|
clients (OQ-SK-02 — settled posture: split by the door). Still current
|
|
at alkcall 0.8.0 — the 0.8.0 additions (reply projection, relay) are
|
|
additive and do not touch it.
|
|
- **The backend inversion point pattern** — substrate-specific types
|
|
confined to feature-gated backend modules, injected at the assembly
|
|
layer (alktty `TtyBackend` / alktunnels no-trait precedent).
|
|
- **The wasm-clean default crate** — protocol-only code should compile
|
|
to `wasm32-unknown-unknown` (alktty/alktunnels precedent). The
|
|
extracted fast-socks5 subset is wasm-clean by construction: the
|
|
inline vendor is an extraction (the `T`-generic subset only; the
|
|
kernel-net handlers are not copied — OQ-SK-04 resolved; ADR-011
|
|
§Amendment, 2026-09-21; POC #4 verified the subset is functionally
|
|
complete).
|
|
- **The upstream substrate this crate composes against** — alkcall
|
|
0.8.0 (reply projection; `ChannelRelay`/`HubLegTemplate` per ADR-051;
|
|
flavor-form open-op ids in discovery per ADR-047 amendment 3) and
|
|
alktunnels 0.2.0 (the direct op `channels/tunnel/direct`, ADR-007;
|
|
the bind-first establisher + forwarded op `channels/tunnel/forwarded`,
|
|
ADR-008; `TunnelListener`). The graduation
|
|
(`alktunnels docs/research/tunnels-graduation.md`) took this crate's
|
|
asks as evidence and landed them in one version bump — see the
|
|
per-OQ notes (OQ-SK-01, OQ-SK-03, OQ-SK-08) for what changed in shape.
|
|
|
|
## Prior art
|
|
|
|
### alktunnels Phase 0 — the closest sibling
|
|
|
|
`/workspace/@alkdev/alktunnels/docs/research/phase-0-findings.md`. The
|
|
load-bearing conclusions this crate inherits:
|
|
|
|
- **Hub-owns-the-connection model** — role follows the resource;
|
|
whoever can reach the target is the producer, whoever wants the bytes
|
|
is the consumer. The "exposed port" is a virtual, ACL-scoped resource,
|
|
not a bind. For alksocks: the producer is the side that dials targets
|
|
(the egress side); the consumer is the side that wants proxied
|
|
egress. "A SOCKS5 service is the same shape [as a produced tunnel
|
|
resource], further downstream" is alktunnels' own wording — this
|
|
crate exists to make it literal.
|
|
- **`-D` composes at the assembly layer** — the original half-answer,
|
|
now with the mechanism named: a SOCKS5 server at some assembly layer
|
|
is just a consumer opening channels with per-connection dynamic
|
|
targets. The refinement this crate adds: the SOCKS5 *service* is
|
|
itself a produced resource (producer half), so a vanilla SOCKS5 client
|
|
(curl, a browser, ssh -D's own client half) can attach through the
|
|
optional local backend, and channels-native consumers get the same
|
|
service in-band. Both halves wrap the same protocol layer.
|
|
- **Target policy = resource policy.** alktunnels OQ-TN-08 dissolved
|
|
dynamic-target policy: whatever ACL governs the socks5 resource
|
|
governs everything reachable through it, plus whatever policy the
|
|
SOCKS5 implementation itself applies downstream. No target allowlists
|
|
in the base crate unless Phase 1 wants them (OQ-SK-05).
|
|
- **The codec conclusion does NOT transfer.** alktunnels needed
|
|
length-framing only for datagram substrates; a SOCKS5 CONNECT session
|
|
is a byte stream end to end (RFC 1928 defines its own framing on the
|
|
wire) — the channel payload is pure pass-through, 0 B overhead over
|
|
the channels 8-byte header. UDP ASSOCIATE's datagram stage is the
|
|
exception (OQ-SK-03, resolved shape below).
|
|
|
|
### fast-socks5 — the vendored implementation
|
|
|
|
`/workspace/fast-socks5` (v1.0.0, MIT — third-party upstream, not ours;
|
|
read-only reference checkout). **The posture is the inline vendor as an
|
|
extraction (OQ-SK-04, resolved 2026-09-20; extraction pinned
|
|
2026-09-21, ADR-011 §Amendment): extract the `T`-generic subset into
|
|
this crate — the kernel-net handlers and the `socks4` module are not
|
|
copied — carrying the wrapper's needs in-tree.**
|
|
`/workspace/fast-socks5` remains the differential-testing reference
|
|
for RFC edge cases (reply-code mapping, domain addressing,
|
|
fragmentation); note the vendor point (v1.0.0) in the vendored code.
|
|
Key surface points, verified:
|
|
|
|
- **Explicit typestate server API** (`src/server.rs`):
|
|
`Socks5ServerProtocol<T, states::Opened|Authenticated|CommandRead>`,
|
|
generic over `T: AsyncRead + AsyncWrite + Unpin`. Flow:
|
|
`start(inner)` → `negotiate_auth(&methods)` →
|
|
`finish_auth()` / `accept_no_auth` / `accept_password_auth` →
|
|
`read_command()` → (`reply_success`, `reply_error`). The legacy
|
|
`Socks5Server`/`Socks5Socket`/`Incoming` API (binds a `TcpListener`)
|
|
is deprecated — the wrapper uses the explicit API only.
|
|
- **Auth surface** — the `AuthMethod<T>` trait (metadata: `method_id`,
|
|
`new`) + `AuthMethodSuccessState<T>` (carry the socket back out);
|
|
`StandardAuthentication` enum for NoAuth + Password with static
|
|
dispatch; custom methods implement the trait. Username/password check
|
|
is a closure (`accept_password_auth`). The auth *decision* is where
|
|
the alkcall identity seam plugs in (OQ-SK-02).
|
|
- **Command handling is swappable** — the interception points:
|
|
- `run_tcp_proxy(proto, addr, timeout, nodelay)` — dials, replies,
|
|
then `transfer(inbound, outbound)` (a `copy_bidirectional` wrapper,
|
|
itself generic). This is where a channels-native dial replaces the
|
|
`tokio::net` dial: the wrapper's producer half intercepts here,
|
|
dials via alktunnels/alkcall primitives (or its own dial policy),
|
|
and returns the `T` back.
|
|
- `run_udp_proxy_custom(proto, addr, peer_bind_ip, reply_ip, transfer)`
|
|
— the customizable UDP ASSOCIATE handler. **Caveat (verified
|
|
2026-09-13):** the "custom" seam is narrower than it looks — it calls
|
|
`udp_bind_random_port(peer_bind_ip)` *unconditionally* and derives
|
|
the reply port from that kernel socket; the wrapper can customize
|
|
the relay loop (`transfer`) and the reply *IP* only. The channels
|
|
path therefore cannot use this handler: it drives the typestate
|
|
directly (`reply_success(sentinel_addr)` + own datagram stage).
|
|
Under the inline vendor this is a change we make in-tree, not an
|
|
upstream ask: make the bind optional or accept a caller-supplied
|
|
socket.
|
|
- `transfer(inbound, outbound)` — plain two-pump copy; a channels
|
|
`BiStream` is a legal `T` on either side.
|
|
- **UDP support** — `new_udp_header(target)` / `parse_udp_request(buf)`
|
|
are public and pure (no socket I/O) — the SOCKS5 UDP datagram header
|
|
codec is reusable without the `Socket2`-based relay machinery. The
|
|
default `run_udp_proxy` binds two kernel sockets (`Socket2`, random
|
|
ports) — the *binds are in the default handler, not in the protocol*,
|
|
which is exactly the seam the no-bind requirement needs.
|
|
- **Client side** — `Socks5Stream<S>` (generic over the backing socket;
|
|
`use_stream` upgrades any `AsyncRead + AsyncWrite + Unpin` already
|
|
connected) and `Socks5Datagram<S>` (UDP associate; also accepts a
|
|
caller-supplied socket via `use_socket`). `Socks5Stream` impls
|
|
`AsyncRead + AsyncWrite` itself, so a consumer session over a
|
|
channels `BiStream` is the intended use, not a hack. Note:
|
|
`Socks5Stream::connect` convenience constructors dial
|
|
`tokio::net::TcpStream` and are non-wasm; `use_stream` is the
|
|
substrate-free path.
|
|
- **Error surface** — `ReplyError` (the RFC reply codes, `as_u8`/
|
|
`from_u8`) and `SocksError`; `SocksServerError` on the explicit API.
|
|
Map these faithfully; never collapse a refusal into a generic error.
|
|
- **`router.rs` example** — the "conditional interception" template: a
|
|
server that inspects the command/target, then decides whether to
|
|
proxy, refuse, or handle in-process. Structurally the shape of the
|
|
channels open handler (inspect params → dial or refuse) and of the
|
|
alknet ADR-090 client (hand the established stream to quinn).
|
|
- **What the vendor must add (the carried needs; re-read under the
|
|
2026-09-21 extraction amendment — ADR-011 §Amendment):** **BIND**
|
|
(absent upstream — fast-socks5 parses `Socks5Command::TCPBind` but
|
|
every handler refuses it, `lib.rs:384-386` / `server.rs:813-816`;
|
|
the wrapper drives the typestate itself) and the publication of
|
|
`new_reply` (the reply encoder is private; the BIND reply#2 writer
|
|
otherwise hand-rolls the 10-byte reply — `poc-bind-findings.md`).
|
|
The other two of the original four needs — the kernel-net feature
|
|
gate (POC #4: ~120 cfg lines, wasm-clean subset functionally
|
|
complete) and the `run_udp_proxy_custom` bind seam — dissolve with
|
|
the kernel-net region, which the extraction does not copy.
|
|
|
|
### iroh-socks5 — the structural sibling (evaluated, not the base)
|
|
|
|
`crates.io/crates/iroh-socks5` (v0.5.0, MIT OR Apache-2.0, repo:
|
|
`github.com/mattgeddes/iroh-socks5` — crates.io lacks the repository
|
|
link). A ~1.3k-LoC iroh `ProtocolHandler` tunneling SOCKS5 over iroh
|
|
QUIC: framed `Request`/`Reply` protocol postcard-encoded over one
|
|
bidirectional stream, then raw pass-through (CONNECT/BIND) or framed
|
|
`Datagram`s (UDP ASSOCIATE); producer-side dial + allow-list
|
|
(`EndpointId`s), consumer-side local TCP front door for vanilla clients.
|
|
Full evaluation: `docs/research/iroh-socks5-eval.md`. Verified:
|
|
|
|
- **Independent confirmation of POC #1's shape** — one stream per SOCKS5
|
|
session, framed control phase then pass-through, RFC codec only at
|
|
vanilla-client boundaries, resolution producer-side. Convergent
|
|
evolution from a codebase with no alk* lineage.
|
|
- **The load-bearing OQ-SK-03 prior art** — its UDP ASSOCIATE design
|
|
carries framed `{addr, data}` datagrams in-stream (no RFC UDP header
|
|
on the tunnel wire; the RFC header codec applies only at the local
|
|
front-door socket), needs no producer-side flow table (per-datagram
|
|
addressing in the frame does the demux), ends the association on
|
|
control-stream EOF, and refuses FRAG ≠ 0. Adopted as the POC #2
|
|
design baseline; POC #2 validated it.
|
|
- **Why it is not the base:** iroh types are load-bearing in the data
|
|
plane (`SendStream`/`RecvStream` in relay signatures; no generic `T`
|
|
seam), the RFC surface is a subset (no-auth only, no auth-method
|
|
machinery, no reply-code enum fidelity), and the client side has no
|
|
in-band session type (local front door only). Extracting a
|
|
substrate-agnostic core yields ~550 LoC of codecs fast-socks5 already
|
|
provides in stricter form — the POC #1/#4 base decision stands.
|
|
|
|
### alknet's SOCKS5 client + the quinn-proxy POC — the client-side ancestor
|
|
|
|
`/workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs` (ADR-090)
|
|
implements `Socks5UdpSocket: quinn::AsyncUdpSocket` — a SOCKS5 UDP
|
|
ASSOCIATE tunnel wrapped as the socket QUIC polls. The quinn-proxy POC
|
|
(`/workspace/@alkdev/alknet/docs/research/quinn-quic-proxy/findings.md`)
|
|
validated the approach end-to-end (5/5 clean runs) against quinn 0.11:
|
|
one public trait (`AsyncUdpSocket`), one public constructor
|
|
(`Endpoint::new_with_abstract_socket`), no fork. Known limitations
|
|
there, likely inherited: ECN is lost through the SOCKS5 header; the
|
|
proxy must support UDP ASSOCIATE; `may_fragment() == true` disables
|
|
path MTU discovery.
|
|
|
|
**This work belongs to the alknet rewrite** (the 2026-09-14 scope
|
|
decision, §Vision): the client wrapper is alknet's ancestor-shaped
|
|
component — it neither produces nor consumes channels, and its open
|
|
research (the noq trait-shape split, OQ-SK-06, §OQ-SK-06) moves to
|
|
alknet's Phase 0. The alknet-side cleanup that applies there: the
|
|
hand-rolled codec in `socks5.rs` inlines the greeting/auth/CONNECT/
|
|
ASSOCIATE byte logic twice — fast-socks5's client types and
|
|
`new_udp_header`/`parse_udp_request` supersede it.
|
|
|
|
### noq — the quinn fork the client story must also fit
|
|
|
|
> **Transferred to the alknet rewrite (2026-09-14, the scope decision,
|
|
> §Vision).** Retained as background research for *alknet's* Phase 0;
|
|
> see OQ-SK-06 for the transfer rationale.
|
|
|
|
`/workspace/noq` (v1.2.0; iroh's fork of quinn — iroh depends on
|
|
`noq = "1.2.0"`). Surface verified:
|
|
|
|
- `noq::AsyncUdpSocket` exists (`noq/src/runtime/mod.rs:44`) but with a
|
|
**different shape** than quinn 0.11: `create_io_poller` +
|
|
`try_send` are replaced by `create_sender() -> Pin<Box<dyn UdpSender>>`
|
|
with `poll_send(transmit, cx)` — a sender-object split (any number of
|
|
`UdpSender`s per socket, each holding its own waker).
|
|
- `RecvMeta` gained fields (`interface_index`, `timestamp`) but stays
|
|
default-constructible; `Transmit` is unchanged in the fields the
|
|
SOCKS5 wrapper touches (`destination`, `contents`, `ecn`, `src_ip`,
|
|
`segment_size`).
|
|
- `Endpoint::new_with_abstract_socket` exists (`noq/src/endpoint.rs:162`)
|
|
with the same doc-comment intent, but takes
|
|
`Box<dyn AsyncUdpSocket>` rather than `Arc<dyn AsyncUdpSocket>` — the
|
|
poller-removal reshuffle also changed the ownership shape.
|
|
- The alknet `Socks5UdpSocket` does **not** drop in unchanged: the impl
|
|
must be rewritten against `create_sender`/`poll_send`, and the
|
|
`Arc`→`Box` change ripples into construction. Whether one crate can
|
|
serve both quinn and noq behind feature flags (shared core, two thin
|
|
trait-impl shells) or whether the shapes have diverged enough to
|
|
justify two impls is the research question (OQ-SK-06, transferred).
|
|
|
|
### Anti-prior-art (what NOT to carry over)
|
|
|
|
- **The alknet client's hand-rolled SOCKS5 codec** —
|
|
`socks5.rs` inlines the greeting/auth/CONNECT/ASSOCIATE byte logic
|
|
twice (handshake for UDP, again for CONNECT). fast-socks5's client
|
|
types and `new_udp_header`/`parse_udp_request` supersede it; the
|
|
vendored crate provides the codecs — no second codec.
|
|
- **`Socks5Server` (the legacy bind-based API)** — deprecated upstream;
|
|
never the default path here.
|
|
- **In-band SOCKS auth as the primary auth story** — alkcall's identity
|
|
seam (CF-005/CF-006) authorizes at the open-op layer. In-band RFC
|
|
username/password remains available only for vanilla-client
|
|
compatibility at the front door (OQ-SK-02).
|
|
|
|
## Open Questions
|
|
|
|
These are the design questions Phase 0 resolved (or explicitly
|
|
deferred) before the architecture spec. Numbered OQ-SK-01.. so they can
|
|
be referenced, tracked, and promoted into `docs/architecture/
|
|
open-questions.md` in Phase 1. Status is stated per question; what
|
|
remains open is named explicitly. The point of this document is to hold
|
|
the reasoning without forcing premature decisions.
|
|
|
|
### OQ-SK-01: Producer dial policy — what dials the target?
|
|
|
|
**Status: shape resolved empirically (POC #1, 2026-09-13); the A/B
|
|
assembly-layer choice is a Phase 1 policy decision.** The dial callback
|
|
exists and is called in-stream at the command-read interception point:
|
|
an injected `DialFn(target) -> boxed halves` closure (function-not-trait,
|
|
the alktunnels ADR-004 shape), returning `TargetHandle { read, write }`
|
|
(boxed halves, `+ Sync`, satisfies the `ChannelPlan` payload bound).
|
|
Per-target refusal is an RFC reply code, not an establishment failure;
|
|
session-level refusal stays channels-level (`channel:open_failed`). The
|
|
no-trait hunch is evidence-backed; `EgressFactory` (OQ-SK-03) and
|
|
`BindAcceptFactory` (OQ-SK-08) extend the same shape to the datagram and
|
|
accept planes.
|
|
|
|
What remains open is where the callback *comes from* (the dial policy):
|
|
|
|
- **Option A: dial via alktunnels (composition)** — the SOCKS5 handler
|
|
opens an `alk/tunnel` channel naming the target, and the two-pump data
|
|
plane is tunnel-channel ↔ SOCKS5-`BiStream`. Since alktunnels 0.2.0
|
|
(ADR-007) the dynamic-target channel exists — the direct op
|
|
(`channels/tunnel/direct`, `{substrate, target}` params, the ssh
|
|
`direct-tcpip` shape, `tunnel:direct` as a separate ACL grant) — so
|
|
Option A no longer needs a pre-registered per-target resource: one
|
|
direct-op channel per CONNECT target (what the direct op was
|
|
graduated *for*). The udp substrate rides the same op (one op, both
|
|
substrates — OQ-SK-03's composed egress uses it too).
|
|
- **Option B: dial directly** — the producer dials the target itself
|
|
(`TcpStream` behind the `local` feature, or an injected dial
|
|
callback). No alktunnels dependency; the dial policy (allowlists,
|
|
routing through another alksocks hop) is the caller's callback.
|
|
- **Option C: injected dialer trait** — a `Dialer`-shaped trait with
|
|
alktunnels and local-TCP implementations behind features. The POC
|
|
evidence stands against the trait (two real implementations exist as
|
|
*callbacks*, not trait impls; alktunnels OQ-TN-05's no-trait
|
|
resolution holds).
|
|
|
|
Considerations: substrate-agnostic-by-construction (AGENTS.md
|
|
convention 7) favors injection over hardwiring; the "hub terminates and
|
|
re-produces" story means a hub-hop SOCKS5 resource's dialer is just
|
|
"open a channel further downstream," which is Option A's shape — so
|
|
whatever is decided must not *prevent* A when composing. The A/B choice
|
|
is an assembly-layer policy question; POC #5's scope-gate finding
|
|
(`socks5:bind` ≠ `socks5:open`) plus ADR-007's own rationale
|
|
("dial `postgres-primary`" vs "dial anything" are different grants)
|
|
both point the same way: separation lives at the ACL, not in the
|
|
protocol. The dialer-trait-vs-callback half of this OQ is closed; the
|
|
Phase 1 ADR picks the default provider set and the params shape for the
|
|
composed path.
|
|
|
|
### OQ-SK-02: Auth mapping — identity seam vs in-band SOCKS auth
|
|
|
|
**Status: settled (2026-09-13): "both," split by the door.** The
|
|
open-op hooks receive the per-call opener identity (alkcall CF-005/
|
|
CF-006); the RFC conversation also carries its own optional
|
|
username/password auth (RFC 1929). The settled posture: both mechanisms
|
|
are real, each on its own side of the door — the consumer's optional
|
|
front door (component 2's ssh `-D` shape, where the vanilla world meets
|
|
the ACL world; the vpn-like endgame composition points tun2proxy's
|
|
`user:pass` at it).
|
|
|
|
- **Producer side: identity only.** The channels-native open is
|
|
authorized by the alkcall ACL/identity seam (CF-005/CF-006); a
|
|
producer-side password is moot — the ACL system is strictly better
|
|
than a shared secret, and the producer's in-band conversation runs
|
|
NoAuth/no-op (fast-socks5's `skip_auth_this_is_not_rfc_compliant`,
|
|
or a no-op NoAuth method). A password there would add nothing the
|
|
identity seam doesn't already do.
|
|
- **Consumer side (the front door): RFC 1929 where vanilla clients
|
|
need it.** The door owns credential presentation: a vanilla client
|
|
carrying `user:pass` (tun2proxy, curl) gets a real RFC 1929
|
|
conversation at the door. The door then maps the accepted credential
|
|
to *local* configuration — which channel identity/endpoint to open
|
|
the producer channel with — and that identity reaches the producer
|
|
via the CF-005/CF-006 seam. The SOCKS password never crosses the
|
|
channel as a SOCKS password; the channel identity never reaches the
|
|
vanilla client.
|
|
|
|
The "replace or second-gate" sub-question dissolves: the mechanisms
|
|
occupy different planes (in-band creds are door-local; identity is
|
|
channel-level). No fast-socks5 `AuthMethod` impl is needed — the door
|
|
checks credentials itself before handing the stream to the typestate
|
|
(the door is this crate's code; the trait was designed for embedders
|
|
who don't own the accept loop). The NoAuth door (POC #1's raw-pipe
|
|
shape, zero protocol translation) remains valid for loopback-only or
|
|
wrapper-aware use; the cred-bearing door is the same bridge plus the
|
|
greeting half (fast-socks5's server typestate for auth + `read_command`,
|
|
then the consumer session in-band) — an assembly-layer choice.
|
|
|
|
Remaining for Phase 1: the door's credential→identity mapping shape
|
|
(static config vs pluggable lookup); whether the producer-side
|
|
optional local backend offers the same door surface (same code shape —
|
|
door = vanilla↔ACL boundary — so presumably yes).
|
|
|
|
### OQ-SK-03: UDP ASSOCIATE without binding — the hard case
|
|
|
|
**Status: baseline settled (2026-09-13, the handle swap) and
|
|
POC-validated end to end (POC #2, 2026-09-14 —
|
|
`poc-udp-associate-findings.md`).** The datagram-framing ADR (the
|
|
one-way door) is Phase 1's, with the first consumer in sight; the
|
|
framing has empirical ground under it.
|
|
|
|
**The handle swap — "tell the client about a UDP port" → "tell the
|
|
client about the UDP tunnel."** The design's load-bearing reading of
|
|
RFC 1928: the ASSOCIATE reply's `BND.ADDR`/`BND.PORT` is *just the
|
|
handle for the relay*, and one relay serves the whole association — the
|
|
per-datagram destination is carried in the datagram header (ATYP/addr/
|
|
port), not in the handle. The relay port was never "a port that goes to
|
|
one place"; it is a multiplexing endpoint. So swapping the handle
|
|
preserves every RFC property:
|
|
|
|
- one relay per association → **one channel per association** (the
|
|
CONNECT shape; no second channel, no correlation params);
|
|
- per-datagram destinations → `{addr, data}` frames in-band (the
|
|
iroh-socks5 cut: boundary-preserving, no RFC header on the tunnel
|
|
wire, no producer-side flow table; RFC codec at front doors only).
|
|
The length prefix is u16 BE — 65507 (the max UDP payload) < 65535,
|
|
so u16 suffices; boundary preservation is mandatory (empty datagram
|
|
≠ EOF — alktunnels F-2's lesson transfers directly; the frame shape
|
|
meets it structurally);
|
|
- lifetime tied to the control connection → channel EOF;
|
|
- "bind locally if you want" → the optional front door on either side
|
|
(a real UDP socket bridged to the channel, RFC codec at that
|
|
boundary only).
|
|
|
|
Vanilla clients still literally `sendto()` the replied address, so the
|
|
in-band path is wrapper-aware-only by RFC necessity — the front-door
|
|
bridge is where a real socket address exists. The reply's `BND.ADDR`/
|
|
`BND.PORT` on the in-band path are pure RFC-compatibility surface (the
|
|
POC used the sentinel `127.0.0.1:0`); only the front door reports a
|
|
real address.
|
|
|
|
**Producer egress — two variants (the "no ports" story has two
|
|
halves).** The handle swap settles the *consumer-facing* half. The
|
|
*producing-side* half is: after `{addr, data}` frames exit the channel,
|
|
what gets each datagram to `addr`? Both are dial-callback policy
|
|
(OQ-SK-01's shape); the protocol layer sees neither.
|
|
|
|
1. **Local egress** — the handler binds a real `UdpSocket` and
|
|
`send_to`s per datagram (iroh-socks5's `relay_udp_server` behind
|
|
this crate's `local` feature). No flow table, native-only. POC #2
|
|
implemented it (`LocalEgress`); domain-form destinations resolve at
|
|
the egress (producer-side DNS — the ssh `-D` semantic,
|
|
POC-resolved as "the egress owns resolution").
|
|
2. **Composed egress** — the dial callback opens an alktunnels
|
|
udp-substrate channel per destination: a lazy destination→tunnel
|
|
table (open on first datagram, LRU-evict; POC #2 implemented it as
|
|
`ComposedEgress`). The flow table is *reborn as channel handles* —
|
|
because alktunnels' udp substrate is **connected** (one tunnel, one
|
|
fixed target) while ASSOCIATE names a different destination per
|
|
datagram. The composed form's vehicle since alktunnels 0.2.0 (ADR-007)
|
|
is the **direct op** — `open_direct` + `Substrate::Udp` dials a real
|
|
UDP target through the dynamic-target op (`channels/tunnel/direct`),
|
|
pinned by alktunnels' own test (a real UDP echo through the
|
|
establisher path); the table entries are direct-op channels, and the
|
|
per-destination ACL question shifts to the `tunnel:direct` grant +
|
|
the dialer's target policy (the ACL governs the capability, the dial
|
|
callback governs per-destination checks). Cost: one open per
|
|
destination, measured ~0.8 ms (POC #2, N=20 loopback) — fine for DNS
|
|
+ browsing; the 256-channel default cap bounds fan-out (OQ-SK-09).
|
|
Benefit: **per-target ACL for free** — dissolving most of OQ-SK-05
|
|
for the composed path — and hub relaying is per-target
|
|
terminate-and-re-produce.
|
|
|
|
**No upstream ask exists here:** alktunnels' connected-udp substrate is
|
|
correct as-is for per-target composition; an "unconnected
|
|
arbitrary-egress udp resource" upstream would just be SOCKS5 again with
|
|
different framing (circular).
|
|
|
|
**Phase model vs alktty-style demux (the extensibility trade — Phase 1
|
|
ADR input).** An alternative shape surfaced: alktty's logical demux
|
|
(input/output/error/control sub-streams inside one channel, type byte
|
|
per chunk) applied to the SOCKS5 channel. Analysis against RFC 1928's
|
|
actual structure: **RFC 1928 is one command per connection, and control
|
|
and data never interleave in either direction.** CONNECT's control
|
|
conversation ends at the reply, after which the channel is a pure byte
|
|
stream; ASSOCIATE's control stream goes silent after the reply —
|
|
fast-socks5's own `wait_on_tcp` (`src/server.rs:1195`) treats any
|
|
post-reply control byte as protocol garbage
|
|
(`UnexpectedUdpControlGarbage`); the phase model needs no mode byte
|
|
(the reply itself synchronizes the switch — POC #2 confirmed post-reply
|
|
garbage is refused structurally). So the per-chunk type byte demuxes a
|
|
problem SOCKS5 doesn't have. What the demux genuinely buys: an
|
|
**additive in-band vocabulary** — new stream types extend the wire
|
|
additively (no format break), whereas adding a post-reply phase later
|
|
is a wire break. alktunnels accepted the opposite ("no in-band control
|
|
path, ever — the escape hatch is a new ALPN, a wire change is not");
|
|
the demux reintroduces that vocabulary at 2 B/chunk (type byte + length
|
|
prefix) on every datagram. The one-way-door cost is real either way:
|
|
demux bakes the vocabulary in before it has a user; phase model keeps
|
|
0 B overhead on CONNECT and 2 B on ASSOCIATE datagrams. Evidence after
|
|
both POCs favors the phase model — no in-band control vocabulary was
|
|
needed. Decide via ADR with the first consumer in sight.
|
|
|
|
### OQ-SK-04: fast-socks5 wasm posture — RESOLVED: inline vendor
|
|
|
|
**RESOLVED 2026-09-20: inline vendor (decided).** Two facts settled it:
|
|
(1) the ownership premise was wrong — we do *not* own fast-socks5
|
|
(third-party upstream), so the "file asks / PR" legs of the old
|
|
decision tree lose their cheap path; and (2) the wrapper's needs go
|
|
beyond the wasm gate regardless — BIND is absent upstream (POC #5
|
|
proved the wrapper must drive it itself), and the two seam fixes
|
|
(`run_udp_proxy_custom`'s unconditional bind; private `new_reply`) were
|
|
only "asks" under the ownership assumption. The change set (net-gate +
|
|
BIND + bind seam + `new_reply`) is exactly the shape POC #4 measured as
|
|
minimal (~120 cfg lines + additive additions in the `T`-generic
|
|
region), so the vendor pays for itself without leaning on the adapter
|
|
story. Mechanics in the Convergence §Layering bullet; the reasoning
|
|
trail below is retained for the record.
|
|
|
|
The decision tree (2026-09-12, both legs written under the wrong
|
|
ownership premise "we own upstream"):
|
|
|
|
- **Case 1: wasm matters for this crate → fork-first.** Make the
|
|
minimal fork and use it — offer it upstream as a PR (merge if they
|
|
want it; we carry the fork regardless; never wait on approval).
|
|
Concretely "fork" means **vendoring the relevant subset into this
|
|
crate** (the codecs, typestate machinery, client `use_stream` path —
|
|
the parts that are already `T`-generic) and gating the native-net
|
|
pieces behind the crate's own `local` feature. (The vendoring shape
|
|
is what was adopted — the PR is optional courtesy, and "shrinks to a
|
|
plain dependency" is moot since the vendor also carries BIND + the
|
|
seam fixes upstream lacks.)
|
|
- **Case 2: wasm doesn't matter → plain dependency, no fork.** Ride the
|
|
`router.rs` native path exactly as-is (the alkhttp precedent — a
|
|
crate in this suite can ship native-only). Strictly better *if its
|
|
premise holds* — a wrap-only relationship with no carried changes.
|
|
(Superseded: the premise was falsified by the BIND + seam needs.)
|
|
|
|
The root question was whether wasm makes sense for alksocks; the
|
|
analysis on file: what wasm would serve is the protocol layer
|
|
(components 1+2) in a sandboxed adapter — the alktty/alktunnels posture
|
|
that a wasm-compiled protocol crate is the protocol layer for
|
|
downstream TS/Python adapters; the natural wasm shape is a **consumer**
|
|
(speak RFC 1928 client-side over a channels `BiStream` — pure byte
|
|
framing; no local sockets needed client-side). A wasm **producer** is
|
|
odd standalone (no kernel egress) but composes — its dialer can open
|
|
alktunnels channels further downstream (the hub story). What wasm would
|
|
not serve: the quinn/noq client wrapper (since transferred to alknet) —
|
|
native-only in practice. The alkhttp contrast, honestly weighed:
|
|
alkhttp's substance is socket-native so nobody misses wasm there;
|
|
alksocks differs structurally — its protocol substance *is* byte-framing
|
|
over a generic `T` — but "costs nothing under the fork" is only an
|
|
argument once Case 1 is chosen; the use case is a planned sandboxed
|
|
SOCKS consumer. (Retrospective, 2026-09-20: the ownership correction
|
|
resolved the OQ ahead of the adapter story — the vendor is justified by
|
|
BIND + the seam fixes alone. The sandboxed-wasm-consumer adapter story
|
|
remains a product input for how much wasm surface to exercise, not a
|
|
gate.)
|
|
|
|
### OQ-SK-05: Target policy and egress scoping
|
|
|
|
SOCKS5 is arbitrary-egress by nature — the open gate plus the target
|
|
policy are the security boundary (AGENTS.md convention 12). alktunnels
|
|
resolved that whatever ACL governs the socks5 resource governs
|
|
everything reachable through it; OQ-SK-01's dial policy is the
|
|
mechanism. Residual questions for Phase 1 (note: for the *composed* UDP
|
|
egress path, per-target ACL comes free — each destination rides its own
|
|
direct-op channel governed by the tunnel grant's ACL, OQ-SK-03 variant
|
|
2):
|
|
|
|
- Does the base crate ship a per-target allowlist hook (producer-side
|
|
policy injected at registration), or is target policy entirely the
|
|
dialer's concern (the dial callback refuses)? The dialer-refuses
|
|
shape avoids a second policy layer (alktunnels' resolution pattern,
|
|
evidence-backed); the allowlist-hook shape makes a common policy
|
|
declarative.
|
|
- Is there a `SOCKS5_OPEN_SCOPE` (scope-gate on the open op, alktty
|
|
`TTY_OPEN_SCOPE` shape) — presumably yes, but the exact scope-string
|
|
convention should follow alkcall's registry conventions. POC-validated
|
|
in shape (`socks5:open`, `socks5:bind`); the convention ADR is
|
|
Phase 1's.
|
|
- Domain-form targets (RFC 1928 ATYP 0x03): resolve producer-side
|
|
(fast-socks5's `dns_resolve` config), refuse, or pass through to the
|
|
dialer unresolved? POC #2 resolved the local-egress half ("the egress
|
|
owns resolution"); the general dialer-pass-through posture is the
|
|
remaining Phase 1 choice (wasm producers may not have a resolver at
|
|
all — an interaction with the vendored crate's wasm posture,
|
|
OQ-SK-04 resolved 2026-09-20).
|
|
|
|
**Virtual address space / identity-scoped subnets (added 2026-09-14,
|
|
post-POC #2).** A proposal surfaced during the findings review: give
|
|
identities scoped virtual subnets (e.g. 10.x.0.0/16 slices) so proxied
|
|
resources become IP-addressable — "BND.ADDR ↔ channel id" translation,
|
|
a hub exposing proxied resources per identity under ACL. The goal is
|
|
adoptable; the mechanism needs **no wire change**: DST.ADDR and
|
|
BND.ADDR are arbitrary RFC fields, so virtual addresses ride ordinary
|
|
CONNECT/BIND requests — the translation layer is the dial callback
|
|
(OQ-SK-01's shape: `dial(target)` matches a virtual subnet against a
|
|
per-identity resource table), per-identity scoping is resources + ACL
|
|
+ target policy (this OQ), and hub exposure is the
|
|
terminate-and-re-produce story with no new wire vocabulary. BND.ADDR
|
|
stays per the handle swap (OQ-SK-03): in-band the handle is the channel
|
|
(compat surface only); a real network-B address exists wherever a local
|
|
backend binds — the listener's `local_addr` (OQ-SK-08 variant 2, the
|
|
bind-first `bound` reply field). The one place a virtual subnet becomes
|
|
*routable from vanilla apps* is the full-tunnel composition: the host
|
|
routes the subnet into the proxy (tun2proxy), so
|
|
`connect(10.x.y.z:p)` is an ordinary CONNECT the producer translates.
|
|
Worth checking tun2proxy's virtual-network/virtual-DNS support as
|
|
prior art before Phase 1 decides whether the crate ships a
|
|
translation-table helper or leaves it entirely to assembly-layer
|
|
policy.
|
|
|
|
### OQ-SK-06: noq client support — RESOLVED by transfer
|
|
|
|
**Resolved by the scope decision (2026-09-14, §Vision): transferred to
|
|
the alknet rewrite.** The client wrapper is alknet's ancestor-shaped
|
|
component (ADR-090 + quinn-proxy POC already live there), consumes no
|
|
channels, and has no consumers planned inside this crate — so its
|
|
research questions (the noq trait-shape comparison: `create_sender() ->
|
|
Pin<Box<dyn UdpSender>>` replacing `create_io_poller` + `try_send`, the
|
|
`Box<dyn AsyncUdpSocket>` constructor ownership change; the dependency
|
|
posture — noq is consumed from the n0 workspace/git, not crates.io; the
|
|
ECN/MTU carry-over) move to alknet's Phase 0 wholesale. This crate's
|
|
client-side story is complete without it: the channels-native consumer
|
|
session is POC-validated (POC #1's `use_stream` over a `BiStream`, POC
|
|
#2's associate session). The noq POC (POC #3) is dropped from this
|
|
crate's register accordingly. The noq surface notes are retained under
|
|
§Prior art as alknet-facing background.
|
|
|
|
### OQ-SK-07: POC register (closed)
|
|
|
|
**All POCs resolved; the register is closed for this crate's Phase 0.**
|
|
Numbering is historical (do not renumber — findings files and commit
|
|
messages reference these numbers):
|
|
|
|
| # | What | Status | Where |
|
|
|---|------|--------|-------|
|
|
| 1 | Channels-native SOCKS5 CONNECT | **Passed 2026-09-13** | `poc-connect-wasm-findings.md` |
|
|
| 2 | UDP ASSOCIATE (handle-swap shape) | **Passed 2026-09-14** | `poc-udp-associate-findings.md` |
|
|
| 3 | noq `AsyncUdpSocket` impl | **Dropped 2026-09-14** — transferred to alknet with OQ-SK-06 | — |
|
|
| 4 | fast-socks5 wasm fork-minimality | **Passed 2026-09-13** — folded into #1's worktree, shares #1's findings file | `poc-connect-wasm-findings.md` §POC #4 |
|
|
| 5 | BIND (accept-side command) | **Passed 2026-09-15** | `poc-bind-findings.md` |
|
|
|
|
Highlights (details in the findings files):
|
|
|
|
- **POC #1** validated the `BiStream`-as-`T` genericity claim,
|
|
`register_openable_with_establisher` fit, `pump_bidi` as the data
|
|
plane, the dial-at-command-read layering, the identity seam end to
|
|
end, typed refusal paths (session-level at the registry ACL;
|
|
target-level as RFC reply codes), and the curl front door.
|
|
- **POC #4** measured the fast-socks5 wasm gating at ~120 cfg lines
|
|
across 5 files; the wasm-clean subset is the *functional* protocol
|
|
subset (a full RFC 1928 CONNECT conversation passes
|
|
`--no-default-features`).
|
|
- **POC #2** validated the handle-swap baseline end to end (both
|
|
egress variants; composed per-destination open cost ~0.8 ms;
|
|
front-door RFC-codec composition) and produced the load-bearing API
|
|
findings: the datagram stage is `select!`-driven with the egress
|
|
readiness contract (`next_reply` must be `Pending` when no reply is
|
|
ready — an instantly-ready `Ok(None)` starves a biased select into
|
|
livelock), framing applied exactly once by the wire layer (inner
|
|
bytes at the egress seam), and the egress/session ownership must ride
|
|
the table entry (teardown-at-birth).
|
|
- **POC #5** validated the BIND phase model end to end (both accept
|
|
variants; the `socks5:bind` scope gate; failure mapping RFC-faithful;
|
|
the vanilla front door for BIND), pinned the composed-variant
|
|
sequencing against alktunnels 0.1.0 (superseded in shape by the
|
|
bind-first graduation — see OQ-SK-08), and surfaced the
|
|
ownership-rides-the-handle lesson as the third witness of the
|
|
teardown-at-birth pattern.
|
|
|
|
POC placement conventions (inherited from alktunnels): a POC that needs
|
|
code from this repo runs in a worktree/branch (`.worktrees/research/
|
|
<task-id>/` per the SDD process); a self-contained POC runs as a
|
|
standalone crate in the global workspace with findings written into
|
|
`docs/research/` here. Findings always land in `docs/research/`
|
|
regardless of where the code lives.
|
|
|
|
### OQ-SK-08: BIND — the accept-side command
|
|
|
|
**Status: use case found, POC-validated (POC #5, 2026-09-15); the
|
|
composed path composes against alktunnels 0.2.0 (the graduation
|
|
superseded the ask ledger's shape); the ship-in-base-crate and default
|
|
accept variant are Phase 1 ADRs.**
|
|
|
|
**The gap (fast-socks5):** `Socks5Command::TCPBind` is parsed
|
|
(`lib.rs:91`) but every
|
|
handler refuses it (`lib.rs:384-386`, `server.rs:813-816` —
|
|
`CommandNotSupported`); no bind machinery exists. The typestate flow
|
|
(`read_command()` returning `cmd` + `TargetAddr`) means the wrapper
|
|
drives the command dispatch itself — implementing BIND is wrapper-side
|
|
code, carried in the vendor (no upstream ask). POC-confirmed:
|
|
`reply_success`'s `sock_addr` parameter carries reply#1's `BND.ADDR`
|
|
verbatim; the courtesy item is publishing `new_reply` (rides the
|
|
vendor — see §Prior art's carried needs).
|
|
|
|
**The shape (convergent precedent: iroh-socks5's channels-native BIND —
|
|
server relays raw bytes post-reply for CONNECT/BIND, `docs/research/
|
|
iroh-socks5-eval.md`):** one channel per BIND
|
|
session, phase model — request → reply#1 carrying `BND.ADDR` → quiet
|
|
wait (any control byte after reply#1 is protocol garbage, same as
|
|
ASSOCIATE's post-reply stage) → exactly one inbound accept → reply#2
|
|
carrying the peer address → raw pass-through data plane (zero
|
|
per-packet headers in-band, same as CONNECT; only the ASSOCIATE
|
|
datagram stage is framed). RFC-refusal of BIND remains valid for old
|
|
consumers; support is purely additive (same ALPN, same params, no
|
|
framing change — no one-way-door exposure).
|
|
|
|
**The mechanism — accept-side resources (the `-R` far-side listener
|
|
shape).** The consumer's `-R` path in alktunnels proves accept-side
|
|
resources exist without forced binds; SOCKS5 BIND is their mirror
|
|
image: the producer-side listener (network B — the app server must
|
|
reach it) is the accept resource, each popped accept two-pumps against
|
|
the SOCKS5 `BiStream`. The phase-0 hand-off note ("a producer-side BIND
|
|
wants the accept-side shape, not a dial") predicted exactly this; the
|
|
composition supplies the use case it lacked.
|
|
|
|
**Accept variants (mirror of OQ-SK-03's egress variants):**
|
|
|
|
1. **Local backend binds for real** (feature-gated `local`) — the
|
|
faithful RFC shape, iroh-socks5-faithful. A real network-B address
|
|
goes in reply#1, vanilla front-door clients work; FTP active mode
|
|
is the canonical use and the reason vanilla client libraries speak
|
|
BIND at all. No upstream ask.
|
|
2. **Composed (alktunnels listen resource)** — the listener lives on a
|
|
listen resource (per-listener ACL for free, hub-relayable per the
|
|
terminate-and-re-produce story). **Superseded in shape (2026-09-20):
|
|
the ask ledger landed via the bind-first/forwarded graduation** —
|
|
see below.
|
|
3. **Wrapper-aware only** — zeros in reply#1, reply#2 in-band;
|
|
simplest, but vanilla clients lose BIND and the use case mostly
|
|
evaporates. Retained for completeness; weak hunch against.
|
|
|
|
**POC #5's resolution (2026-09-15 — `poc-bind-findings.md`):** variants
|
|
1 and 2 both implemented and passing. The hunch (variant 1 as the
|
|
faithful + zero-ask default) was right. Against alktunnels 0.1.0 the
|
|
composed variant needed a pinned workaround — **reply#1 first, the
|
|
tunnel open rides the accept future** (the accept-first establisher
|
|
could not open before reply#1 without deadlocking into the
|
|
establishment timeout) — plus a two-item ask ledger (learn the bound
|
|
address without consuming an accept; peer metadata on accepted
|
|
handles).
|
|
|
|
**The graduation landed — superseded in shape (2026-09-20, alkcall
|
|
0.8.0 + alktunnels 0.2.0).** The graduation (alktunnels
|
|
`docs/research/tunnels-graduation.md` → ADRs 007/008) took the ledger's
|
|
*two asks* as evidence and adopted the ssh `forwarded-tcpip`-shaped
|
|
alternative over the per-ask carriers:
|
|
|
|
- **Ask #1 (learn the bound address without consuming an accept) is
|
|
dissolved, not implemented.** The bind-first establisher
|
|
(`bindfirst_establisher`, ADR-008 §1) binds during the establishment
|
|
phase — fast, no app-server dependency — so the open-op reply
|
|
natively carries the real OS-chosen bound address (the `bound` reply
|
|
field, projected via alkcall 0.8.0's
|
|
`Establishment::with_reply_field`, ADR-049 amendment 3; consumed via
|
|
`open_channel_with_reply` / `TunnelListener::bound()`). There is no
|
|
accept to avoid consuming: establishment = bind. This *replaces* the
|
|
pinned "reply#1 first, the tunnel open rides the accept future"
|
|
workaround (it existed for the accept-first establisher) and
|
|
dissolves POC #5's establishment-bound finding: the accept wait now
|
|
lives in the teardown-governed pump phase, so FTP-length BIND waits
|
|
stop resolving as spurious 10s-establishment refusals (ADR-008's
|
|
stated second win). The old accept-first surface
|
|
(`listen_establisher` + `AcceptQueue`) stays in 0.2.0 for the
|
|
accept-as-open shape.
|
|
- **Ask #2 (peer metadata on accepted handles) landed at the wire's
|
|
native moment.** The forwarded op (`channels/tunnel/forwarded`,
|
|
ADR-008 §3) carries `{listen, substrate, peer}` params per accept —
|
|
the peer address rides the open-op params (deny-unknown,
|
|
schema-gated) rather than a `TargetHandle` field; `ForwardedOpener` +
|
|
`make_forwarded_op_handler` are the serving side. Correlation is a
|
|
producer-minted u32 token (ADR-008 Amendment 1, 2026-09-18: the
|
|
`listen` reply field + forwarded param), not the originally-planned
|
|
channel-ID key.
|
|
|
|
Consequence for variant 2: the composed BIND path is now *fully
|
|
vanilla-fidelity* with no interim stand-ins — the factory no longer
|
|
carries an expected `bind_addr`, reply#1 advertises the real `bound`,
|
|
reply#2 carries the real peer, and no establishment bound threatens
|
|
long BIND waits. POC #5's findings remain accurate as the record of
|
|
what was validated against alktunnels 0.1.0; the Phase 1 shape composes
|
|
against 0.2.0 (`register_tunnel_bindfirst_openable` +
|
|
`BindFirstInjections`, or `TunnelListener` on the opener side).
|
|
|
|
**Security boundary note:** BIND is an *inbound* listener — the
|
|
accept-side twin of arbitrary egress. Whatever ACL governs the socks5
|
|
resource now also governs who can mint listeners on the producer's
|
|
network; per-listener policy (address/port ranges allowed to bind) is
|
|
the accept-callback's target-policy twin (OQ-SK-05's mirror). The
|
|
open-op gate (`SOCKS5_OPEN_SCOPE`) must distinguish CONNECT/ASSOCIATE
|
|
from BIND, or any identity with proxy egress can also open ingress
|
|
listeners. POC-validated (`socks5:bind` checked at command dispatch
|
|
from the per-call opener identity; `ConnectionNotAllowed` (0x02)
|
|
refusal while CONNECT still serves the same identity).
|
|
|
|
### OQ-SK-09: Channel capacity for proxy workloads (the 256 default)
|
|
|
|
**Status: analysis complete (2026-09-14); the documented proxy-workload
|
|
recommendation is Phase 1's.** The 256-channel default cap
|
|
(per-connection ADR-040; per-identity ADR-041) is tight for a proxy
|
|
workload: every proxied flow *is* a channel (a browser or tun2proxy
|
|
fan-out opens one CONNECT channel per flow; composed UDP egress spends
|
|
a downstream channel per destination; BIND spends an accept resource
|
|
per listener).
|
|
|
|
- **No wire change** — rejected permanently: the caps are policy knobs
|
|
(configurable without a wire change; the `u32` channel-ID wire space
|
|
is ~4 billion). The resolution is config, not wire.
|
|
- **Assembly-layer config** is the mechanism (ADR-040/041): a
|
|
proxy-shaped deployment raises the per-connection/per-identity caps;
|
|
the default stays conservative for everyone else.
|
|
- **Documented proxy-workload default** for Phase 1: the crate docs
|
|
should recommend a raised cap (and its memory arithmetic: a session
|
|
is bounded + predictable — CONNECT/ASSOCIATE are bounded-buffer
|
|
pumps, BIND adds one listener + one accept) so deployments don't
|
|
discover the cap in production.
|
|
- **Defense-in-depth posture:** the cap's rationales both still apply
|
|
under SOCKS5. Per-connection memory bound — arguably *weaker* here,
|
|
since the three command shapes are well-known and bounded (a
|
|
session's memory is predictable); per-identity DoS brake — arguably
|
|
*stronger* here, since each SOCKS5 session maps to real producer-side
|
|
resources (a dial, a downstream channel, an accept listener) and the
|
|
cap is the brake on one identity opening thousands. Layered defense:
|
|
ACL → per-connection cap → per-identity cap → dial/accept policy;
|
|
relaxing the caps leans on the rest.
|
|
|
|
**Considered and rejected: in-channel port multiplexing.** A related
|
|
proposal (2026-09-14 review) would reframe the cap by multiplexing
|
|
"ports" inside one channel — a `[port: u16][len][payload]` per-chunk
|
|
prefix (channel-id-as-IP, port-as-port; modeled on alktty's wire).
|
|
Rejected on the established grounds: it re-opens the demux-vs-phase
|
|
trade OQ-SK-03 already resolved (phase model, no in-band control
|
|
vocabulary needed across both POCs), it breaks the 0-B pass-through
|
|
that lets fast-socks5's raw `transfer` drop in unchanged (POC #1's
|
|
validated fit), it imposes chunk boundaries on a stream RFC 1928 treats
|
|
as unstructured, and it violates the one-channel-per-SOCKS5-session
|
|
convention (AGENTS.md #10). Channel-ID economy is not a wire problem
|
|
the u32 space + policy knobs fail at. **Revisit trigger:** if a
|
|
deployment hits a fixed upstream channel budget that config cannot
|
|
raise, the additive escape is a *new multiplexed ALPN* — never a change
|
|
to this ALPN's framing.
|
|
|
|
**The related idea that survives — virtual address space /
|
|
identity-scoped subnets** — is recorded under OQ-SK-05: BND.ADDR/
|
|
DST.ADDR translation via the dial callback, no wire change.
|
|
|
|
## Survey / prior-art list
|
|
|
|
Candidate reading for the research specialist (to be expanded):
|
|
|
|
- RFC 1928 (SOCKS5) — the fixed protocol: method negotiation, CONNECT,
|
|
BIND, UDP ASSOCIATE, reply codes. RFC 1929 (username/password).
|
|
- fast-socks5 `/workspace/fast-socks5` — `src/server.rs` (typestate
|
|
API, interception points, auth traits), `src/client.rs`
|
|
(`Socks5Stream`/`Socks5Datagram`, `use_stream`), `src/lib.rs`
|
|
(`new_udp_header`/`parse_udp_request`, `ReplyError`),
|
|
`examples/router.rs` (conditional interception), `examples/
|
|
custom_auth_server.rs`. (Third-party upstream; the inline vendor —
|
|
OQ-SK-04, resolved 2026-09-20 — vendors the `T`-generic subset into
|
|
this crate; this checkout is the differential-testing reference.)
|
|
- iroh-socks5 — `crates.io/crates/iroh-socks5` v0.5.0, repo
|
|
`github.com/mattgeddes/iroh-socks5` (crates.io has no repository
|
|
link; the published source matches). Evaluated in
|
|
`docs/research/iroh-socks5-eval.md`: not the protocol base (iroh
|
|
coupling, RFC subset), but the OQ-SK-03 UDP prior art (framed
|
|
`{addr, data}` in-band datagrams, no flow table, control-EOF
|
|
association lifetime) and an independent confirmation of POC #1's
|
|
one-stream-per-session shape.
|
|
- alktunnels — `docs/research/phase-0-findings.md` (the `-D`
|
|
composition conclusion, hub-owns-the-connection, the UDP codec
|
|
decision + F-2), `docs/research/tunnels-graduation.md` (the direct
|
|
op + bind-first/forwarded graduation — the substrate this crate
|
|
composes against), `docs/architecture/` (params/ALPN/ACL ADR
|
|
template), POC summaries (POC placement and findings conventions).
|
|
- alkcall — `docs/architecture/decisions/` ADR-037/039/047/049/050/051
|
|
(channel ops, params-is-ALPN-specific, establishment, pump_bidi,
|
|
reply projection, hub relay), ledger CF-005/CF-006 (identity seam),
|
|
`src/channels/operations.rs` (`ChannelCore`, `OpenHandler`,
|
|
`Establishment`, `ChannelPlan`), `src/channels/pump.rs`.
|
|
- alknet — `crates/alknet-client/src/socks5.rs` (ADR-090, the client
|
|
ancestor), `docs/research/quinn-quic-proxy/findings.md` (the POC
|
|
findings: trait + constructor surface, ECN/MTU limitations,
|
|
BotBrowser production precedent).
|
|
- noq — `/workspace/noq`: `noq/src/runtime/mod.rs` (`AsyncUdpSocket`,
|
|
`UdpSender`), `noq/src/endpoint.rs` (`new_with_abstract_socket`),
|
|
`noq-udp/src/lib.rs` (`RecvMeta`, `Transmit`), `Cargo.toml` (workspace
|
|
versioning / publication posture). (Research transferred to the
|
|
alknet rewrite with OQ-SK-06 — retained here as alknet-facing
|
|
background.)
|
|
- alktty — backend inversion point (`TtyBackend`), `TTY_OPEN_SCOPE`
|
|
scope-gating shape, feature-gated `local` backend, wasm-clean
|
|
default-crate verification commands.
|
|
- tun2proxy — `/workspace/tun2proxy` `src/udpgw.rs` and its SOCKS5
|
|
files (`socks.rs`, `proxy_handler.rs`): UDP-over-stream framing and
|
|
flow-table prior art (analyzed in alktunnels phase-0-findings; the
|
|
SOCKS5-specific parts feed OQ-SK-03). Also the endgame composition
|
|
partner: it takes `--proxy socks5://...` upstream natively
|
|
(`src/args.rs`) — the vpn-like endgame is tunnel + local bind +
|
|
tun2proxy (§Vision, principle 6).
|
|
- alkhttp — `/workspace/@alkdev/alkhttp`: the native-only precedent in
|
|
the suite (axum/reqwest/hyper, no wasm target) — cited in OQ-SK-04
|
|
as the "wasm-clean is preferred, not mandatory" escape hatch.
|
|
- iroh — `/workspace/iroh` `iroh/Cargo.toml` (the noq dependency
|
|
posture: `noq = "1.2.0"` from the n0 workspace) — context for
|
|
OQ-SK-06's dependency question (transferred to the alknet rewrite);
|
|
also the privacy motivation that originally motivated the client
|
|
wrapper (relays/peers see the real IP).
|
|
|
|
## Convergence checklist (what Phase 0 must produce)
|
|
|
|
- [x] Vision + guiding principles captured (this doc, §Vision —
|
|
including the component split and the scope resolution)
|
|
- [x] Prior-art pass complete: fast-socks5 surface verified (§Prior
|
|
art), alktunnels/alknet/noq lineage mapped, anti-prior-art list
|
|
written; iroh-socks5 evaluated (structural sibling, not the base;
|
|
OQ-SK-03 prior art — `docs/research/iroh-socks5-eval.md`);
|
|
ownership premise corrected 2026-09-20 (fast-socks5 is
|
|
third-party upstream; the posture is the inline vendor, OQ-SK-04
|
|
resolved)
|
|
- [x] OQ-SK-01 (dial policy) — shape resolved empirically (POC #1:
|
|
dial callback at the command-read interception point, boxed
|
|
halves, no trait); the A/B assembly-layer choice is a Phase 1
|
|
policy decision
|
|
- [x] OQ-SK-02 (auth mapping) — settled 2026-09-13 ("both," split by
|
|
the door; POC #1 validated the seam end to end); Phase 1
|
|
formalizes the door's credential→identity mapping
|
|
- [x] OQ-SK-03 (UDP ASSOCIATE) — the handle-swap baseline settled
|
|
2026-09-13 and POC-validated end to end (POC #2, 2026-09-14);
|
|
the datagram framing is wire-stable once published — the Phase 1
|
|
ADR (framing + demux-vs-phase) remains, with empirical ground
|
|
- [x] OQ-SK-04 (fast-socks5 wasm) — resolved 2026-09-20: the inline
|
|
vendor (the ownership premise was corrected — we don't own
|
|
fast-socks5; BIND + the seam fixes must be carried regardless,
|
|
so the vendor pays for itself independent of the adapter story;
|
|
POC #4 de-risked the mechanism)
|
|
- [x] OQ-SK-05 (target policy) — dialer-refuses shape evidence-backed;
|
|
per-target-ACL-for-free validated for the composed egress path
|
|
(POC #2); scope-gate convention (`socks5:open`/`socks5:bind`)
|
|
POC-validated in shape; the allowlist variant, exact scope
|
|
convention, and the 2026-09-14 virtual-subnet proposal are
|
|
Phase 1 decisions
|
|
- [x] OQ-SK-06 (noq client) — resolved by the scope decision
|
|
(2026-09-14): transferred to the alknet rewrite; this crate's
|
|
channels-native client story is POC-complete without it
|
|
- [x] OQ-SK-07 (POC register) — closed: #1, #2, #4, #5 passed; #3
|
|
dropped with the scope decision; findings in `docs/research/`
|
|
- [x] OQ-SK-08 (BIND) — use case + mechanism found; POC #5 validated
|
|
the shape end to end; the ask ledger landed 2026-09-18/20
|
|
(superseded in shape by the bind-first/forwarded graduation);
|
|
the remaining Phase 1 ADR (ship in base crate? default accept
|
|
variant?) has empirical ground under it
|
|
- [x] OQ-SK-09 (channel capacity) — proxy-workload analysis written
|
|
(wire change rejected; config is the mechanism; in-channel port
|
|
multiplexing considered and rejected); Phase 1 pins the
|
|
documented proxy-workload recommendation
|
|
- [x] Targeted POCs run + summaries in `docs/research/` — complete;
|
|
no pending POCs remain for this crate's Phase 0
|
|
- [x] Converge: recommended approach written up, ready to hand to the
|
|
Architect for Phase 1 (§Convergence, 2026-09-14)
|
|
|
|
## Convergence — the recommended approach
|
|
|
|
This is Phase 0's output: a clear WHAT/WHY with validated approaches,
|
|
ready for the Architect.
|
|
|
|
### The crate, scoped
|
|
|
|
**alksocks = the SOCKS5 producer/consumer protocol pair on alkcall
|
|
channels (socks + channels, nothing else).** The component split
|
|
resolved: components 1+2 are this crate; component 3 (the quinn/noq
|
|
`AsyncUdpSocket` client wrapper) moves to the alknet rewrite, where its
|
|
ancestor work (ADR-090, quinn-proxy POC) already lives. No local binds
|
|
except the two explicit, optional, feature-gated assembly shapes
|
|
(producer's local backend; consumer's front door).
|
|
|
|
### The recommended approach (validated pieces → crate shape)
|
|
|
|
- **Protocol base: the vendored fast-socks5 explicit typestate API** —
|
|
the `T: AsyncRead + AsyncWrite + Unpin` genericity carries both
|
|
substrates (POC #1: a channels `BiStream` drops in directly; the
|
|
local backend feeds a `TcpStream`). Drive the typestate directly for
|
|
UDP ASSOCIATE (`run_udp_proxy_custom` is unusable on channels —
|
|
unconditional kernel bind; the vendor fixes the seam in-tree).
|
|
- **Wire shape: the handle swap** (POC #2-validated) — one ALPN
|
|
(`alk/socks5`), one open op (`channels/socks5/sub`), trivial params;
|
|
the RFC conversation is the data plane; CONNECT = `pump_bidi`
|
|
pass-through post-reply (POC #1); ASSOCIATE = the `select!`-driven
|
|
datagram stage over `[len: u16 BE][addr + data]` frames (alktunnels'
|
|
length codec; RSV/FRAG only at front doors; channel EOF ends the
|
|
association; per-datagram addressing, no flow table); BIND = the
|
|
phase model extended (POC #5).
|
|
- **Egress/accept seams: the dial-callback shape, extended** — `DialFn`
|
|
for CONNECT (POC #1's command-read interception point),
|
|
`EgressFactory` for ASSOCIATE (POC #2), and `BindAcceptFactory` for
|
|
BIND (POC #5): all function-not-trait, all returning boxed halves.
|
|
Local-socket and composed providers are assembly-layer policies; the
|
|
composed variant's per-destination open cost is measured (~0.8 ms)
|
|
and its per-target-ACL-for-free benefit is validated. The composed
|
|
providers compose against alktunnels 0.2.0 — the direct op
|
|
(`channels/tunnel/direct`, ADR-007) is the dynamic-target channel
|
|
for CONNECT, and `open_direct` + `Substrate::Udp` is the composed
|
|
ASSOCIATE vehicle (both substrates, one op, one grant:
|
|
`tunnel:direct`); the hub-leg for relaying them is alkcall 0.8.0's
|
|
in-tree `ChannelRelay`/`HubLegTemplate` (ADR-051).
|
|
- **Auth: "both," split by the door** (OQ-SK-02's settled posture) —
|
|
producer side: the alkcall identity seam only (the in-band
|
|
conversation runs NoAuth); the consumer's front door owns RFC 1929
|
|
for vanilla clients and maps credentials to the channel identity.
|
|
- **Layering: the alktty/alktunnels precedents** — substrate-agnostic
|
|
protocol modules at the crate core; feature-gated backend modules
|
|
(local binds) never imported by the protocol layer; producer +
|
|
consumer + params modules mirroring POC #1/#2's structure. **The
|
|
fast-socks5 posture is the inline vendor as an extraction (OQ-SK-04,
|
|
resolved 2026-09-20; extraction pinned 2026-09-21, ADR-011
|
|
§Amendment)**: extract the `T`-generic subset into this crate (the
|
|
codecs, typestate machinery, client `use_stream` path; the
|
|
kernel-net handlers and `socks4` are not copied, so there is no
|
|
`net` feature — POC #4's gate map survives as the extraction
|
|
recipe) — and carry the two extracted-region needs in the same
|
|
vendor: BIND (absent upstream; POC #5's typestate-driven shape) and
|
|
the `new_reply` publication. `/workspace/fast-socks5` stays the
|
|
differential-testing reference; note the vendor point (v1.0.0) in
|
|
the vendored code.
|
|
- **Open for Phase 1 ADRs** (inputs collected, decisions pending): the
|
|
demux-vs-phase extensibility trade (evidence now favors the phase
|
|
model — no in-band control vocabulary was needed across both POCs);
|
|
the datagram-framing ADR (the one-way door — decide with the first
|
|
consumer in sight); the params-format/ALPN naming ADRs (wire-stable
|
|
once published, no consumers exist yet); `SOCKS5_OPEN_SCOPE`
|
|
convention (must distinguish BIND from CONNECT/ASSOCIATE — OQ-SK-08's
|
|
security note); the door's credential→identity mapping shape
|
|
(OQ-SK-02); the A/B egress provider default (OQ-SK-01); BIND's
|
|
base-crate inclusion + default accept variant (OQ-SK-08); the
|
|
proxy-workload channel-cap recommendation (OQ-SK-09); target-policy
|
|
shape + the virtual-subnet translation-layer shape (OQ-SK-05). The
|
|
wasm decision itself is settled; only the sandboxed-wasm-consumer
|
|
adapter story remains a product input, and nothing waits on it.
|
|
|
|
### What Phase 0 could NOT answer (handed to Phase 1)
|
|
|
|
- The datagram-framing ADR (the one-way door) — empirical ground in
|
|
hand: framing validated, LRU/dedup semantics pinned, open cost
|
|
measured. Decide with the first consumer in sight.
|
|
- Target-policy shape (OQ-SK-05) — the dialer-refuses shape is
|
|
evidence-backed; the declarative-allowlist variant stays a Phase 1
|
|
choice. (The 2026-09-14 virtual-subnet proposal is recorded there —
|
|
dial-callback translation, no wire change.)
|
|
- Channel capacity for proxy workloads (OQ-SK-09) — wire change
|
|
rejected; assembly-layer config + a documented proxy-workload cap
|
|
recommendation stay Phase 1 decisions.
|
|
- BIND — validated end to end (POC #5); the graduation supplied the
|
|
composed variant's full-fidelity substrate (alktunnels 0.2.0's
|
|
bind-first establisher + forwarded op). Deciding whether BIND ships
|
|
in the base crate (and which accept variant is default) is a Phase 1
|
|
ADR — support is additive (old consumers still get the RFC refusal),
|
|
so nothing here is a one-way door.
|
|
- The door's credential→identity mapping shape (OQ-SK-02) and the
|
|
scope-string convention — Phase 1 ADRs with POC ground under them. |