feat(marmot-app): optional SOCKS5 proxy for relay connections#783
feat(marmot-app): optional SOCKS5 proxy for relay connections#783tsgx1990 wants to merge 3 commits into
Conversation
…proxy MarmotAppConfig had no way to make the relay plane's nostr-sdk Client dial through a proxy: from_sdk built the Client with builder().build() and no opts, so on a censored network the relay WebSocket is torn down with no way to route around it. Add MarmotAppConfig::relay_connection (RelayConnectionMode: Direct | Socks5(SocketAddr), default Direct) + with_relay_connection(...), and thread it through the relay-plane constructors so from_sdk builds the Client with ClientOptions::connection(Connection::proxy(addr)) when set. The same Client backs the user-directory fetcher, so a proxy routes all relay traffic. The proxy socket is dialed by the nostr-sdk Client and is deliberately outside the relay-host safety chokepoint (relay_plane/safety.rs), which still validates the relay URL; the proxy is an explicit, user-configured egress akin to a VPN. cargo check -p marmot-app passes. SOCKS5 needs no cargo feature (tokio-socks is already in the graph via nostr-sdk); embedded arti-Tor is left for a follow-up behind the `tor` feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first commit proxied the shared relay plane but missed MarmotApp::relay_client_for_endpoints -- the per-account publish client that account setup / relay-list / KeyPackage publish uses. Left direct, `moyu init` still dialed relays directly and failed on a censored network even with the plane proxied. Route it through the same relay_plane::relay_client_options helper (extracted here) so every relay-facing client honors MarmotAppConfig::relay_connection. cargo check -p marmot-app passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ready to review this PR? Stage has broken it down into 4 individual chapters for you: Chapters generated by Stage for commit 4c611ef on Jul 7, 2026 5:01pm UTC. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughAdds a ChangesRelay Connection Mode Configuration
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant MarmotApp
participant MarmotRelayPlane
participant build_sdk_client
participant NostrSdkClient
MarmotApp->>MarmotRelayPlane: runtime_default_with_loopback(lookback, allow_loopback, &config.relay_connection)
MarmotRelayPlane->>build_sdk_client: build_sdk_client(connection)
build_sdk_client->>NostrSdkClient: apply relay_client_options(connection)
NostrSdkClient-->>MarmotRelayPlane: configured client
MarmotApp->>NostrSdkClient: relay_client_for_endpoints builds client with relay_client_options(&self.config.relay_connection)
Related issues: None specified. Related PRs: None specified. Suggested labels: enhancement, relay-plane, configuration Suggested reviewers: None specified. 🐰 A whisper through a SOCKS5 hole, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
Adds an optional SOCKS5 proxy for all relay connections, configured on
MarmotAppConfig:New public API:
RelayConnectionMode { Direct, Socks5(SocketAddr) }(inmarmot_app::config, re-exported at the crate root),#[default] Direct.MarmotAppConfig::relay_connectionfield +with_relay_connection(...)builder, mirroring the existingwith_allow_loopback_relay_endpointspattern.Why
On a censored network, a direct relay WebSocket is torn down before it can publish, and today there's no way to route relay traffic through a proxy —
MarmotAppConfighas no proxy knob and the relay client is built withNostrSdkClient::builder().build()(no opts). Pointing the client at a local Tor (or other SOCKS5) proxy is the standard way around this, andnostr-sdkalready supports it viaClientOptions::connection(Connection::proxy(addr)).How
The mode maps to
nostr-sdkclient options through a single shared helper,relay_plane::relay_client_options, applied at both places MDK builds a relay-facing client:relay_plane::from_sdk— the shared relay transport + user-directory fetcher.MarmotApp::relay_client_for_endpoints— the per-account publish client used by account setup / relay-list / KeyPackage publish.That second one is easy to miss: with only the plane proxied, account setup still dials directly and fails on a censored network. Routing both through one helper keeps them from drifting.
Notes
Direct— zero behavior change unless a host app opts in.Connection::proxyis SOCKS5 andtokio-socksis already in the graph vianostr-sdk. (Embedded arti-Tor,Connection::embedded_tor, is intentionally left for a follow-up behind atorfeature.)nostr-sdkclient and sits outside the relay-host safety chokepoint (relay_plane/safety.rs), which still validates the relay URL. The proxy is an explicit, user-configured egress (typically a loopback Tor/SOCKS port) analogous to a system VPN, so it is intentionally not run throughreject_non_public_ip. Happy to gate it differently if you'd prefer.Testing
cargo check -p marmot-apppasses;cargo fmtclean;cargo clippy -p marmot-appclean for the added code.wss://nos.lol), both directions decrypting, with the client pointed at a local SOCKS5 proxy on a network that tears down direct WebSockets — where the same round-trip fails without the proxy.Happy to adjust the API shape, naming, or split the commits differently.
Summary by CodeRabbit