A lightweight embedded Tor client for iOS/macOS by Olib AI
Used in StealthOS - The privacy-focused operating environment.
This package provides a native Swift wrapper around the Tor C library, enabling anonymous network access directly within your app without external dependencies.
Because we can. We wanted a clean, slim Tor client that we fully control — from source compilation to the final binary. No bloated wrappers, no outdated dependencies dragged in from abandoned repos, no guessing what's inside the black box. Just Tor compiled from official sources, statically linked, with a modern Swift API on top. Full transparency, full control, minimal footprint.
- Embedded Tor Daemon: Full Tor implementation compiled as a static library - no external processes or dependencies
- Client-Only Mode: Optimized build with relay functionality disabled for smaller footprint
- Auto Port Selection: Automatic SOCKS5 port assignment to avoid conflicts with other Tor instances
- Connection Status Monitoring: Real-time bootstrap progress tracking via stdout parsing
- Swift Concurrency: Modern async/await API with strict Swift 6 concurrency compliance
- Actor Isolation: Thread-safe
TorServiceactor ensures data race prevention - Circuit Management: Extended circuit lifetime settings for stable long-running connections
- New Circuit on Demand: Request new Tor identity (SIGNAL NEWNYM) via in-process control socket
- iOS 18.0+
- Swift 6.0+
- Xcode 16+
To build the XCFramework from source, you also need:
- macOS with Apple Silicon (arm64) or Intel Mac
- Xcode Command Line Tools (
xcode-select --install) - Homebrew packages:
brew install autoconf automake libtool pkg-configThe TorClientC.xcframework is included pre-built for convenience. If you want to build it yourself (to verify, modify, or update versions):
# From the TorClient package directory:
./Scripts/build-xcframework.sh
# Or clean and rebuild:
./Scripts/build-xcframework.sh --cleanThe build script will:
- Download source tarballs from official mirrors (zlib, OpenSSL, libevent, Tor)
- Cross-compile each library for iOS device (arm64) and simulator (arm64 + x86_64 stub)
- Combine all libraries into a single
libTorClient.aper platform - Package everything into
TorClientC.xcframework
| Library | Version | Source |
|---|---|---|
| Tor | 0.4.9.6 | https://dist.torproject.org/ |
| OpenSSL | 3.6.1 | https://github.com/openssl/openssl |
| libevent | 2.1.12-stable | https://github.com/libevent/libevent |
| zlib | 1.3.2 | https://zlib.net/ |
Build time: ~10-30 minutes depending on CPU. OpenSSL is the longest step.
Downloaded source tarballs are cached in .build-tor/ and reused on subsequent runs.
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/Olib-AI/TorClient.git", from: "1.2.1")
]Then add the dependency to your target:
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "TorClientWrapper", package: "TorClient")
]
)
]If using XcodeGen, add to your project.yml:
packages:
TorClient:
path: LocalPackages/TorClient
targets:
YourApp:
dependencies:
- package: TorClient
product: TorClientWrapperThen regenerate: xcodegen generate
import TorClientWrapper
// Get the shared TorService instance
let torService = TorService.shared
// Configure Tor
let dataDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("tor")
let config = TorConfiguration(
dataDirectory: dataDirectory,
socksPort: .auto, // Let Tor pick an available port
clientOnly: true, // No relay functionality
avoidDiskWrites: true // Minimize disk I/O
)
do {
// Configure and start Tor
try await torService.configure(config)
try await torService.start()
// Wait for Tor to bootstrap (connect to the network)
try await torService.waitForBootstrap(timeout: 120)
// Get the SOCKS5 proxy URL
if let proxyURL = await torService.socksProxyURL {
print("Tor SOCKS5 proxy: \(proxyURL)")
// Use with URLSession or other networking
}
// Get the actual port Tor bound to
let port = await torService.actualSocksPort
print("Tor running on port: \(port)")
} catch {
print("Tor error: \(error)")
}let port = await torService.actualSocksPort
let sessionConfig = URLSessionConfiguration.ephemeral
sessionConfig.connectionProxyDictionary = [
kCFProxyTypeKey as String: kCFProxyTypeSOCKS,
kCFStreamPropertySOCKSProxyHost as String: "127.0.0.1",
kCFStreamPropertySOCKSProxyPort as String: port
]
let session = URLSession(configuration: sessionConfig)
let (data, response) = try await session.data(from: url)| Property | Type | Default | Description |
|---|---|---|---|
dataDirectory |
URL |
Required | Directory for Tor state files (keys, consensus, etc.) |
socksPortMode |
PortMode |
.auto |
SOCKS5 port configuration |
bridges |
[String] |
[] |
Bridge relay addresses for censored networks |
clientOnly |
Bool |
true |
Disable relay/exit functionality |
avoidDiskWrites |
Bool |
true |
Minimize disk I/O for privacy |
geoIPFile |
URL? |
nil |
Path to GeoIP database file |
geoIP6File |
URL? |
nil |
Path to GeoIPv6 database file |
public enum PortMode: Sendable, Equatable {
case auto // Let Tor select an available port
case fixed(UInt16) // Use a specific port number
}Recommendation: Use .auto to avoid port conflicts with other Tor instances or services.
TorService is the main interface for controlling the Tor daemon. It is implemented as a Swift actor for thread safety.
/// Shared singleton instance
public static let shared: TorService
/// Whether Tor daemon is currently running
public var isRunning: Bool { get async }
/// Whether Tor is ready to accept connections (running and bootstrapped)
public var isReady: Bool { get async }
/// The actual SOCKS port Tor bound to (0 if not yet discovered)
public var actualSocksPort: UInt16 { get async }
/// Current Tor status
public var status: TorStatus { get async }
/// SOCKS5 proxy URL (nil if not running)
public var socksProxyURL: URL? { get async }/// Configure Tor with the specified settings
/// Must be called before start(). Cannot reconfigure while running.
public func configure(_ config: TorConfiguration) async throws
/// Start the Tor daemon
/// If already running, returns immediately without error.
public func start() async throws
/// Stop the Tor daemon
/// WARNING: Tor C library has global state - cannot restart after stop!
public func stop() async throws
/// Wait for Tor to fully bootstrap
/// - Parameter timeout: Maximum time to wait (default: 120 seconds)
public func waitForBootstrap(timeout: TimeInterval = 120) async throws
/// Request a new Tor circuit (new identity / exit node)
/// Sends SIGNAL NEWNYM via the in-process control socket.
public func sendNewnym() async throws
/// Whether the control socket is available for sending commands
public var hasControlSocket: Bool { get }
/// Get current bootstrap progress (0-100)
public func getBootstrapProgress() async -> Int?
/// Stream of status updates
public var statusStream: AsyncStream<TorStatus> { get async }public enum TorStatus: Sendable, Equatable {
case idle // Not started
case starting // Starting up
case connecting // Connecting to network
case bootstrapping(progress: Int) // Bootstrap in progress (0-100%)
case ready // Fully connected and ready
case stopping // Shutting down
case error(TorError) // Error occurred
}public enum TorError: Error, Sendable {
case configurationFailed(String) // Configuration issue
case startFailed(String) // Failed to start daemon
case alreadyRunning // Tor already running (configure error)
case notRunning // Tor not running (operation error)
case bootstrapTimeout // Bootstrap took too long
case connectionFailed // Network connection failed
case shutdownFailed // Failed to shut down cleanly
case controlSocketFailed // Control socket error
}CRITICAL: The Tor C library (tor_run_main()) can only be called once per process. The library uses global state that is not reset between calls. Attempting to restart Tor after stopping will cause an assertion failure.
Best Practice: Keep Tor running for the lifetime of your application. If you need to "disconnect," simply stop routing traffic through the SOCKS5 proxy rather than stopping the daemon.
This implementation uses tor_main_configuration_setup_control_socket() to obtain a pre-authenticated in-process control socket instead of a TCP control port. Status monitoring is done by parsing Tor's stdout. This approach:
- Avoids TCP port conflicts
- Reduces attack surface (no network-accessible control port)
- Works reliably on iOS where socket permissions may be restricted
- Enables
SIGNAL NEWNYMfor requesting new circuits/identities on demand
TorService is an actor. All property access and method calls must use await from non-isolated contexts:
// Correct
let port = await TorService.shared.actualSocksPort
// Incorrect - will not compile
let port = TorService.shared.actualSocksPortTorClient/
├── Package.swift # Swift Package manifest
├── Scripts/
│ └── build-xcframework.sh # Build Tor + deps from source
├── Sources/
│ └── TorClientWrapper/
│ └── TorService.swift # Swift actor wrapping Tor C API
└── TorClientC.xcframework/ # Pre-built static libraries
├── ios-arm64/ # iOS Device
│ ├── Headers/
│ │ ├── tor_api.h # Tor C API
│ │ ├── module.modulemap
│ │ └── openssl/ # OpenSSL headers
│ └── libTorClient.a # Combined static library (~13MB)
└── ios-arm64_x86_64-simulator/
└── ... # Simulator libraries (arm64 real + x86_64 stub)
libTorClient.a is a combined static library containing:
- Tor 0.4.9.6: The Onion Router (client-only build)
- OpenSSL 3.6.1: Cryptographic library
- libevent 2.1.12: Event notification library
- zlib 1.3.2: Compression library
All dependencies are statically linked - no dynamic frameworks required.
- Initialization:
TorService.configure()creates the data directory and stores configuration - Startup:
TorService.start()launchestor_run_main()in a background thread - Port Discovery: stdout is parsed to find the auto-assigned SOCKS5 port
- Bootstrap Monitoring: stdout is parsed for bootstrap progress (0-100%)
- Control Socket:
tor_main_configuration_setup_control_socket()provides a pre-authenticated control connection for runtime commands - SOCKS5 Proxy: Applications route traffic through
127.0.0.1:<port> - New Circuit:
sendNewnym()writesSIGNAL NEWNYMto the control socket, forcing Tor to build new circuits with a different exit node - Persistence: Tor daemon runs until app termination (no restart capability)
The Swift wrapper sets these Tor options automatically:
--DataDirectory /path/to/tor # State storage
--SocksPort auto # Auto port selection
--ControlPort 0 # Disable TCP control port (uses in-process socket instead)
--ClientOnly 1 # No relay functionality
--DirCache 0 # Disable directory cache
--HiddenServiceStatistics 0 # Disable HS stats
--AvoidDiskWrites 1 # Minimize disk I/O
--Log notice stdout # Log to stdout for parsing
--MaxCircuitDirtiness 1800 # 30 min circuit lifetime
--NewCircuitPeriod 60 # Less aggressive circuit building
--CircuitBuildTimeout 90 # Patient circuit building
--SocksTimeout 120 # Generous SOCKS timeout
Check the console logs for Tor's error messages. Common causes:
- Invalid data directory permissions
- Missing GeoIP files (optional, but Tor may warn)
- Network connectivity issues
Tor may take 1-2 minutes to bootstrap on slow networks. Consider:
- Increasing the timeout:
try await tor.waitForBootstrap(timeout: 180) - Using bridges if Tor is blocked:
TorConfiguration(bridges: [...])
Use .auto port mode (default) to let Tor find an available port.
This is a known limitation of the Tor C library. Design your app to:
- Start Tor once at launch
- Keep it running until app termination
- "Disconnect" by routing traffic directly instead of through SOCKS5
MIT License
Copyright (c) 2025 Olib AI
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- Olib AI - Package maintainer and StealthOS developer
- The Tor Project - Tor software and anonymity network
- OpenSSL Project - Cryptographic library
- libevent - Event notification library
- zlib - Compression library
Contributions are welcome! Please ensure:
- Code compiles under Swift 6 strict concurrency
- All public APIs are documented
- Actor isolation is maintained for thread safety
- No use of
@preconcurrencyescape hatches
If you discover a security vulnerability, please report it privately to security@olib.ai rather than opening a public issue.