BitChat Tor Integration Architecture: How the Arti SOCKS5 Proxy Works
BitChat routes all internet traffic through an embedded Arti Tor client that exposes a SOCKS5 proxy on 127.0.0.1:39050, using an in-process Swift wrapper to enforce fail-closed networking without external tor binaries.
The permissionlesstech/bitchat repository implements a privacy-first networking model where Nostr relay connections and directory fetches are forcibly tunneled through Tor. This architecture eliminates the need for users to run a separate Tor daemon by embedding the Arti Rust client directly into the iOS app via Swift Package Manager.
How BitChat Embeds Arti as an In-Process Tor Client
Unlike traditional Tor apps that depend on a system tor binary or control port, BitChat vendors Arti as a local Swift package under localPackages/Arti. This package wraps a pre-built Rust static library (localPackages/Arti/Frameworks/arti.xcframework) and exposes it through Foreign Function Interface (FFI) bindings.
The integration is declared in the root Package.swift:
.package(path: "localPackages/Arti")
Because Arti runs in-process, there is no torrc file to manage and no external process to spawn. The app links directly against the Rust static library, creating a completely self-contained Tor client that starts and stops with the application lifecycle.
The SOCKS5 Proxy Interface
When Arti initializes, it binds a SOCKS5 listener on 127.0.0.1:39050. This hard-coded port serves as the single network egress point for the entire app. According to the project's Tor integration documentation, this localhost endpoint is the only network interface exposed to the rest of the BitChat codebase.
All outbound WebSocket connections to Nostr relays and HTTP requests to the geo-relay directory must flow through this SOCKS5 proxy. The architecture deliberately avoids exposing raw sockets to prevent accidental clearnet leakage.
TorManager: The Core Swift Controller
The TorManager class in localPackages/Arti/Sources/TorManager.swift orchestrates the Arti lifecycle, bootstrap monitoring, and readiness signaling. It maintains singleton state via TorManager.shared and publishes key properties including isReady, isStarting, bootstrapProgress, and bootstrapDidStall.
Starting Arti via FFI
When the app enters the foreground and Tor is permitted, startIfNeeded() performs initialization:
await TorManager.shared.startIfNeeded()
This method ensures the filesystem layout exists under ~/Library/Application Support/bitchat/arti, then invokes the FFI function arti_start(dataDir, socksPort) with the hard-coded port 39050. The underlying Rust client begins bootstrapping into the Tor network immediately.
Bootstrap Monitoring and Readiness Detection
After starting Arti, TorManager runs a readiness probe using probeSocksOnce(), which attempts to connect to the SOCKS socket using the Network framework. If the socket accepts within 60 seconds, socksReady flips to true.
Concurrently, bootstrapPollLoop queries the Arti FFI every second:
// Pseudocode representation of the polling logic
let progress = arti_bootstrap_progress()
let summary = arti_bootstrap_summary()
The loop runs for up to 75 seconds, waiting for bootstrapProgress to reach 100%. If bootstrap stalls, bootstrapDidStall becomes true and the app posts a .TorBootstrapDidStall notification, triggering UI error states. Only when both socksReady and bootstrapProgress ≥ 100 does isReady become true, unblocking network operations.
Foreground and Background Lifecycle
BitChat handles app lifecycle transitions strictly to prevent leaks:
- Backgrounding:
goDormantOnBackground()clears readiness flags because Arti does not support true dormancy. - Foregrounding:
ensureRunningOnForeground()checks state and may triggerrestartArti()if the client is not ready.
This ensures that Tor is either fully active or explicitly offline—never in a half-open state that might accidentally route traffic outside the proxy.
Enforcing Tor for All Network Traffic
Unless the compile-time flag BITCHAT_DEV_ALLOW_CLEARNET is defined, the torEnforced property returns true. All network code uses TorURLSession, a shared URLSession configured via setProxyMode(useTor:):
var config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFNetworkProxiesSOCKSProxy: TorManager.shared.socksHost,
kCFNetworkProxiesSOCKSPort: TorManager.shared.socksPort
]
let session = URLSession(configuration: config)
This creates a fail-closed model: if Arti cannot bootstrap or the SOCKS proxy is unreachable, outbound connections queue rather than falling back to clearnet. Both public and private Nostr messages route through the same Arti SOCKS5 proxy, ensuring consistent anonymity.
Network Path Monitoring and Recovery
On platforms supporting the Network framework, TorManager instantiates an NWPathMonitor to observe connectivity changes. When the network path changes while the app is foregrounded and Arti is not ready, pokeTorOnPathChange() calls ensureRunningOnForeground() to initiate recovery.
This automatic healing handles scenarios where the device switches from Wi-Fi to cellular or encounters network interruptions that invalidate Tor circuits.
Summary
- BitChat embeds Arti as a vendored Swift package (
localPackages/Arti) wrapping a Rust static library, eliminating external Tor daemon dependencies. - The Arti SOCKS5 proxy listens strictly on
127.0.0.1:39050and serves as the exclusive network egress for all app traffic. TorManager.swiftcontrols lifecycle via FFI functionsarti_start,arti_bootstrap_progress, andarti_bootstrap_summary, enforcing a 75-second bootstrap timeout.- The app implements a fail-closed policy: connections are proxied through
TorURLSessionor queued, never sent clearnet, unless theBITCHAT_DEV_ALLOW_CLEARNETflag is present. - Network path monitoring via
NWPathMonitortriggers automatic Tor recovery when connectivity changes occur.
Frequently Asked Questions
What port does BitChat's Arti SOCKS5 proxy use?
BitChat's Arti SOCKS5 proxy binds to port 39050 on localhost (127.0.0.1). This port is hard-coded in the source code and defined in the TorManager initialization logic.
How does BitChat handle Tor bootstrap failures?
If Arti fails to reach 100% bootstrap progress within approximately 75 seconds, TorManager sets bootstrapDidStall to true and posts a .TorBootstrapDidStall notification. The app treats this as a hard failure, keeping isReady false and preventing network requests from leaving the device until the user resolves the connectivity issue.
Does BitChat require an external Tor daemon?
No. BitChat does not use the system tor binary or a control port. Instead, it embeds Arti as a static Rust library (arti.xcframework) vendored under localPackages/Arti/Frameworks/, linking it directly into the iOS app process via Swift Package Manager.
How does BitChat maintain anonymity when the app backgrounds?
When the app backgrounds, goDormantOnBackground() clears all readiness flags, effectively pausing network activity. Because Arti does not support true dormant mode, BitChat treats the client as offline during backgrounding. Upon returning to the foreground, ensureRunningOnForeground() may execute restartArti() to re-establish the SOCKS5 proxy and ensure no clearnet connections occur until the Tor circuit is fully rebuilt.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →