Files
sovereign_browser/plans/selective-tor-onion-routing.md

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 interface
  • nostr:// → 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

  1. on_decide_policy in tab_manager.c intercepts navigation to .onion URLs (both http://*.onion and https://*.onion).

  2. Rewrite to tor:// scheme: The intercepted URL is rewritten:

    • http://abc.onion/path?q=1tor://abc.onion/path?q=1
    • https://abc.onion/pathtor://abc.onion/path
    • The scheme handler knows to use Tor's SOCKS proxy.
  3. tor:// URI scheme handler (new src/tor_scheme.c):

    • Registered via webkit_web_context_register_uri_scheme(ctx, "tor", ...)
    • Parses the .onion hostname, port, path, query from the URI
    • Fetches the content via Tor's SOCKS proxy using libcurl with CURLOPT_PROXY set 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)
  4. URL bar: When the user types abc.onion in the URL bar, normalize_url() should recognize .onion and prepend http:// (or https://), then the on_decide_policy interception handles the rest.

  5. Links on pages: When a web page has <a href="http://abc.onion">, clicking it triggers on_decide_policy, which intercepts and rewrites to tor://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:

  1. GET requests — fetch URL via curl, return body + content type
  2. 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.
  3. Redirects — curl follows redirects by default (CURLOPT_FOLLOWLOCATION). The final URL is returned.
  4. Headers — curl captures response headers. We pass through Content-Type so WebKit renders correctly (HTML, JSON, images, etc.).
  5. 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.
  6. 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 CUSTOM proxy 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 tor URI 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 as nostr_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

  1. Create tor_scheme.c — curl-based .onion fetcher with SOCKS proxy
  2. Register tor:// scheme in main.c
  3. Intercept .onion in on_decide_policy and normalize_url
  4. Simplify net_services_refresh_proxy() to always NO_PROXY
  5. Enable Tor and FIPS by default in settings
  6. Test: http://duckduckgogg42tjsool4r5mn3r2onion.onion loads through Tor

Phase 2: Polish

  1. Handle .onion redirects (curl follows, but URL bar should show final)
  2. Error pages for .onion when Tor is not ready
  3. Cookie handling for .onion (curl cookie jar)
  4. Timeout handling (30s for .onion)
  5. Content-type passthrough (HTML, JSON, images, CSS, JS)

Phase 3: .onion Nostr relays (future)

  1. Add SOCKS proxy support to relay fetch code
  2. Allow wss://relay.onion in relay configuration
  3. Route .onion relay connections through Tor

Testing

  1. Build and start browser
  2. Verify http://example.com loads directly (not through Tor)
  3. Verify file:///path works
  4. If Tor binary is available:
    • Verify http://something.onion loads through Tor
    • Verify Tor log shows the .onion connection
  5. If Tor binary is not available:
    • Verify .onion URLs show "Tor not ready" error
    • Verify clearnet still works fine
  6. Verify FIPS .fips URLs still work (if FIPS is available)
  7. Verify Tor and FIPS are enabled by default on fresh install