BitChat Tor Integration: How the iOS App Routes All Traffic Through Tor by Default

BitChat routes all Internet-bound traffic through the Tor network by default using an embedded Arti client that exposes a local SOCKS5 proxy on 127.0.0.1:39050, with a fail-closed design that prevents clearnet leaks when the user enables "Tor routing."

The permissionlesstech/bitchat repository implements comprehensive BitChat Tor integration by embedding the Arti library directly into the Swift application as a local package. This architecture ensures that all Nostr relay connections and geo-relay directory fetches are transparently proxied through the Tor network without requiring users to install external Tor daemon applications.

Core Architecture of BitChat Tor Integration

TorManager and the Embedded Arti Client

At the heart of the system lies TorManager, defined in localPackages/Arti/Sources/TorManager.swift. This singleton owns the Arti library—an in-process Tor client compiled as a Swift package within localPackages/Arti/.

Key responsibilities include:

  • Creating the Arti data directory under Application Support
  • Starting the embedded client and exposing a SOCKS5 listener on 127.0.0.1:39050
  • Providing the async awaitReady() API for blocking until the proxy is reachable
  • Managing lifecycle states including goDormantOnBackground() and shutdownCompletely()

TorURLSession for Transparent Proxying

The TorURLSession class in localPackages/Arti/Sources/TorURLSession.swift wraps a shared URLSession instance. It automatically configures the SOCKS5 proxy when Tor is enabled via setProxyMode(useTor:), ensuring all network requests route through the local Tor client without manual configuration by network consumers.

NetworkActivationService for User Control

User preferences are managed by NetworkActivationService in bitchat/Services/NetworkActivationService.swift. This service reads the user-visible "Tor routing" toggle (userTorEnabled) and determines whether TorManager should auto-start via the isAutoStartAllowed property.

How BitChat Tor Integration Works

The integration follows a strict initialization sequence to maintain privacy guarantees:

  1. Startup Check – AppRuntime queries NetworkActivationService to determine if Tor is allowed. If isAutoStartAllowed returns true, it calls TorManager.shared.ensureRunningOnForeground().

  2. Arti Initialization – TorManager launches the Arti static library, logging the SOCKS endpoint at 127.0.0.1:39050 upon successful bootstrap.

  3. Readiness Verification – Network components like NostrRelayManager (in bitchat/Nostr/NostrRelayManager.swift) and GeoRelayDirectory call awaitReady() before opening connections. This blocks until the proxy responds or a timeout triggers a .TorBootstrapDidStall notification handled by ChatViewModel+Tor.swift.

  4. Traffic Proxying – TorURLSession.setProxyMode(useTor:) switches the shared session to use the SOCKS5 proxy. All outbound traffic—from Nostr websockets to CSV directory downloads—flows through this single chokepoint.

  5. Graceful Shutdown – When the app enters the background or the user disables the feature, TorManager either enters dormancy or terminates completely to conserve battery and reduce network fingerprinting.

Fail-Closed Security Model

BitChat employs a fail-closed design: if Tor is enabled but not ready, network calls are queued or skipped rather than falling back to clearnet. Developers can override this behavior using the compile-time flag BITCHAT_DEV_ALLOW_CLEARNET, but this is strictly disabled in production builds to prevent IP leaks.

Implementation Example: Using Tor in Swift Code

Enable Tor routing and wait for bootstrap completion before making requests:

import Bitchat
import Arti

// Enable Tor via the user preference system
NetworkActivationService.shared.setUserTorEnabled(true)

// Check current Tor status
if TorManager.shared.torEnforced && TorManager.shared.isReady {
    print("Tor is active – all traffic is proxy-wrapped")
}

// Block until SOCKS proxy is ready (60s default timeout)
Task {
    do {
        try await TorManager.shared.awaitReady()
        
        // Use the Tor-aware URLSession
        let session = TorURLSession.shared.session
        let (data, _) = try await session.data(from: URL(string: "https://example.onion")!)
    } catch {
        print("Tor bootstrap stalled or disabled")
    }
}

// Manually toggle proxy mode (normally handled automatically by NetworkActivationService)
TorURLSession.shared.setProxyMode(useTor: true)

Summary

  • BitChat Tor integration embeds the Arti client as a Swift package in localPackages/Arti/, eliminating external dependencies on Orbot or other Tor apps.
  • The TorManager singleton exposes a local SOCKS5 proxy at 127.0.0.1:39050 and manages the embedded client lifecycle including dormancy and shutdown.
  • TorURLSession provides a shared URLSession that automatically routes traffic through the Tor proxy when enabled, falling back to direct connections only when Tor is explicitly disabled.
  • NetworkActivationService gates Tor startup based on the user-controlled "Tor routing" toggle, ensuring privacy preferences are respected across app sessions.
  • The fail-closed architecture ensures no clearnet leakage occurs when Tor is enabled but unavailable, with optional developer overrides via compile-time flags.

Frequently Asked Questions

Does BitChat require Tor to function?

No. While BitChat Tor integration is enabled by default, users can disable it via the "Tor routing" toggle in Settings. When disabled, TorURLSession falls back to a direct URLSession connection without proxying through the local SOCKS port.

What happens if Tor fails to bootstrap?

If the Arti client cannot establish a connection within the timeout period, TorManager posts a .TorBootstrapDidStall notification. The UI layer—handled in bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift—displays a warning to the user, and the fail-closed design prevents any network requests from leaking to clearnet while Tor remains in a non-ready state.

Can I disable the fail-closed behavior for testing?

Yes, but only at compile time. Developers can define the BITCHAT_DEV_ALLOW_CLEARNET flag to permit clearnet connections when Tor is unavailable. This flag is strictly disabled in production builds to guarantee user privacy and prevent accidental IP exposure.

Which network components use the Tor proxy?

All network-dependent subsystems consume TorURLSession.shared, including NostrRelayManager for WebSocket connections to Nostr relays and GeoRelayDirectory for fetching the relay CSV directory. This ensures comprehensive traffic coverage through the single SOCKS5 endpoint at 127.0.0.1:39050.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →