10 KiB
Selective Tor Routing — .onion Only
Goal
sovereign_browser should make all address types "just work":
http:///https://→ direct (clearnet)file://→ direct (local).onion→ through Tor SOCKS proxy.fips→ through FIPS TUN interfacenostr://→ direct to relays (or through Tor if relay is.onion)
Tor and FIPS are enabled by default. Tor provides a SOCKS proxy that
is used only for .onion addresses. Regular internet traffic is
never routed through Tor.
This is NOT Tor Browser. It's a browser where .onion addresses just
work because Tor is running in the background.
Current State
The current implementation (in src/net_services.c) sets WebKit's proxy
to route all HTTP/HTTPS through Tor when Tor is enabled (fail-closed
mode). This is the wrong default for sovereign_browser's vision — it
would break clearnet access and isn't what we want.
Design
Traffic Routing
| URL pattern | Route | How |
|---|---|---|
http://example.com |
Direct | WebKit default networking, no proxy |
https://example.com |
Direct | WebKit default networking, no proxy |
file:///path |
Direct | Local file access |
http://something.onion |
Tor SOCKS | Custom scheme handler |
https://something.onion |
Tor SOCKS | Custom scheme handler |
http://npub1abc.fips/ |
FIPS TUN | DNS resolves to fd00::/8, TUN routes |
nostr://npub1... |
Direct to relays | nostr:// scheme handler |
sovereign://... |
Direct (internal) | sovereign:// scheme handler |
WebKit Proxy Configuration
Always WEBKIT_NETWORK_PROXY_MODE_NO_PROXY. WebKit never uses a
proxy for its default network stack. All proxying is done via custom
URI scheme handlers.
This is a change from the current code, which sets CUSTOM mode with
Tor's SOCKS endpoint. We remove that entirely.
.onion Handling
Approach: Intercept + custom scheme
-
on_decide_policyintab_manager.cintercepts navigation to.onionURLs (bothhttp://*.onionandhttps://*.onion). -
Rewrite to
tor://scheme: The intercepted URL is rewritten:http://abc.onion/path?q=1→tor://abc.onion/path?q=1https://abc.onion/path→tor://abc.onion/path- The scheme handler knows to use Tor's SOCKS proxy.
-
tor://URI scheme handler (newsrc/tor_scheme.c):- Registered via
webkit_web_context_register_uri_scheme(ctx, "tor", ...) - Parses the
.onionhostname, port, path, query from the URI - Fetches the content via Tor's SOCKS proxy using libcurl with
CURLOPT_PROXYset to Tor's SOCKS endpoint - Returns the response (headers + body) to WebKit via
webkit_uri_scheme_request_finish() - Supports both HTTP and HTTPS over .onion (Tor handles the TLS)
- Registered via
-
URL bar: When the user types
abc.onionin the URL bar,normalize_url()should recognize.onionand prependhttp://(orhttps://), then theon_decide_policyinterception handles the rest. -
Links on pages: When a web page has
<a href="http://abc.onion">, clicking it triggerson_decide_policy, which intercepts and rewrites totor://abc.onion.
Why a custom scheme handler instead of WebKit proxy?
WebKitGTK's proxy API (WebKitNetworkProxySettings) only supports
"proxy everything, ignore these hosts" — not "proxy only these hosts."
Setting a proxy for everything would route clearnet through Tor, which
we don't want. A custom scheme handler gives us precise control: only
.onion traffic goes through Tor.
The downside is that the custom scheme handler must handle HTTP semantics (GET, POST, headers, cookies, redirects) itself. However, libcurl handles all of this — we just pass the request to curl with the SOCKS proxy configured and return the response.
libcurl for .onion fetches
The tor:// scheme handler uses libcurl (already a dependency — used
in nostr_scheme.c for NIP-11 fetches) with:
curl_easy_setopt(curl, CURLOPT_URL, "http://abc.onion/path?q=1");
curl_easy_setopt(curl, CURLOPT_PROXY, "socks5h://127.0.0.1:9050");
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
socks5h:// means curl sends the hostname to the SOCKS proxy (Tor
resolves it — no DNS leak). CURLPROXY_SOCKS5_HOSTNAME ensures the
.onion hostname is resolved by Tor, not locally.
For HTTPS over .onion, curl handles TLS normally — Tor just provides the transport.
Request handling
The scheme handler must handle:
- GET requests — fetch URL via curl, return body + content type
- POST requests — WebKit custom scheme handlers don't directly expose POST bodies. This is a known WebKitGTK limitation. For Phase 1, POST to .onion may not work (GET is sufficient for most .onion sites and Nostr relay web interfaces). A future enhancement could use a WebExtension for POST body access.
- Redirects — curl follows redirects by default
(
CURLOPT_FOLLOWLOCATION). The final URL is returned. - Headers — curl captures response headers. We pass through Content-Type so WebKit renders correctly (HTML, JSON, images, etc.).
- Cookies — .onion cookies are handled by curl's cookie engine (separate from WebKit's cookie jar). For Phase 1, this is acceptable. Future: share cookies between WebKit and the Tor handler.
- Timeouts — 30 second timeout for .onion (Tor can be slow).
Async handling
Like nostr_scheme.c, the tor:// handler uses GTask to run the
curl fetch on a worker thread and finish the WebKit request on the main
context. This prevents blocking the UI.
Tor SOCKS endpoint
The Tor SOCKS endpoint is determined by the existing Tor service
management in net_services.c / tor_control.c:
- Attached Tor: use the discovered SOCKS endpoint
(e.g.,
socks5://127.0.0.1:9050) - Managed Tor: use the private Unix socket
(e.g.,
socks5://unix:/home/user/.sovereign_browser/tor/socks.sock)
The tor:// scheme handler queries net_service_get_status(NET_SERVICE_TOR)
to get the current SOCKS endpoint. If Tor is not ready, it returns an
error page.
FIPS .onion relays
If a Nostr relay URL is wss://relay.onion, the C-side relay fetch
code (relay_fetch.c) would need to connect through Tor's SOCKS proxy.
This is a future enhancement — the nostr_core_lib relay client would
need SOCKS support. For Phase 1, only WebKit .onion browsing is
supported.
Changes Required
1. Change WebKit proxy to always NO_PROXY
In src/net_services.c, net_services_refresh_proxy():
- Remove the current logic that sets
CUSTOMproxy mode with Tor's SOCKS endpoint. - Always set
WEBKIT_NETWORK_PROXY_MODE_NO_PROXY. - Tor proxying is now handled entirely by the
tor://scheme handler, not by WebKit's proxy settings. - Keep the function (it's called from various places) but simplify it to always set NO_PROXY. Or remove the calls and just set NO_PROXY once at startup.
2. Create src/tor_scheme.c / src/tor_scheme.h
New URI scheme handler for tor://:
void tor_scheme_register(WebKitWebContext *ctx);
- Registers the
torURI scheme - On request: parse the .onion URL, get Tor SOCKS endpoint from
net_service_get_status(NET_SERVICE_TOR), fetch via libcurl with SOCKS proxy, return response to WebKit - Async via
GTask(same pattern asnostr_scheme.c) - Returns error JSON if Tor is not ready
3. Intercept .onion in on_decide_policy
In src/tab_manager.c, on_decide_policy():
/* Check if the navigation target is a .onion URL */
if (uri && (g_str_has_suffix(host, ".onion"))) {
/* Rewrite to tor:// scheme */
char *tor_url = g_strdup_printf("tor://%s", uri + strlen("http://"));
/* or preserve https:// → tor:// */
webkit_web_view_load_uri(webview, tor_url);
g_free(tor_url);
webkit_policy_decision_ignore(decision);
return TRUE;
}
Also intercept in normalize_url() so the URL bar shows the original
http://abc.onion form (not tor://abc.onion).
4. Register tor:// scheme in main.c
tor_scheme_register(web_ctx);
5. Enable Tor and FIPS by default
In src/settings.c, change defaults:
s->tor_enabled = TRUE; /* was FALSE */
s->fips_enabled = TRUE; /* was FALSE */
6. Update Makefile
Add src/tor_scheme.c to SRC.
7. Update net_services_refresh_proxy()
Simplify to always set NO_PROXY. Remove the fail-closed logic (no longer needed — clearnet always works, .onion goes through the scheme handler).
Files
src/
├── tor_scheme.c # tor:// URI scheme handler (new)
├── tor_scheme.h # Public API (new)
├── net_services.c # Simplified: always NO_PROXY
├── tab_manager.c # .onion interception in on_decide_policy + normalize_url
├── main.c # Register tor:// scheme
├── settings.c # Tor/FIPS enabled by default
└── Makefile # Add tor_scheme.c
Implementation Phases
Phase 1: .onion browsing via tor:// scheme
- Create
tor_scheme.c— curl-based .onion fetcher with SOCKS proxy - Register
tor://scheme inmain.c - Intercept
.onioninon_decide_policyandnormalize_url - Simplify
net_services_refresh_proxy()to always NO_PROXY - Enable Tor and FIPS by default in settings
- Test:
http://duckduckgogg42tjsool4r5mn3r2onion.onionloads through Tor
Phase 2: Polish
- Handle .onion redirects (curl follows, but URL bar should show final)
- Error pages for .onion when Tor is not ready
- Cookie handling for .onion (curl cookie jar)
- Timeout handling (30s for .onion)
- Content-type passthrough (HTML, JSON, images, CSS, JS)
Phase 3: .onion Nostr relays (future)
- Add SOCKS proxy support to relay fetch code
- Allow
wss://relay.onionin relay configuration - Route .onion relay connections through Tor
Testing
- Build and start browser
- Verify
http://example.comloads directly (not through Tor) - Verify
file:///pathworks - If Tor binary is available:
- Verify
http://something.onionloads through Tor - Verify Tor log shows the .onion connection
- Verify
- If Tor binary is not available:
- Verify .onion URLs show "Tor not ready" error
- Verify clearnet still works fine
- Verify FIPS .fips URLs still work (if FIPS is available)
- Verify Tor and FIPS are enabled by default on fresh install