docs(arch): resolve OQ-67 — iroh proxy force-relay-only + HTTP-to-SOCKS5 bridge (ADR-090 §5 amended)
The iroh-proxy POC (/workspace/iroh-proxy-poc, 5/5 runs clean) settled OQ-67: iroh does NOT expose a socket-injection hook for the IP/direct transport (noq_endpoint() is pub(crate), the IP transport binds its own netwatch::UdpSocket, CustomTransport operates on a separate CustomAddr address space iroh's hole-punching doesn't route through). The quinn POC's Socks5UdpSocket does not transfer to iroh. The decision: force relay-only when a proxy is configured, via three stable public iroh Builder knobs — clear_ip_transports() + addr_filter(relay_only) + proxy_url. The peer sees the relay's IP; the relay sees the proxy's IP; the client's real IP is hidden on both surfaces. No iroh fork required. Because iroh's proxy_url expects an HTTP CONNECT proxy (not SOCKS5), the integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines) so a single Socks5ProxyConfig covers all three dials uniformly: UDP ASSOCIATE for dial_quic, CONNECT for dial_tcp_tls, force-relay-only + HTTP-to-SOCKS5 bridge for dial_iroh. The POC also corrected a factual error: iroh's proxy_url proxies the relay WebSocket only, not pkarr/DoH (those use pkarr/hickory-resolver directly). Acceptable for the force-relay-only config (QAD disabled); spec text corrected. Force relay-only forgoes iroh's direct-path latency advantage (negligible for the hub deployment, which runs its own relay) and makes relay availability a hard dependency — the intended privacy/availability tradeoff; a caller that prefers availability over privacy for the iroh path simply does not set the proxy. - ADR-090 §5 amended: iroh force-relay-only decision + proxy_url coverage correction + HTTP-to-SOCKS5 bridge - OQ-67: resolved (force relay-only) - client README: iroh proxy row, bridge, limitations, ADR/OQ entries - README/open-questions: OQ-67 resolved, Current State amendment
This commit is contained in:
1 parent
f46482253b
commit
34e3be2801
5 files changed
+351
-167
No files matched your search
@@ -3,94 +3,105 @@
|
||||
- **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
|
||||
- **Status**: resolved (by the iroh-proxy POC and ADR-090 §5 amendment,
|
||||
2026-07-16)
|
||||
- **Door type**: one-way (the force-relay-only decision is a deployment
|
||||
tradeoff: relay-bounded availability, no direct fallback)
|
||||
- **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),
|
||||
- **Impacts** (now closed): A client that dials over iroh direct
|
||||
(hole-punched) connections and wants to hide its real IP from the
|
||||
peer. Did 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). Did NOT block the
|
||||
relay-mediated iroh path. The gap was the iroh *direct* path: when
|
||||
iroh hole-punches a direct QUIC connection, the peer sees the client's
|
||||
real IP. **Resolved by eliminating the direct path when a proxy is
|
||||
configured** — force relay-only.
|
||||
- **Investigation** (completed): Worked through the iroh socket stack
|
||||
to determine whether iroh exposes a socket abstraction analogous to
|
||||
quinn's `AsyncUdpSocket` + `new_with_abstract_socket`. **Result: it
|
||||
does not.** iroh uses a quinn fork (`noq`) that *has*
|
||||
`new_with_abstract_socket`, but iroh calls it internally with its
|
||||
own `Transport` multiplexer and keeps `noq_endpoint()` as
|
||||
`pub(crate)`. The IP/direct transport binds its own
|
||||
`netwatch::UdpSocket` with no injection point. The only public
|
||||
socket-injection surface (`unstable-custom-transports`,
|
||||
`CustomTransport`) operates on a separate `CustomAddr` address space
|
||||
that iroh's hole-punching does not route through — wrong shape. The
|
||||
quinn POC's `Socks5UdpSocket` does not transfer to iroh. Making
|
||||
SOCKS5-UDP-over-iroh-direct work would require forking iroh.
|
||||
- **What is decided (ADR-090 §5, amended 2026-07-16)**: `dial_iroh`
|
||||
with a proxy configured forces **relay-only** via three stable public
|
||||
iroh Builder knobs: `clear_ip_transports()` (no IP/direct transport
|
||||
is bound), `addr_filter(AddrFilter::relay_only())` (no direct IPs
|
||||
published), and `proxy_url(http://...)` (relay WebSocket tunneled
|
||||
through an HTTP CONNECT proxy). The peer sees the relay's IP; the
|
||||
relay sees the proxy's IP; the client's real IP is hidden on both
|
||||
surfaces. The POC (`/workspace/iroh-proxy-poc`,
|
||||
`docs/research/iroh-proxy-poc/findings.md`) validates this
|
||||
end-to-end: `selected_is_relay=true`, `selected_is_ip=false`,
|
||||
`any_direct_ip_path=false`, 45-byte echo through the proxied relay,
|
||||
5/5 runs clean. No iroh fork required — all three knobs are
|
||||
unconditional public APIs.
|
||||
- **What is open**: the direct-path SOCKS5-UDP option (Option 1) is
|
||||
**not pursued**. It requires forking iroh (no public IP-transport
|
||||
socket-injection hook; `CustomTransport` is the wrong address space),
|
||||
for a use case that is currently hypothetical (direct-path peer-IP
|
||||
privacy with direct-path latency). The quinn POC's `Socks5UdpSocket`
|
||||
remains the reference shape *if* iroh ever exposes an IP-transport
|
||||
socket hook upstream — tracked informally, not as an OQ (no concrete
|
||||
use case). The `proxy_url` coverage gap on pkarr/DoH (see below) is a
|
||||
separate gap, not this OQ's direct-path question.
|
||||
- **Correction to this OQ's original premises**: the original
|
||||
"What is decided" section stated iroh's `proxy_url` "proxies iroh's
|
||||
HTTP(S) traffic (relay connections, DNS-over-HTTPS, pkarr
|
||||
publishing)." Tracing iroh 1.0.2, this is **only partially true**:
|
||||
`proxy_url` flows only to the relay transport actor and is used only
|
||||
for the relay's WebSocket connection (an HTTP CONNECT handshake). It
|
||||
does **not** flow to the pkarr publisher (uses the `pkarr` crate
|
||||
directly, not reqwest), nor to the DNS-over-HTTPS resolver (uses
|
||||
`hickory-resolver` directly), nor to the `reqwest` client builder
|
||||
(`util.rs:80-91` has no `.proxy()` call). `proxy_url` covers the
|
||||
**relay WebSocket** exposure surface only. For alknet's privacy
|
||||
model this is fine because the recommended configuration is force
|
||||
relay-only: with `clear_ip_transports()`, QAD (QUIC address
|
||||
discovery) is disabled, and peer-IP exposure is fully handled by the
|
||||
relay path. If a deployment needs pkarr/DoH proxied too, that is a
|
||||
separate gap (a small upstream contribution to wire reqwest's
|
||||
`.proxy()` into iroh's `util::reqwest_client_builder`), not this OQ.
|
||||
- **The HTTP-to-SOCKS5 bridge**: iroh's `proxy_url` expects an HTTP
|
||||
CONNECT proxy, but `Socks5ProxyConfig` (ADR-090) is SOCKS5. The
|
||||
integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines, the
|
||||
POC's `src/proxy.rs` is the template) in the alknet client process
|
||||
when `Socks5ProxyConfig` is set and the iroh path is used. iroh's
|
||||
`proxy_url` is pointed at `http://127.0.0.1:<local-port>`. This
|
||||
unifies on a single `Socks5ProxyConfig` and is invisible to the
|
||||
operator. Behind the `socks5` feature flag.
|
||||
- **What is given up**: force relay-only forgoes iroh's
|
||||
direct-connection latency advantage (the relay adds one network hop).
|
||||
For the hub deployment (hub runs its own relay), this is negligible.
|
||||
Relay availability becomes a hard dependency — with
|
||||
`clear_ip_transports()`, if the relay is down, the client cannot
|
||||
connect at all. This is the intended privacy/availability tradeoff; a
|
||||
caller that prefers availability over privacy for the iroh path
|
||||
simply does not set the proxy.
|
||||
- **Resolution**: **Force relay-only (Option 2) is the default.** Validated
|
||||
by the iroh-proxy POC, uses only stable public iroh APIs, requires no
|
||||
fork, and fully closes the peer-IP-exposure gap (by eliminating the
|
||||
direct path when a proxy is configured). Option 1 (SOCKS5 UDP
|
||||
ASSOCIATE over iroh's direct path) is not feasible without forking
|
||||
iroh and is not pursued (no concrete use case justifies the fork
|
||||
maintenance burden). The `Socks5ProxyConfig` covers all three dials
|
||||
uniformly: UDP ASSOCIATE for `dial_quic`, CONNECT for `dial_tcp_tls`,
|
||||
and force-relay-only + HTTP-to-SOCKS5 bridge for `dial_iroh`. See
|
||||
ADR-090 §5 (amended 2026-07-16).
|
||||
- **Cross-references**: ADR-090 (§5 — resolves this OQ with the
|
||||
force-relay-only decision + HTTP-to-SOCKS5 bridge), 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).
|
||||
(the quinn-over-SOCKS5 PoC — the reference shape Option 1 would need;
|
||||
does not transfer to iroh),
|
||||
[`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
|
||||
(the iroh-proxy POC findings — the investigation that resolved this
|
||||
OQ).
|
||||
Reference in new issue
Block a user