Swift package that embeds Tor (libtor) and provides a Swift-concurrency-first API (TorClient), plus Tor control protocol utilities (including ephemeral onion service management).
- Features
- Platforms
- Installation
- Quick Start (Basic)
- Creating a Hidden Service
- Demo
- Testing
- Roadmap
- Security
- Contributing
- License
- Embedded Tor: run Tor in-process (via the
libtorproduct) - High-level API:
TorClientactor to start/stop Tor and observe events - Control protocol:
TorControlClientforGETINFO,SIGNAL,ADD_ONION,DEL_ONION, etc. - Onion services: create/delete ephemeral v3 onion services
- Caching: optional
cacheDirectoryto reuse consensus/descriptor cache across runs - Apple-only networking helper:
URLSessionConfiguration+TorClient.makeURLSession()(guarded bycanImport(CFNetwork))
- macOS: 15+
- iOS: 18+
- Linux: Ubuntu 22.04+ (via Docker)
Important
tvOS/watchOS/visionOS are not supported. Tor's codebase relies on UNIX process primitives (fork, execve, daemon, setuid) that Apple prohibits on these platforms. These restrictions are enforced at the App Store review level and would cause runtime crashes.
This package uses Swift Package Manager.
- Go to
File > Add Packages... - Enter the package URL:
https://github.com/21-DOT-DEV/swift-tor - Select the desired version
Add the dependency:
.package(url: "https://github.com/21-DOT-DEV/swift-tor", from: "0.1.0"),Warning
This package is pre-1.0 (SemVer major version zero). The public API is not stable and may change with any release. Pin a version using exact: to avoid unexpected breaking changes.
Then add Tor as a dependency:
.target(
name: "MyApp",
dependencies: [
.product(name: "Tor", package: "swift-tor")
]
)Start Tor, wait for bootstrap, and get a SOCKS endpoint:
import Tor
let config = TorConfiguration.makeDefault()
let client = TorClient(configuration: config)
try await client.start()
try await client.waitUntilBootstrapped()
let socks = await client.socksEndpointTip
Reusing a cacheDirectory across runs can significantly reduce bootstrap time.
import Tor
import Foundation
let tempDataDir = FileManager.default.temporaryDirectory
.appendingPathComponent("tor-data-\(UUID().uuidString)")
.path
let cacheDir = FileManager.default.temporaryDirectory
.appendingPathComponent("tor-cache")
.path
try? FileManager.default.createDirectory(atPath: cacheDir, withIntermediateDirectories: true)
let config = TorConfiguration(
dataDirectory: tempDataDir,
cacheDirectory: cacheDir,
socksPort: .ephemeral
)
let client = TorClient(configuration: config)
try await client.start()
try await client.waitUntilBootstrapped()Note
This helper requires CFNetwork and is only available on Apple platforms.
#if canImport(CFNetwork)
let session = try await client.makeURLSession()
let (data, _) = try await session.data(from: URL(string: "https://check.torproject.org/api/ip")!)
#endifCreating a Hidden Service
Create an ephemeral v3 onion service that forwards traffic to a local server:
import Tor
// Start Tor first
let client = TorClient(configuration: .makeDefault())
try await client.start()
try await client.waitUntilBootstrapped()
// Get the control client
let control = try await client.control()
// Create an ephemeral onion service
// - Maps port 80 on the .onion to localhost:8080
// - Private key is discarded (service won't survive restart)
let service = try await control.addOnion(
key: .newV3(discardPrivateKey: true),
ports: [.toLocalPort(80, localPort: 8080)]
)
print("π§
Hidden service running at: \(service.onionAddress)")
// e.g., "duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion"
// Your local HTTP server on port 8080 is now accessible via Tor!
// Users can reach it at: http://<serviceID>.onion/
// When done, clean up
try await control.delOnion(service)
await client.stop()Persistent Hidden Services
To create a hidden service that survives restarts, keep the private key:
// Create service and get the private key
let service = try await control.addOnion(
key: .newV3(discardPrivateKey: false), // Keep the key
ports: [.toLocalPort(443, localPort: 8443)]
)
// Save service.privateKey securely for later use
let privateKey = service.privateKey! // e.g., "ED25519-V3:base64..."
// Later, recreate the same .onion address:
let restoredService = try await control.addOnion(
key: .providedV3(privateKey),
ports: [.toLocalPort(443, localPort: 8443)]
)
// restoredService.onionAddress == service.onionAddressWarning
Store private keys securely (e.g., Keychain on Apple platforms). Anyone with the private key controls the .onion address.
Run the bundled demo:
swift run TorDemoThe demo starts Tor, fetches a clearnet URL via Tor, fetches an .onion, and creates/deletes an ephemeral onion service.
Run unit tests:
swift testIntegration tests are env-gated and skipped by default:
TOR_INTEGRATION_TESTS=1 swift test --filter IntegrationTests- Linux support: β complete (Phase 1)
- Remove libbsd dependency: β complete (Phase 2)
- iOS Target Refactor: π planned (Phase 3)
- Binary Size Optimization: π planned (Phase 3.5)
See roadmap.md for full details.
For information on reporting security vulnerabilities in swift-tor, see SECURITY.md. For other 21-DOT-DEV projects, see the organization Security Policy.
Caution
Tor can't "fix" unsafe application behavior. Review the Tor Project guidance on staying anonymous. Avoid logging sensitive information (credentials, onion private keys). Consider your threat model β Tor integration is only one part of privacy/security.
Contributions welcome! Please read the 21-DOT-DEV contributing guidelines for general workflow. For swift-tor specific guidance and AI-assisted development, see AGENTS.md.
This project is licensed under the MIT License. See LICENSE.
Tor source code is vendored in Vendor/tor and is subject to its own license(s). See Vendor/tor/LICENSE.