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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
Reference in new issue
Block a user