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
6.6 KiB
6.6 KiB
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: 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 (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 hasnew_with_abstract_socket, but iroh calls it internally with its ownTransportmultiplexer and keepsnoq_endpoint()aspub(crate). The IP/direct transport binds its ownnetwatch::UdpSocketwith no injection point. The only public socket-injection surface (unstable-custom-transports,CustomTransport) operates on a separateCustomAddraddress space that iroh's hole-punching does not route through — wrong shape. The quinn POC'sSocks5UdpSocketdoes 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_irohwith 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), andproxy_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;
CustomTransportis the wrong address space), for a use case that is currently hypothetical (direct-path peer-IP privacy with direct-path latency). The quinn POC'sSocks5UdpSocketremains the reference shape if iroh ever exposes an IP-transport socket hook upstream — tracked informally, not as an OQ (no concrete use case). Theproxy_urlcoverage 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_urlflows 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 thepkarrcrate directly, not reqwest), nor to the DNS-over-HTTPS resolver (useshickory-resolverdirectly), nor to thereqwestclient builder (util.rs:80-91has no.proxy()call).proxy_urlcovers the relay WebSocket exposure surface only. For alknet's privacy model this is fine because the recommended configuration is force relay-only: withclear_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'sutil::reqwest_client_builder), not this OQ. - The HTTP-to-SOCKS5 bridge: iroh's
proxy_urlexpects an HTTP CONNECT proxy, butSocks5ProxyConfig(ADR-090) is SOCKS5. The integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines, the POC'ssrc/proxy.rsis the template) in the alknet client process whenSocks5ProxyConfigis set and the iroh path is used. iroh'sproxy_urlis pointed athttp://127.0.0.1:<local-port>. This unifies on a singleSocks5ProxyConfigand is invisible to the operator. Behind thesocks5feature 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
Socks5ProxyConfigcovers all three dials uniformly: UDP ASSOCIATE fordial_quic, CONNECT fordial_tcp_tls, and force-relay-only + HTTP-to-SOCKS5 bridge fordial_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_urlis set at the assembly layer), ADR-087 (§3 — the iroh exception on the client side),docs/research/quinn-quic-proxy/findings.md(the quinn-over-SOCKS5 PoC — the reference shape Option 1 would need; does not transfer to iroh),docs/research/iroh-proxy-poc/findings.md(the iroh-proxy POC findings — the investigation that resolved this OQ).