docs(arch): client-dial SOCKS5 proxy seam — ADR-090, OQ-67

AlknetClient gains an optional SOCKS5 proxy (with_socks5_proxy) so a
native client can hide its real IP from the hub. dial_quic routes QUIC
through SOCKS5 UDP ASSOCIATE (validated by the /workspace/quinn-proxy-poc
PoC — quinn's AsyncUdpSocket + new_with_abstract_socket is the extension
point, 5/5 runs clean); dial_tcp_tls routes through SOCKS5 CONNECT. The
proxy is invisible above the dial (Connection, dispatch, credentials,
TLS config all proxy-unaware) and the no-proxy path is the zero-cost
default (socks5 feature + fast-socks5 dep are opt-in).

SOCKS5 is the sole proxy protocol (covers both TCP and UDP, so no HTTP
CONNECT variant needed). The two distinct SOCKS5 concepts — the
client-dial proxy (ADR-090, transport-layer privacy) and the planned
alknet-socks5 channels data-channel handler (ADR-085 scope, a service
one side offers the other) — compose at the SOCKS5 protocol level
without alknet-type-level coupling.

iroh is the exception: dial_iroh does not consume Socks5ProxyConfig —
iroh's proxy_url covers the relay-exposure surface, but the
direct-connection peer-exposure case is OQ-67 (deferred(unclear) — the
pieces exist but the iroh socket-stack composition isn't clear; does
not block the first hub deployment, which uses QUIC/TCP+TLS).

- ADR-090: Client-Dial SOCKS5 Proxy Seam
- OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure)
- client README: proxy section, struct/builder, Proxy error variant,
  socks5 feature gate, deps, assembly example, decisions/open-questions
- README/open-questions: index entries, Current State, OQ count 67/20
This commit is contained in:
glm-5.2 committed 2026-07-16 08:42:33 +00:00
1 parent ed40f95d96
commit b7d67e5a5f
5 files changed
+775 -11

No files matched your search

@@ -0,0 +1,96 @@
# OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure)
- **Origin**: `docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md`
§5; `docs/architecture/crates/client/README.md` §"SOCKS5 proxy
(ADR-090)".
- **Status**: deferred(unclear)
- **Door type**: two-way
- **Priority**: medium
- **Impacts**: A client that dials over iroh direct (hole-punched)
connections and wants to hide its real IP from the peer. Does NOT
block the first hub deployment — the hub's outbound worker dials
(the hub-as-client case, ADR-087 §5) use QUIC or TCP+TLS, both of
which honor the proxy (ADR-090). Does NOT block the relay-mediated
iroh path — iroh's relay already hides the client's IP from the peer
(the peer sees the relay's IP), and iroh's `proxy_url` (set at the
assembly layer) hides the client's IP from the relay. The gap is the
iroh *direct* path: when iroh hole-punches a direct QUIC connection,
the peer sees the client's real IP, and `Socks5ProxyConfig` (ADR-090)
does not cover it.
- **Investigation**: Work through the iroh socket stack to determine
whether iroh exposes a socket abstraction analogous to quinn's
`AsyncUdpSocket` + `new_with_abstract_socket` (the hook the quinn
POC uses, validated in `docs/research/quinn-quic-proxy/findings.md`).
If it does, a `Socks5UdpSocket`-equivalent for iroh is the same shape
as the quinn integration (adapt the PoC). If it does not, the
alternatives are: (a) force relay-only when a proxy is configured
(disable iroh's direct-connection path — the peer always sees the
relay's IP, never the client's), or (b) accept the gap (document that
iroh direct connections expose the client's IP, and privacy-conscious
iroh deployments use `proxy_url` + relay-only). The investigation
needs a concrete iroh-direct-with-proxy use case to drive the choice
— without one, "force relay-only" is the conservative default.
- **What is decided (ADR-090 §5)**: `dial_iroh` does **not** consume
`Socks5ProxyConfig`. iroh's `proxy_url` (set at `iroh::Endpoint`
construction by the assembly layer — ADR-089 §3, "iroh shares the
key, not the config") covers the relay-exposure surface: it proxies
iroh's HTTP(S) traffic (relay connections, DNS-over-HTTPS, pkarr
publishing) through the configured proxy, hiding the client's IP
from iroh's relay. For the relay-mediated path, this covers both
exposure surfaces (peer sees relay's IP; relay sees proxy's IP). The
open case is the direct path.
- **What is open**: how to cover iroh's *direct* (hole-punched)
connection path when a SOCKS5 proxy is configured. Three candidate
approaches:
1. **Wrap iroh's QUIC in SOCKS5 UDP ASSOCIATE.** Analogous to the
quinn POC — if iroh exposes a socket abstraction that accepts a
custom UDP impl, wrap it in a `Socks5UdpSocket`-equivalent. This
is a *different* integration than the quinn POC (iroh has its own
socket stack, not `quinn::AsyncUdpSocket`), so the PoC does not
directly transfer. Requires investigating iroh's API surface for a
socket-injection hook.
2. **Force relay-only when a proxy is configured.** Disable iroh's
direct-connection path when `Socks5ProxyConfig` is set (or when
the assembly layer sets iroh's `proxy_url`); all iroh connections
go through the relay, which hides the client's IP from the peer
by design. This is the conservative default — no new socket
integration, no ECN loss (the relay path is TCP-based), but it
gives up iroh's direct-connection latency advantage.
3. **Accept the gap.** Document that iroh direct connections expose
the client's IP to the peer, and that privacy-conscious iroh
deployments should use `proxy_url` (relay-exposure) + force
relay-only (peer-exposure). This is honest but leaves the
`Socks5ProxyConfig` partially effective for iroh.
The choice depends on whether a concrete iroh-direct-with-proxy use
case exists that needs the peer-IP privacy AND the direct-connection
latency. Without that use case, (2) is the conservative default; if
the use case exists, (1) is the target (and needs the iroh socket
investigation).
- **Why deferred(unclear), not deferred(scope)**: The pieces exist —
SOCKS5 UDP ASSOCIATE is validated for quinn (the PoC), iroh's
`proxy_url` exists and covers the relay path, iroh's direct vs relay
path is a known iroh property. What is unclear is the *composition*
for iroh specifically: does iroh expose a socket hook (making (1)
feasible), and is there a concrete use case that needs direct-path
privacy (making (1) worth building vs. (2) sufficient)? The
resolution is *investigation* (work through iroh's socket stack, find
the use case), not *waiting* (for a spec or use case to arrive). The
quinn POC settled the quinn case; the iroh case needs its own
investigation, not a copy of the quinn answer.
- **Resolution**: Not yet decidable. The iroh socket stack needs
investigation (does it expose a `new_with_abstract_socket`-equivalent?),
and a concrete iroh-direct-with-proxy use case needs to surface
(or a deliberate decision to force relay-only needs to be made). The
quinn POC's `Socks5UdpSocket` is the reference shape if iroh exposes
the hook; the "force relay-only" fallback is the conservative default
if it does not or if no use case demands direct-path privacy. This
does not block the first hub deployment or the QUIC/TCP+TLS proxy
capability (ADR-090) — it is a gap specific to iroh direct
connections.
- **Cross-references**: ADR-090 (§5 — defers iroh proxy support to
this OQ), ADR-089 (§3 — iroh shares the key, not the config;
`proxy_url` is set at the assembly layer), ADR-087 (§3 — the iroh
exception on the client side),
[`docs/research/quinn-quic-proxy/findings.md`](../../research/quinn-quic-proxy/findings.md)
(the quinn-over-SOCKS5 PoC — the reference shape for the iroh
investigation).