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:
glm-5.2 committed 2026-07-16 09:18:56 +00:00
1 parent f46482253b
commit 34e3be2801
5 files changed
+351 -167

No files matched your search

+35 -6
View File
@@ -25,14 +25,43 @@ and the planned `alknet-socks5` channels data-channel handler (ADR-085
scope table, a service one side offers the other) — compose without scope table, a service one side offers the other) — compose without
coupling (a client using the hub's `alknet/socks5` service tunnels it coupling (a client using the hub's `alknet/socks5` service tunnels it
locally and points its `Socks5ProxyConfig` at the local tunnel end). locally and points its `Socks5ProxyConfig` at the local tunnel end).
iroh is the exception: `dial_iroh` does not consume iroh was the exception: `dial_iroh` did not consume
`Socks5ProxyConfig` — iroh's `proxy_url` covers the relay-exposure `Socks5ProxyConfig` — iroh's `proxy_url` covers the relay-exposure
surface, but the direct-connection peer-exposure case is surface, but the direct-connection peer-exposure case was
[OQ-67](open-questions.md) (deferred(unclear) — the pieces exist but [OQ-67](open-questions.md) (deferred(unclear) — the pieces existed but
the iroh socket-stack composition isn't clear; does not block the the iroh socket-stack composition wasn't clear; did not block the
first hub deployment, which uses QUIC/TCP+TLS). See first hub deployment, which uses QUIC/TCP+TLS). See
[ADR-090](decisions/090-client-dial-socks5-proxy-seam.md). [ADR-090](decisions/090-client-dial-socks5-proxy-seam.md).
**iroh proxy resolved — force relay-only + HTTP-to-SOCKS5 bridge (ADR-090
§5 amended, OQ-67 resolved, 2026-07-16).** The iroh-proxy POC
(`/workspace/iroh-proxy-poc`,
[`docs/research/iroh-proxy-poc/findings.md`](../research/iroh-proxy-poc/findings.md),
5/5 runs clean) settled OQ-67: iroh does **not** expose a
socket-injection hook for the IP/direct transport (the quinn POC's
`Socks5UdpSocket` does not transfer — `noq_endpoint()` is `pub(crate)`,
the IP transport binds its own `netwatch::UdpSocket`, and
`CustomTransport` operates on a separate `CustomAddr` address space
iroh's hole-punching doesn't route through). The decision is **force
relay-only** when a proxy is configured: three stable public iroh
Builder knobs (`clear_ip_transports()` + `addr_filter(relay_only)` +
`proxy_url`) eliminate the direct path and tunnel the relay WebSocket
through the 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. 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. 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), but the spec text in ADR-090
§5 is 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. See
[ADR-090](decisions/090-client-dial-socks5-proxy-seam.md) §5.
**Workspace scope corrected (ADR-085, 2026-07-15).** The overview's **Workspace scope corrected (ADR-085, 2026-07-15).** The overview's
crate graph had been describing the wrong scope since ADR-003 — a flat crate graph had been describing the wrong scope since ADR-003 — a flat
~12-crate workspace including DNS, messaging, and NAPI, while omitting ~12-crate workspace including DNS, messaging, and NAPI, while omitting
@@ -213,7 +242,7 @@ adapter location map is now consistent: all HTTP-backed adapters
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior | | [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery | | [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
| [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) | | [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
| [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS; iroh deferred OQ-67); produces `Connection` for `CallClient`/`ChannelClient` take-over; `alknet/register` named (wire protocol deferred, OQ-66) | | [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `alknet/register` named (wire protocol deferred, OQ-66) |
| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; handler crates no longer transitively link quinn/iroh | | [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; handler crates no longer transitively link quinn/iroh |
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream | | [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates | | [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
@@ -316,7 +345,7 @@ adapter location map is now consistent: all HTTP-backed adapters
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted | | [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted |
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted | | [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted |
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55) | | [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55) |
| [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (raises OQ-67 for iroh) | | [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) |
## Open Questions ## Open Questions
+46 -18
View File
@@ -14,8 +14,9 @@ take-overs (`CallClient::spawn_dispatch`,
client; it does not run protocols, manage peer lifecycle, or supervise client; it does not run protocols, manage peer lifecycle, or supervise
reconnection. It dials and produces a `Connection`. A client that wants reconnection. It dials and produces a `Connection`. A client that wants
to hide its real IP from the hub configures a SOCKS5 proxy to hide its real IP from the hub configures a SOCKS5 proxy
(`with_socks5_proxy`, ADR-090) — the rustls dials route through it (`with_socks5_proxy`, ADR-090) — all three dials route through it
transparently. (UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only +
HTTP-to-SOCKS5 bridge for iroh).
## What ## What
@@ -61,9 +62,10 @@ Narrowed to the **native case**: dialing native endpoint types (QUIC +
TCP+TLS, both rustls-consuming via `TlsClientConfig`; iroh as the TCP+TLS, both rustls-consuming via `TlsClientConfig`; iroh as the
key-not-config exception) over the native ALPNs (`alknet/register`, key-not-config exception) over the native ALPNs (`alknet/register`,
`alknet/call`, `alknet/channels`). With an optional SOCKS5 proxy `alknet/call`, `alknet/channels`). With an optional SOCKS5 proxy
(ADR-090), the rustls dials route their transport through the proxy (ADR-090), the dials route their transport through the proxy — UDP
(UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS) to hide the client's ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only +
real IP from the hub. HTTP-to-SOCKS5 bridge for iroh — to hide the client's real IP from
the hub.
### What `AlknetClient` is NOT ### What `AlknetClient` is NOT
@@ -109,7 +111,8 @@ pub struct AlknetClient {
iroh: Option<iroh::Endpoint>, iroh: Option<iroh::Endpoint>,
// When set, `dial_quic` and `dial_tcp_tls` route through this // When set, `dial_quic` and `dial_tcp_tls` route through this
// SOCKS5 proxy (UDP ASSOCIATE / CONNECT respectively). `dial_iroh` // SOCKS5 proxy (UDP ASSOCIATE / CONNECT respectively). `dial_iroh`
// is the exception — see OQ-67. Feature-gated on `socks5`. // forces relay-only via an HTTP-to-SOCKS5 bridge — see ADR-090 §5.
// Feature-gated on `socks5`.
#[cfg(feature = "socks5")] #[cfg(feature = "socks5")]
socks5: Option<Socks5ProxyConfig>, socks5: Option<Socks5ProxyConfig>,
} }
@@ -128,8 +131,9 @@ impl AlknetClient {
/// Set the SOCKS5 proxy for all subsequent dials. When set, every /// Set the SOCKS5 proxy for all subsequent dials. When set, every
/// dial routes its transport through this proxy: UDP ASSOCIATE for /// dial routes its transport through this proxy: UDP ASSOCIATE for
/// `dial_quic`, CONNECT for `dial_tcp_tls`. `dial_iroh` is the /// `dial_quic`, CONNECT for `dial_tcp_tls`, and force-relay-only +
/// exception — see OQ-67. Feature-gated on `socks5`. /// HTTP-to-SOCKS5 bridge for `dial_iroh` (ADR-090 §5). Feature-gated
/// on `socks5`.
#[cfg(feature = "socks5")] #[cfg(feature = "socks5")]
pub fn with_socks5_proxy(mut self, proxy: Socks5ProxyConfig) -> Self; pub fn with_socks5_proxy(mut self, proxy: Socks5ProxyConfig) -> Self;
} }
@@ -237,7 +241,7 @@ full rationale, the PoC grounding, and the limitations.
|------|----------------|-----------|--------------| |------|----------------|-----------|--------------|
| `dial_quic` | UDP ASSOCIATE (RFC 1928 §6) | `Socks5UdpSocket` (the crate's `quinn::AsyncUdpSocket` impl, behind `socks5`) → `new_with_abstract_socket` — see ADR-090 §3 | The hub (peer) | | `dial_quic` | UDP ASSOCIATE (RFC 1928 §6) | `Socks5UdpSocket` (the crate's `quinn::AsyncUdpSocket` impl, behind `socks5`) → `new_with_abstract_socket` — see ADR-090 §3 | The hub (peer) |
| `dial_tcp_tls` | CONNECT (RFC 1928 §3) | `TcpStream` to the proxy + SOCKS5 CONNECT handshake, then `TlsConnector` over the proxied stream | The hub (peer) | | `dial_tcp_tls` | CONNECT (RFC 1928 §3) | `TcpStream` to the proxy + SOCKS5 CONNECT handshake, then `TlsConnector` over the proxied stream | The hub (peer) |
| `dial_iroh` | (exception — OQ-67) | iroh's own `proxy_url` (set at the assembly layer) covers the relay-exposure surface; direct-connection peer exposure is deferred | The relay (via `proxy_url`); peer exposure on direct connections is OQ-67 | | `dial_iroh` | force relay-only (HTTP CONNECT, bridged) | `clear_ip_transports()` + `addr_filter(relay_only)` + `proxy_url` (HTTP CONNECT via a local HTTP-to-SOCKS5 bridge) — see ADR-090 §5 | The peer (via relay) + the relay (via the bridge → SOCKS5 proxy) |
The proxy config: The proxy config:
@@ -280,6 +284,25 @@ that don't use a proxy pay nothing.
deployment requirement — `ssh -D` does not work; a UDP-capable SOCKS5 deployment requirement — `ssh -D` does not work; a UDP-capable SOCKS5
daemon (dante, fast-socks5-based, etc.) is needed. The dial surfaces daemon (dante, fast-socks5-based, etc.) is needed. The dial surfaces
a clear error when the proxy lacks UDP support. a clear error when the proxy lacks UDP support.
- **`dial_iroh` forces relay-only, forgoing direct-path latency.**
iroh does not expose a socket-injection hook for the IP/direct
transport (the quinn POC's `Socks5UdpSocket` does not transfer —
see ADR-090 §5). With a proxy configured, the iroh endpoint is built
with `clear_ip_transports()` + `addr_filter(relay_only)` +
`proxy_url`, eliminating the direct path. The peer sees the relay's
IP; the relay sees the proxy's IP (via the local HTTP-to-SOCKS5
bridge). Relay availability becomes a hard dependency — 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.
- **`proxy_url` covers the relay WebSocket only, not pkarr/DoH.** iroh's
`proxy_url` proxies the relay WebSocket (HTTP CONNECT), not pkarr
publishing or DNS-over-HTTPS (those use `pkarr`/`hickory-resolver`
directly, not reqwest with a proxy). For the recommended
force-relay-only configuration this is acceptable (QAD is disabled,
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), not this ADR's scope.
- **No silent fallback.** When a proxy is configured and the proxy - **No silent fallback.** When a proxy is configured and the proxy
rejects the command, the dial returns `ClientDialError::Proxy`. The rejects the command, the dial returns `ClientDialError::Proxy`. The
dial does not silently fall back to a direct connection — that would dial does not silently fall back to a direct connection — that would
@@ -615,7 +638,7 @@ All design decisions are documented as ADRs in
| ADR | Decision | Summary | | ADR | Decision | Summary |
|-----|----------|---------| |-----|----------|---------|
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred | | [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred |
| [090](../../decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | `AlknetClient` gains `with_socks5_proxy`; `dial_quic` routes via UDP ASSOCIATE, `dial_tcp_tls` via CONNECT; iroh deferred (OQ-67); grounded in the quinn-proxy PoC | | [090](../../decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | `AlknetClient` gains `with_socks5_proxy`; `dial_quic` routes via UDP ASSOCIATE, `dial_tcp_tls` via CONNECT, `dial_iroh` forces relay-only via an HTTP-to-SOCKS5 bridge; OQ-67 resolved; grounded in the quinn-proxy + iroh-proxy PoCs |
## Open Questions ## Open Questions
@@ -634,14 +657,16 @@ See [open-questions.md](../../open-questions.md) for full details.
crate's ACL and OQ-58 (the token model is the shared blocker) and crate's ACL and OQ-58 (the token model is the shared blocker) and
needs a dedicated ADR. The HTTP registration endpoint (OQ-58) needs a dedicated ADR. The HTTP registration endpoint (OQ-58)
remains the first implementation. remains the first implementation.
- **OQ-67** (deferred(unclear)): iroh proxy support — `dial_iroh` does - **OQ-67** (resolved by ADR-090 §5 amendment): iroh proxy support —
not consume `Socks5ProxyConfig` (ADR-090). iroh's `proxy_url` (set at `dial_iroh` with a proxy configured forces relay-only via three
the assembly layer) covers the relay-exposure surface; the open case stable public iroh Builder knobs (`clear_ip_transports()` +
is iroh's *direct* (hole-punched) connection, where the peer sees the `addr_filter(relay_only)` + `proxy_url`), with a local
client's real IP. Wrapping iroh's QUIC in SOCKS5 UDP ASSOCIATE is a HTTP-to-SOCKS5 bridge adapting the SOCKS5 proxy to iroh's HTTP
different integration than the quinn POC (iroh has its own socket CONNECT expectation. iroh does not expose a socket-injection hook
stack, not `quinn::AsyncUdpSocket`). Does not block the first hub for the IP/direct transport (the quinn POC's `Socks5UdpSocket` does
deployment (hub outbound dials use QUIC/TCP+TLS). See not transfer), so force-relay-only is the conservative default — no
fork, fully closes the peer-IP-exposure gap by eliminating the
direct path. Grounded in the iroh-proxy POC. See
[OQ-67](../../questions/067-iroh-proxy-support.md). [OQ-67](../../questions/067-iroh-proxy-support.md).
## References ## References
@@ -673,6 +698,9 @@ See [open-questions.md](../../open-questions.md) for full details.
— `CallClient::spawn_dispatch` (the take-over the dial feeds) — `CallClient::spawn_dispatch` (the take-over the dial feeds)
- [`docs/research/quinn-quic-proxy/findings.md`](../../research/quinn-quic-proxy/findings.md) - [`docs/research/quinn-quic-proxy/findings.md`](../../research/quinn-quic-proxy/findings.md)
— the quinn-over-SOCKS5 PoC findings (grounds ADR-090's QUIC path) — the quinn-over-SOCKS5 PoC findings (grounds ADR-090's QUIC path)
- [`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
— the iroh-proxy PoC findings (grounds ADR-090 §5's iroh
force-relay-only decision + HTTP-to-SOCKS5 bridge)
- [`crates/endpoint/README.md`](../endpoint/README.md) — `AlknetEndpoint` - [`crates/endpoint/README.md`](../endpoint/README.md) — `AlknetEndpoint`
(the server-side complement) (the server-side complement)
- [`crates/tls/README.md`](../tls/README.md) — `TlsClientConfig` - [`crates/tls/README.md`](../tls/README.md) — `TlsClientConfig`
@@ -2,7 +2,9 @@
## Status ## Status
Accepted (adds a capability to ADR-089's `AlknetClient`; raises OQ-67 for iroh) Accepted (adds a capability to ADR-089's `AlknetClient`; §5 amended
2026-07-16 — OQ-67 resolved: iroh proxy support decided as force
relay-only + HTTP-to-SOCKS5 bridge, grounded in the iroh-proxy POC)
## Context ## Context
@@ -149,8 +151,8 @@ builder pattern:
impl AlknetClient { impl AlknetClient {
/// Set the SOCKS5 proxy for all subsequent dials. When set, every /// Set the SOCKS5 proxy for all subsequent dials. When set, every
/// dial routes its transport through this proxy: UDP ASSOCIATE for /// dial routes its transport through this proxy: UDP ASSOCIATE for
/// `dial_quic`, CONNECT for `dial_tcp_tls`. `dial_iroh` is the /// `dial_quic`, CONNECT for `dial_tcp_tls`, and force-relay-only +
/// exception — see OQ-67. /// HTTP-to-SOCKS5 bridge for `dial_iroh` (§5).
pub fn with_socks5_proxy(mut self, proxy: Socks5ProxyConfig) -> Self; pub fn with_socks5_proxy(mut self, proxy: Socks5ProxyConfig) -> Self;
} }
``` ```
@@ -237,31 +239,127 @@ TCP-native. The `TlsClientConfig`, the SNI, the ALPN, and the
Without a proxy, `dial_tcp_tls` is unchanged — `TcpStream::connect(addr)` Without a proxy, `dial_tcp_tls` is unchanged — `TcpStream::connect(addr)`
then `TlsConnector::connect`. Same zero-cost default as `dial_quic`. then `TlsConnector::connect`. Same zero-cost default as `dial_quic`.
### 5. `dial_iroh` is the exception — deferred (OQ-67) ### 5. `dial_iroh` — force relay-only via iroh's `proxy_url` (OQ-67 resolved)
iroh's `proxy_url` (set at `iroh::Endpoint` construction) proxies iroh's > **Amendment 2026-07-16** — OQ-67 resolved by the iroh-proxy POC
HTTP(S) traffic — relay connections, DNS-over-HTTPS, pkarr publishing — > (`/workspace/iroh-proxy-poc`,
through the proxy. This hides the client's IP from iroh's *relay*, which > [`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)).
is a distinct privacy goal from hiding it from the *peer*. iroh's relay > The original §5 deferred the iroh direct-path to OQ-67. This
already hides the client's IP from the peer (the peer sees the relay's > amendment replaces that deferral with the decision: **force
IP, by design), so `proxy_url` covers both exposure surfaces for the > relay-only when a proxy is configured**, via three stable public
relay-mediated path without touching iroh's QUIC socket. > iroh Builder knobs. It also corrects a factual error about what
> iroh's `proxy_url` covers (relay WebSocket only, not pkarr/DoH).
The open case is iroh's *direct* (hole-punched) connection: if iroh When a proxy is configured, `dial_iroh` forces the iroh endpoint to
establishes a direct QUIC connection to the peer, the peer sees the relay-only mode, eliminating the direct (hole-punched) path entirely
client's real IP. Wrapping iroh's QUIC in SOCKS5 UDP ASSOCIATE is a and tunneling the relay WebSocket through the proxy. The peer sees
*different* integration than the quinn POC — iroh has its own socket the relay's IP; the relay sees the proxy's IP; the client's real IP is
stack, not `quinn::AsyncUdpSocket` — and whether to force relay-only hidden on both surfaces.
when a proxy is configured vs. wrap iroh's QUIC is an open question. See
[OQ-67](../questions/067-iroh-proxy-support.md).
For now, `dial_iroh` does not consume `Socks5ProxyConfig`. The iroh The iroh endpoint is built by the assembly layer (ADR-089 §3 — iroh
endpoint is built by the assembly layer (ADR-089 §3 — iroh shares the shares the key, not the config). When `Socks5ProxyConfig` is set, the
key, not the config); if the assembly layer sets iroh's `proxy_url`, the assembly applies three stable public iroh Builder knobs together:
relay-exposure surface is covered. The peer-exposure surface for iroh
direct connections is deferred to OQ-67. This does not block the first 1. **`clear_ip_transports()`** — no IP/direct transport is bound. There
hub deployment — the hub's outbound worker dials (the hub-as-client case, is no kernel UDP socket from which a direct path could start.
ADR-087 §5) use QUIC or TCP+TLS, both of which honor the proxy. 2. **`addr_filter(AddrFilter::relay_only())`** — the endpoint's
published addresses contain only relay URLs, no direct IPs, so the
peer cannot attempt a direct connection.
3. **`proxy_url(http://...)`** — the relay's WebSocket is tunneled
through an HTTP CONNECT proxy. The relay sees the proxy's IP.
All three are unconditional (no `unstable-*` feature flags). The POC
(`docs/research/iroh-proxy-poc/findings.md`) validates the composition
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. See
[`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
§5.
**Why force relay-only, not wrap iroh's QUIC in SOCKS5 UDP ASSOCIATE.**
iroh uses a quinn fork (`noq`) that *has*
`Endpoint::new_with_abstract_socket` (the hook the quinn POC uses), but
iroh calls it internally with its own `Transport` multiplexer and keeps
the accessor `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, for a use
case that is currently hypothetical (direct-path peer-IP privacy with
direct-path latency). Force relay-only uses only stable public APIs,
requires no fork, and fully closes the peer-IP-exposure gap (by
eliminating the direct path when a proxy is configured). The quinn
POC's `Socks5UdpSocket` remains the reference shape *if* iroh ever
exposes an IP-transport socket hook upstream. See
[`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
§2–§3.
**Correction: what `proxy_url` actually covers.** The original §5
stated `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, `iroh-relay-1.0.2/src/client/tls.rs:127-216`). 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). So `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 the peer-IP exposure is fully handled by
the relay path. The pkarr/DoH surfaces are not proxied by `proxy_url`;
if a deployment needs those proxied too, that is a separate gap (a
small upstream contribution to wire reqwest's `.proxy()` into
`util::reqwest_client_builder`), not this ADR's scope. See
[`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
§4.
**The proxy-protocol mismatch: HTTP-to-SOCKS5 bridge.** There is one
integration wrinkle. `Socks5ProxyConfig` (this ADR) is a SOCKS5 proxy.
iroh's `proxy_url` expects an **HTTP CONNECT** proxy
(`iroh-relay-1.0.2/src/client/tls.rs:173` — `Method::CONNECT`, checks
`scheme() == "http"`). They are different protocols:
- `dial_quic` uses SOCKS5 UDP ASSOCIATE (UDP/QUIC).
- `dial_tcp_tls` uses SOCKS5 CONNECT (TCP).
- iroh's relay path uses HTTP CONNECT (the relay WebSocket).
A single `Socks5ProxyConfig` cannot be fed verbatim into iroh's
`proxy_url`. The integration runs a tiny local **HTTP-to-SOCKS5 bridge**
in the alknet client process when `Socks5ProxyConfig` is set and the
iroh path is used: a local HTTP CONNECT server (~80 lines, the POC's
`src/proxy.rs` is the template) that forwards each CONNECT tunnel to
the SOCKS5 proxy's CONNECT command. iroh's `proxy_url` is then pointed
at `http://127.0.0.1:<local-port>`. This unifies on a single
`Socks5ProxyConfig` and is invisible to the operator — the operator
configures one SOCKS5 proxy, and the bridge adapts it to the HTTP
CONNECT protocol iroh's relay client expects. The bridge is local-only
(no external surface), behind the `socks5` feature flag, and adds no
new deps beyond what the quinn PoC already uses (`fast-socks5`).
Without a proxy, `dial_iroh` is unchanged — the iroh endpoint is built
with direct + relay as before. The proxy is a strict addition; the
no-proxy path is the zero-cost default (same as `dial_quic` and
`dial_tcp_tls`).
**What is given up.** Force relay-only forgoes iroh's direct-connection
latency advantage (the relay adds one network hop). For the hub
deployment (ADR-087, where the 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 — there is no direct fallback. This is the intended
privacy/availability tradeoff: privacy is absolute (no IP leak),
availability is relay-bounded. A caller that prefers availability over
privacy for the iroh path simply does not set the proxy.
This 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 directly (no bridge needed). The iroh
path is one of three dials, and the force-relay-only resolution is the
conservative default that closes the peer-IP-exposure gap with no fork.
### 6. Fallback policy is a caller concern, not a dial decision ### 6. Fallback policy is a caller concern, not a dial decision
@@ -399,12 +497,24 @@ SOCKS5 protocol implementation.
**Negative:** **Negative:**
- **iroh direct connections are not proxied by this ADR.** `dial_iroh` - **iroh direct connections forgo direct-path latency when a proxy is
does not consume `Socks5ProxyConfig`; iroh's `proxy_url` (set at the configured.** `dial_iroh` with a proxy forces relay-only (§5),
assembly layer) covers the relay-exposure surface, but a direct eliminating the direct path. The relay adds one network hop; for the
iroh connection exposes the client's real IP to the peer. This is hub deployment (hub runs its own relay), this is negligible. Relay
OQ-67. It does not block the first hub deployment (hub outbound availability becomes a hard dependency — with `clear_ip_transports()`,
dials use QUIC/TCP+TLS), but it is a gap for iroh-direct privacy. 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. Does not block the first hub deployment (hub outbound dials
use QUIC/TCP+TLS).
- **pkarr/DoH exposure surfaces are not covered by `proxy_url`.**
iroh's `proxy_url` proxies the relay WebSocket only, not pkarr
publishing or DNS-over-HTTPS (§5 correction). For the recommended
force-relay-only configuration this is acceptable (QAD is disabled,
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 ADR's scope.
- **The `socks5` feature and `fast-socks5` dep are new.** A new - **The `socks5` feature and `fast-socks5` dep are new.** A new
optional feature and a new optional dep. The cost is contained (off optional feature and a new optional dep. The cost is contained (off
by default, only pulled when a deployment uses a proxy), but it is by default, only pulled when a deployment uses a proxy), but it is
@@ -432,7 +542,9 @@ them after consumers exist is a rewrite. The `Proxy` variant on
`socks5` feature name and the `fast-socks5` dep choice are two-way `socks5` feature name and the `fast-socks5` dep choice are two-way
(the feature can be renamed pre-1.0; the dep can be vendored later). (the feature can be renamed pre-1.0; the dep can be vendored later).
The `Socks5UdpSocket` internal implementation is two-way. The iroh The `Socks5UdpSocket` internal implementation is two-way. The iroh
proxy question (OQ-67) is two-way until decided. The fallback policy force-relay-only decision (§5) is one-way (it's a deployment
tradeoff: relay-bounded availability, no direct fallback) — the
HTTP-to-SOCKS5 bridge implementation is two-way. The fallback policy
(caller's concern, not the dial's) is one-way — once the dial refuses (caller's concern, not the dial's) is one-way — once the dial refuses
to silently fall back, callers rely on that for their privacy to silently fall back, callers rely on that for their privacy
posture. posture.
@@ -473,5 +585,13 @@ posture.
- BotBrowser UDP-over-SOCKS5 - BotBrowser UDP-over-SOCKS5
(`deepwiki.com/botswin/BotBrowser/6.4-udp-over-socks5`) — production (`deepwiki.com/botswin/BotBrowser/6.4-udp-over-socks5`) — production
implementation of the identical pattern in Chromium's network stack. implementation of the identical pattern in Chromium's network stack.
- OQ-67 (raised by this ADR) — iroh proxy support (direct-connection - **iroh-proxy PoC:** `/workspace/iroh-proxy-poc` — run `cargo run -r`.
peer exposure). Load-bearing files: `src/main.rs` (Option 2 end-to-end), `src/proxy.rs`
(HTTP CONNECT proxy). 5/5 runs clean.
- **iroh-proxy research findings:**
[`docs/research/iroh-proxy-poc/findings.md`](../../research/iroh-proxy-poc/findings.md)
— the iroh socket-stack investigation (no public IP-transport
injection hook; `CustomTransport` is the wrong address space), the
`proxy_url` coverage correction (relay WebSocket only, not pkarr/DoH),
the three force-relay-only knobs, and the HTTP-to-SOCKS5 bridge.
- OQ-67 (resolved by this ADR's §5 amendment) — iroh proxy support.
+16 -20
View File
@@ -208,7 +208,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis
| OQ | Title | Status | Door | Pri | | OQ | Title | Status | Door | Pri |
|----|-------|--------|------|-----| |----|-------|--------|------|-----|
| [OQ-66](questions/066-alknet-register-wire-protocol.md) | `alknet/register` Wire Protocol | deferred(scope) | one | med | | [OQ-66](questions/066-alknet-register-wire-protocol.md) | `alknet/register` Wire Protocol | deferred(scope) | one | med |
| [OQ-67](questions/067-iroh-proxy-support.md) | iroh Proxy Support (Direct-Connection Peer Exposure) | deferred(unclear) | two | med | | [OQ-67](questions/067-iroh-proxy-support.md) | iroh Proxy Support (Direct-Connection Peer Exposure) | resolved | one | med |
## Deferred / Blocked ## Deferred / Blocked
@@ -306,25 +306,21 @@ filtering the tables above.
### OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure) ### OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure)
- **Investigation**: Work through the iroh socket stack to determine - **Resolved** (ADR-090 §5 amendment, 2026-07-16): `dial_iroh` with a
whether iroh exposes a socket abstraction analogous to quinn's proxy configured forces relay-only via three stable public iroh
`AsyncUdpSocket` + `new_with_abstract_socket` (the hook the quinn Builder knobs (`clear_ip_transports()` + `addr_filter(relay_only)`
POC uses). If it does, a `Socks5UdpSocket`-equivalent for iroh is + `proxy_url`), with a local HTTP-to-SOCKS5 bridge adapting the
the same shape as the quinn integration. If it does not, the SOCKS5 proxy to iroh's HTTP CONNECT expectation. iroh does not expose
alternatives are: (a) force relay-only when a proxy is configured a socket-injection hook for the IP/direct transport (the quinn
(the conservative default — the peer always sees the relay's IP, POC's `Socks5UdpSocket` does not transfer), so force-relay-only is
never the client's), or (b) accept the gap (document that iroh direct the conservative default — no fork, fully closes the peer-IP-exposure
connections expose the client's IP). The investigation needs a gap by eliminating the direct path. Grounded in the iroh-proxy POC
concrete iroh-direct-with-proxy use case to drive the choice — (`docs/research/iroh-proxy-poc/findings.md`, 5/5 runs clean). The
without one, "force relay-only" is the conservative default. `Socks5ProxyConfig` now covers all three dials uniformly: UDP
- **Priority**: medium ASSOCIATE for `dial_quic`, CONNECT for `dial_tcp_tls`,
- **Impacts**: A client that dials over iroh direct (hole-punched) force-relay-only + HTTP-to-SOCKS5 bridge for `dial_iroh`. See
connections and wants to hide its real IP from the peer. Does NOT [ADR-090](decisions/090-client-dial-socks5-proxy-seam.md) §5 and
block the first hub deployment — hub outbound dials use QUIC/TCP+TLS [OQ-67](questions/067-iroh-proxy-support.md).
(both honor the proxy, ADR-090). Does NOT block the relay-mediated
iroh path — iroh's relay hides the client's IP from the peer, and
iroh's `proxy_url` hides it from the relay. The gap is the iroh
*direct* path only.
- **Full file**: [OQ-67](questions/067-iroh-proxy-support.md) - **Full file**: [OQ-67](questions/067-iroh-proxy-support.md)
### OQ-56: Full Channel-Level Flow-Control Windowing ### OQ-56: Full Channel-Level Flow-Control Windowing
@@ -3,94 +3,105 @@
- **Origin**: `docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md` - **Origin**: `docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md`
§5; `docs/architecture/crates/client/README.md` §"SOCKS5 proxy §5; `docs/architecture/crates/client/README.md` §"SOCKS5 proxy
(ADR-090)". (ADR-090)".
- **Status**: deferred(unclear) - **Status**: resolved (by the iroh-proxy POC and ADR-090 §5 amendment,
- **Door type**: two-way 2026-07-16)
- **Door type**: one-way (the force-relay-only decision is a deployment
tradeoff: relay-bounded availability, no direct fallback)
- **Priority**: medium - **Priority**: medium
- **Impacts**: A client that dials over iroh direct (hole-punched) - **Impacts** (now closed): A client that dials over iroh direct
connections and wants to hide its real IP from the peer. Does NOT (hole-punched) connections and wants to hide its real IP from the
block the first hub deployment — the hub's outbound worker dials peer. Did NOT block the first hub deployment — the hub's outbound
(the hub-as-client case, ADR-087 §5) use QUIC or TCP+TLS, both of worker dials (the hub-as-client case, ADR-087 §5) use QUIC or TCP+TLS,
which honor the proxy (ADR-090). Does NOT block the relay-mediated both of which honor the proxy (ADR-090). Did NOT block the
iroh path — iroh's relay already hides the client's IP from the peer relay-mediated iroh path. The gap was the iroh *direct* path: when
(the peer sees the relay's IP), and iroh's `proxy_url` (set at the iroh hole-punches a direct QUIC connection, the peer sees the client's
assembly layer) hides the client's IP from the relay. The gap is the real IP. **Resolved by eliminating the direct path when a proxy is
iroh *direct* path: when iroh hole-punches a direct QUIC connection, configured** — force relay-only.
the peer sees the client's real IP, and `Socks5ProxyConfig` (ADR-090) - **Investigation** (completed): Worked through the iroh socket stack
does not cover it. to determine whether iroh exposes a socket abstraction analogous to
- **Investigation**: Work through the iroh socket stack to determine quinn's `AsyncUdpSocket` + `new_with_abstract_socket`. **Result: it
whether iroh exposes a socket abstraction analogous to quinn's does not.** iroh uses a quinn fork (`noq`) that *has*
`AsyncUdpSocket` + `new_with_abstract_socket` (the hook the quinn `new_with_abstract_socket`, but iroh calls it internally with its
POC uses, validated in `docs/research/quinn-quic-proxy/findings.md`). own `Transport` multiplexer and keeps `noq_endpoint()` as
If it does, a `Socks5UdpSocket`-equivalent for iroh is the same shape `pub(crate)`. The IP/direct transport binds its own
as the quinn integration (adapt the PoC). If it does not, the `netwatch::UdpSocket` with no injection point. The only public
alternatives are: (a) force relay-only when a proxy is configured socket-injection surface (`unstable-custom-transports`,
(disable iroh's direct-connection path — the peer always sees the `CustomTransport`) operates on a separate `CustomAddr` address space
relay's IP, never the client's), or (b) accept the gap (document that that iroh's hole-punching does not route through — wrong shape. The
iroh direct connections expose the client's IP, and privacy-conscious quinn POC's `Socks5UdpSocket` does not transfer to iroh. Making
iroh deployments use `proxy_url` + relay-only). The investigation SOCKS5-UDP-over-iroh-direct work would require forking iroh.
needs a concrete iroh-direct-with-proxy use case to drive the choice - **What is decided (ADR-090 §5, amended 2026-07-16)**: `dial_iroh`
— without one, "force relay-only" is the conservative default. with a proxy configured forces **relay-only** via three stable public
- **What is decided (ADR-090 §5)**: `dial_iroh` does **not** consume iroh Builder knobs: `clear_ip_transports()` (no IP/direct transport
`Socks5ProxyConfig`. iroh's `proxy_url` (set at `iroh::Endpoint` is bound), `addr_filter(AddrFilter::relay_only())` (no direct IPs
construction by the assembly layer — ADR-089 §3, "iroh shares the published), and `proxy_url(http://...)` (relay WebSocket tunneled
key, not the config") covers the relay-exposure surface: it proxies through an HTTP CONNECT proxy). The peer sees the relay's IP; the
iroh's HTTP(S) traffic (relay connections, DNS-over-HTTPS, pkarr relay sees the proxy's IP; the client's real IP is hidden on both
publishing) through the configured proxy, hiding the client's IP surfaces. The POC (`/workspace/iroh-proxy-poc`,
from iroh's relay. For the relay-mediated path, this covers both `docs/research/iroh-proxy-poc/findings.md`) validates this
exposure surfaces (peer sees relay's IP; relay sees proxy's IP). The end-to-end: `selected_is_relay=true`, `selected_is_ip=false`,
open case is the direct path. `any_direct_ip_path=false`, 45-byte echo through the proxied relay,
- **What is open**: how to cover iroh's *direct* (hole-punched) 5/5 runs clean. No iroh fork required — all three knobs are
connection path when a SOCKS5 proxy is configured. Three candidate unconditional public APIs.
approaches: - **What is open**: the direct-path SOCKS5-UDP option (Option 1) is
1. **Wrap iroh's QUIC in SOCKS5 UDP ASSOCIATE.** Analogous to the **not pursued**. It requires forking iroh (no public IP-transport
quinn POC — if iroh exposes a socket abstraction that accepts a socket-injection hook; `CustomTransport` is the wrong address space),
custom UDP impl, wrap it in a `Socks5UdpSocket`-equivalent. This for a use case that is currently hypothetical (direct-path peer-IP
is a *different* integration than the quinn POC (iroh has its own privacy with direct-path latency). The quinn POC's `Socks5UdpSocket`
socket stack, not `quinn::AsyncUdpSocket`), so the PoC does not remains the reference shape *if* iroh ever exposes an IP-transport
directly transfer. Requires investigating iroh's API surface for a socket hook upstream — tracked informally, not as an OQ (no concrete
socket-injection hook. use case). The `proxy_url` coverage gap on pkarr/DoH (see below) is a
2. **Force relay-only when a proxy is configured.** Disable iroh's separate gap, not this OQ's direct-path question.
direct-connection path when `Socks5ProxyConfig` is set (or when - **Correction to this OQ's original premises**: the original
the assembly layer sets iroh's `proxy_url`); all iroh connections "What is decided" section stated iroh's `proxy_url` "proxies iroh's
go through the relay, which hides the client's IP from the peer HTTP(S) traffic (relay connections, DNS-over-HTTPS, pkarr
by design. This is the conservative default — no new socket publishing)." Tracing iroh 1.0.2, this is **only partially true**:
integration, no ECN loss (the relay path is TCP-based), but it `proxy_url` flows only to the relay transport actor and is used only
gives up iroh's direct-connection latency advantage. for the relay's WebSocket connection (an HTTP CONNECT handshake). It
3. **Accept the gap.** Document that iroh direct connections expose does **not** flow to the pkarr publisher (uses the `pkarr` crate
the client's IP to the peer, and that privacy-conscious iroh directly, not reqwest), nor to the DNS-over-HTTPS resolver (uses
deployments should use `proxy_url` (relay-exposure) + force `hickory-resolver` directly), nor to the `reqwest` client builder
relay-only (peer-exposure). This is honest but leaves the (`util.rs:80-91` has no `.proxy()` call). `proxy_url` covers the
`Socks5ProxyConfig` partially effective for iroh. **relay WebSocket** exposure surface only. For alknet's privacy
The choice depends on whether a concrete iroh-direct-with-proxy use model this is fine because the recommended configuration is force
case exists that needs the peer-IP privacy AND the direct-connection relay-only: with `clear_ip_transports()`, QAD (QUIC address
latency. Without that use case, (2) is the conservative default; if discovery) is disabled, and peer-IP exposure is fully handled by the
the use case exists, (1) is the target (and needs the iroh socket relay path. If a deployment needs pkarr/DoH proxied too, that is a
investigation). separate gap (a small upstream contribution to wire reqwest's
- **Why deferred(unclear), not deferred(scope)**: The pieces exist — `.proxy()` into iroh's `util::reqwest_client_builder`), not this OQ.
SOCKS5 UDP ASSOCIATE is validated for quinn (the PoC), iroh's - **The HTTP-to-SOCKS5 bridge**: iroh's `proxy_url` expects an HTTP
`proxy_url` exists and covers the relay path, iroh's direct vs relay CONNECT proxy, but `Socks5ProxyConfig` (ADR-090) is SOCKS5. The
path is a known iroh property. What is unclear is the *composition* integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines, the
for iroh specifically: does iroh expose a socket hook (making (1) POC's `src/proxy.rs` is the template) in the alknet client process
feasible), and is there a concrete use case that needs direct-path when `Socks5ProxyConfig` is set and the iroh path is used. iroh's
privacy (making (1) worth building vs. (2) sufficient)? The `proxy_url` is pointed at `http://127.0.0.1:<local-port>`. This
resolution is *investigation* (work through iroh's socket stack, find unifies on a single `Socks5ProxyConfig` and is invisible to the
the use case), not *waiting* (for a spec or use case to arrive). The operator. Behind the `socks5` feature flag.
quinn POC settled the quinn case; the iroh case needs its own - **What is given up**: force relay-only forgoes iroh's
investigation, not a copy of the quinn answer. direct-connection latency advantage (the relay adds one network hop).
- **Resolution**: Not yet decidable. The iroh socket stack needs For the hub deployment (hub runs its own relay), this is negligible.
investigation (does it expose a `new_with_abstract_socket`-equivalent?), Relay availability becomes a hard dependency — with
and a concrete iroh-direct-with-proxy use case needs to surface `clear_ip_transports()`, if the relay is down, the client cannot
(or a deliberate decision to force relay-only needs to be made). The connect at all. This is the intended privacy/availability tradeoff; a
quinn POC's `Socks5UdpSocket` is the reference shape if iroh exposes caller that prefers availability over privacy for the iroh path
the hook; the "force relay-only" fallback is the conservative default simply does not set the proxy.
if it does not or if no use case demands direct-path privacy. This - **Resolution**: **Force relay-only (Option 2) is the default.** Validated
does not block the first hub deployment or the QUIC/TCP+TLS proxy by the iroh-proxy POC, uses only stable public iroh APIs, requires no
capability (ADR-090) — it is a gap specific to iroh direct fork, and fully closes the peer-IP-exposure gap (by eliminating the
connections. direct path when a proxy is configured). Option 1 (SOCKS5 UDP
- **Cross-references**: ADR-090 (§5 — defers iroh proxy support to ASSOCIATE over iroh's direct path) is not feasible without forking
this OQ), ADR-089 (§3 — iroh shares the key, not the config; iroh and is not pursued (no concrete use case justifies the fork
`proxy_url` is set at the assembly layer), ADR-087 (§3 — the iroh maintenance burden). The `Socks5ProxyConfig` covers all three dials
exception on the client side), 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) [`docs/research/quinn-quic-proxy/findings.md`](../../research/quinn-quic-proxy/findings.md)
(the quinn-over-SOCKS5 PoC — the reference shape for the iroh (the quinn-over-SOCKS5 PoC — the reference shape Option 1 would need;
investigation). 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).