mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:05:20 +00:00
Replace C Tor with Rust Arti for Tor integration
- Replace C Tor (0.4.8.21) with Rust Arti (1.9.0/arti-client 0.38) - 70% smaller binary: 21MB xcframework vs 67MB (6.9MB vs 14MB per slice) - Memory-safe Rust implementation with modern async (tokio) - Same SOCKS5 proxy interface at 127.0.0.1:39050 for drop-in compatibility - FFI wrapper (arti-bitchat crate) with C-compatible exports - Swift TorManager maintains identical public API - Aggressive size optimization: opt-level=z, lto=fat, panic=abort, strip - Supports iOS device, iOS simulator (Apple Silicon), and macOS Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/.build/
|
||||
/.swiftpm/
|
||||
Generated
+5046
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["arti-bitchat"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>macos-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>macos</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
// swift-tools-version:5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Tor", // Keep name "Tor" for drop-in compatibility
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v13),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "Tor",
|
||||
targets: ["Tor"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../BitLogger"),
|
||||
],
|
||||
targets: [
|
||||
// Main Swift target
|
||||
.target(
|
||||
name: "Tor",
|
||||
dependencies: [
|
||||
"arti",
|
||||
.product(name: "BitLogger", package: "BitLogger"),
|
||||
],
|
||||
path: "Sources",
|
||||
exclude: ["C"],
|
||||
sources: [
|
||||
"TorManager.swift",
|
||||
"TorURLSession.swift",
|
||||
"TorNotifications.swift",
|
||||
],
|
||||
linkerSettings: [
|
||||
.linkedLibrary("resolv"),
|
||||
.linkedLibrary("z"),
|
||||
.linkedLibrary("sqlite3"),
|
||||
]
|
||||
),
|
||||
// Binary framework containing the Rust static library
|
||||
.binaryTarget(
|
||||
name: "arti",
|
||||
path: "Frameworks/arti.xcframework"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
// Empty shim file to satisfy SPM target requirements.
|
||||
// The actual implementation is in the Rust static library (arti.xcframework).
|
||||
// This file exists only to make SPM happy with a C target.
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* @param data_dir Path to data directory for Tor state (C string)
|
||||
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
|
||||
* @return 0 on success, negative on error:
|
||||
* -1: already running
|
||||
* -2: invalid data_dir
|
||||
* -3: runtime initialization failed
|
||||
* -4: bootstrap failed
|
||||
*/
|
||||
int32_t arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* @return 1 if running, 0 if not running
|
||||
*/
|
||||
int32_t arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*
|
||||
* @return Progress percentage
|
||||
*/
|
||||
int32_t arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* @param buf Buffer to write the summary into
|
||||
* @param len Length of the buffer
|
||||
* @return Number of bytes written, -1 on error
|
||||
*/
|
||||
int32_t arti_bootstrap_summary(char *buf, int32_t len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_wake(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ARTI_H */
|
||||
@@ -0,0 +1,4 @@
|
||||
module ArtiC {
|
||||
header "arti.h"
|
||||
export *
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
import BitLogger
|
||||
import Foundation
|
||||
#if canImport(Network)
|
||||
import Network
|
||||
#endif
|
||||
|
||||
#if !canImport(Network)
|
||||
private final class NWPathMonitor {
|
||||
var pathUpdateHandler: ((Any) -> Void)?
|
||||
|
||||
func start(queue: DispatchQueue) {
|
||||
// Path monitoring is unavailable on this platform; nothing to do.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// FFI declarations for Arti (Rust)
|
||||
@_silgen_name("arti_start")
|
||||
private func arti_start(_ dataDir: UnsafePointer<CChar>, _ socksPort: UInt16) -> Int32
|
||||
|
||||
@_silgen_name("arti_stop")
|
||||
private func arti_stop() -> Int32
|
||||
|
||||
@_silgen_name("arti_is_running")
|
||||
private func arti_is_running() -> Int32
|
||||
|
||||
@_silgen_name("arti_bootstrap_progress")
|
||||
private func arti_bootstrap_progress() -> Int32
|
||||
|
||||
@_silgen_name("arti_bootstrap_summary")
|
||||
private func arti_bootstrap_summary(_ buf: UnsafeMutablePointer<CChar>, _ len: Int32) -> Int32
|
||||
|
||||
@_silgen_name("arti_go_dormant")
|
||||
private func arti_go_dormant() -> Int32
|
||||
|
||||
@_silgen_name("arti_wake")
|
||||
private func arti_wake() -> Int32
|
||||
|
||||
/// Arti-based Tor integration for BitChat.
|
||||
/// - Boots a local Arti client and exposes a SOCKS5 proxy
|
||||
/// on 127.0.0.1:socksPort. All app networking should await readiness and
|
||||
/// route via this proxy. Fails closed by default when Tor is unavailable.
|
||||
@MainActor
|
||||
public final class TorManager: ObservableObject {
|
||||
public static let shared = TorManager()
|
||||
|
||||
// SOCKS endpoint where Arti listens
|
||||
let socksHost: String = "127.0.0.1"
|
||||
let socksPort: Int = 39050
|
||||
|
||||
// State
|
||||
@Published private(set) public var isReady: Bool = false
|
||||
@Published private(set) var isStarting: Bool = false
|
||||
@Published private(set) var lastError: Error?
|
||||
@Published private(set) var bootstrapProgress: Int = 0
|
||||
@Published private(set) var bootstrapSummary: String = ""
|
||||
|
||||
// Internal readiness trackers
|
||||
private var socksReady: Bool = false { didSet { recomputeReady() } }
|
||||
private var restarting: Bool = false
|
||||
|
||||
// Whether the app must enforce Tor for all connections (fail-closed).
|
||||
public var torEnforced: Bool {
|
||||
#if BITCHAT_DEV_ALLOW_CLEARNET
|
||||
return false
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns true only when Tor is actually up (or dev fallback is compiled).
|
||||
var networkPermitted: Bool {
|
||||
if torEnforced { return isReady }
|
||||
return true
|
||||
}
|
||||
|
||||
private var didStart = false
|
||||
private var bootstrapMonitorStarted = false
|
||||
private var pathMonitor: NWPathMonitor?
|
||||
private var isAppForeground: Bool = true
|
||||
private var isDormant: Bool = false
|
||||
private var lastRestartAt: Date? = nil
|
||||
private var startedAt: Date? = nil // Tracks initial startup time for grace period
|
||||
private(set) var allowAutoStart: Bool = false
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
public func startIfNeeded() {
|
||||
guard allowAutoStart else { return }
|
||||
guard isAppForeground else { return }
|
||||
guard !didStart else { return }
|
||||
didStart = true
|
||||
isDormant = false
|
||||
isStarting = true
|
||||
startedAt = Date() // Track startup time for grace period
|
||||
SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session)
|
||||
lastError = nil
|
||||
NotificationCenter.default.post(name: .TorWillStart, object: nil)
|
||||
ensureFilesystemLayout()
|
||||
startArti()
|
||||
startPathMonitorIfNeeded()
|
||||
}
|
||||
|
||||
public func setAppForeground(_ foreground: Bool) {
|
||||
isAppForeground = foreground
|
||||
}
|
||||
|
||||
public func isForeground() -> Bool { isAppForeground }
|
||||
|
||||
nonisolated
|
||||
public func awaitReady(timeout: TimeInterval = 25.0) async -> Bool {
|
||||
await MainActor.run {
|
||||
if self.isAppForeground { self.startIfNeeded() }
|
||||
}
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
if await MainActor.run(body: { self.networkPermitted }) { return true }
|
||||
while Date() < deadline {
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
if await MainActor.run(body: { self.networkPermitted }) { return true }
|
||||
}
|
||||
return await MainActor.run(body: { self.networkPermitted })
|
||||
}
|
||||
|
||||
// MARK: - Filesystem
|
||||
|
||||
func dataDirectoryURL() -> URL? {
|
||||
do {
|
||||
let base = try FileManager.default.url(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true
|
||||
)
|
||||
let dir = base.appendingPathComponent("bitchat/arti", isDirectory: true)
|
||||
return dir
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureFilesystemLayout() {
|
||||
guard let dir = dataDirectoryURL() else { return }
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
// Non-fatal; Arti will surface errors during start if paths are missing
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arti Integration
|
||||
|
||||
private func startArti() {
|
||||
guard let dir = dataDirectoryURL()?.path else {
|
||||
isStarting = false
|
||||
lastError = NSError(domain: "TorManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data directory"])
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
if arti_is_running() != 0 {
|
||||
SecureLogger.info("TorManager: Arti already running", category: .session)
|
||||
startBootstrapMonitor()
|
||||
return
|
||||
}
|
||||
|
||||
let result = dir.withCString { dptr in
|
||||
arti_start(dptr, UInt16(socksPort))
|
||||
}
|
||||
|
||||
if result != 0 {
|
||||
SecureLogger.error("TorManager: arti_start failed rc=\(result)", category: .session)
|
||||
isStarting = false
|
||||
lastError = NSError(domain: "TorManager", code: Int(result), userInfo: [NSLocalizedDescriptionKey: "Arti start failed"])
|
||||
return
|
||||
}
|
||||
|
||||
SecureLogger.info("TorManager: arti_start OK (SOCKS \(socksHost):\(socksPort))", category: .session)
|
||||
startBootstrapMonitor()
|
||||
|
||||
// Start SOCKS readiness probe
|
||||
Task.detached(priority: .userInitiated) { [weak self] in
|
||||
guard let self else { return }
|
||||
let ready = await self.waitForSocksReady(timeout: 60.0)
|
||||
await MainActor.run {
|
||||
self.socksReady = ready
|
||||
if ready {
|
||||
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session)
|
||||
} else {
|
||||
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "SOCKS not reachable"])
|
||||
SecureLogger.error("TorManager: SOCKS not reachable (timeout)", category: .session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForSocksReady(timeout: TimeInterval) async -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if await probeSocksOnce() { return true }
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func probeSocksOnce() async -> Bool {
|
||||
#if canImport(Network)
|
||||
await withCheckedContinuation { cont in
|
||||
let params = NWParameters.tcp
|
||||
let host = NWEndpoint.Host.ipv4(.loopback)
|
||||
guard let port = NWEndpoint.Port(rawValue: UInt16(socksPort)) else {
|
||||
cont.resume(returning: false)
|
||||
return
|
||||
}
|
||||
let endpoint = NWEndpoint.hostPort(host: host, port: port)
|
||||
let conn = NWConnection(to: endpoint, using: params)
|
||||
|
||||
var resumed = false
|
||||
let resumeOnce: (Bool) -> Void = { value in
|
||||
if !resumed {
|
||||
resumed = true
|
||||
cont.resume(returning: value)
|
||||
}
|
||||
}
|
||||
|
||||
conn.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
resumeOnce(true)
|
||||
conn.cancel()
|
||||
case .failed, .cancelled:
|
||||
resumeOnce(false)
|
||||
conn.cancel()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1.0) {
|
||||
resumeOnce(false)
|
||||
conn.cancel()
|
||||
}
|
||||
|
||||
conn.start(queue: DispatchQueue.global(qos: .utility))
|
||||
}
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Bootstrap Monitoring
|
||||
|
||||
private func startBootstrapMonitor() {
|
||||
guard !bootstrapMonitorStarted else { return }
|
||||
bootstrapMonitorStarted = true
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
await self?.bootstrapPollLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private func bootstrapPollLoop() async {
|
||||
let deadline = Date().addingTimeInterval(75)
|
||||
while Date() < deadline {
|
||||
let progress = Int(arti_bootstrap_progress())
|
||||
let summary = getBootstrapSummary()
|
||||
|
||||
await MainActor.run {
|
||||
self.bootstrapProgress = progress
|
||||
self.bootstrapSummary = summary
|
||||
if progress >= 100 { self.isStarting = false }
|
||||
self.recomputeReady()
|
||||
}
|
||||
|
||||
if progress >= 100 { break }
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
private func getBootstrapSummary() -> String {
|
||||
var buf = [CChar](repeating: 0, count: 256)
|
||||
let len = arti_bootstrap_summary(&buf, Int32(buf.count))
|
||||
if len > 0 {
|
||||
return String(cString: buf)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MARK: - Foreground/Background
|
||||
|
||||
public func ensureRunningOnForeground() {
|
||||
if !allowAutoStart { return }
|
||||
Task.detached(priority: .userInitiated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let claimed: Bool = await MainActor.run {
|
||||
if self.isStarting || self.restarting { return false }
|
||||
self.restarting = true
|
||||
return true
|
||||
}
|
||||
if !claimed { return }
|
||||
|
||||
// Try to wake if dormant
|
||||
if await self.wakeFromDormant() {
|
||||
await MainActor.run { self.restarting = false }
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already ready
|
||||
let alreadyReady = await MainActor.run { self.isReady }
|
||||
if alreadyReady {
|
||||
await MainActor.run { self.restarting = false }
|
||||
return
|
||||
}
|
||||
|
||||
// Restart
|
||||
await self.restartArti()
|
||||
await MainActor.run { self.restarting = false }
|
||||
}
|
||||
}
|
||||
|
||||
public func goDormantOnBackground() {
|
||||
SecureLogger.debug("TorManager: goDormantOnBackground() called", category: .session)
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let result = arti_go_dormant()
|
||||
if result == 0 {
|
||||
SecureLogger.info("TorManager: signalled DORMANT", category: .session)
|
||||
await MainActor.run {
|
||||
self.isDormant = true
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.isStarting = false
|
||||
}
|
||||
} else {
|
||||
// Dormant not supported, do full shutdown
|
||||
SecureLogger.warning("TorManager: DORMANT failed; shutting down", category: .session)
|
||||
_ = arti_stop()
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = false
|
||||
self.didStart = false
|
||||
self.bootstrapMonitorStarted = false
|
||||
// Note: Don't clear startedAt - it will be set fresh on next start
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func shutdownCompletely() {
|
||||
SecureLogger.debug("TorManager: shutdownCompletely() called", category: .session)
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
_ = arti_stop()
|
||||
|
||||
// Wait for shutdown
|
||||
var waited = 0
|
||||
while arti_is_running() != 0 && waited < 50 {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
waited += 1
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = false
|
||||
self.didStart = false
|
||||
self.restarting = false
|
||||
self.bootstrapMonitorStarted = false
|
||||
// Note: Don't clear startedAt here - it will be set fresh on next startIfNeeded()
|
||||
// Clearing it here races with startup and defeats the grace period
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func wakeFromDormant() async -> Bool {
|
||||
let wasDormant = await MainActor.run { self.isDormant }
|
||||
if !wasDormant { return false }
|
||||
|
||||
let result = arti_wake()
|
||||
if result != 0 { return false }
|
||||
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isStarting = true
|
||||
self.socksReady = false
|
||||
}
|
||||
|
||||
let ready = await waitForSocksReady(timeout: 12.0)
|
||||
await MainActor.run {
|
||||
self.socksReady = ready
|
||||
self.isStarting = !ready
|
||||
}
|
||||
|
||||
if ready {
|
||||
SecureLogger.info("TorManager: woke from dormant", category: .session)
|
||||
}
|
||||
return ready
|
||||
}
|
||||
|
||||
private func restartArti() async {
|
||||
SecureLogger.debug("TorManager: restartArti() starting", category: .session)
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .TorWillRestart, object: nil)
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = true
|
||||
self.isDormant = false
|
||||
self.lastRestartAt = Date()
|
||||
}
|
||||
|
||||
_ = arti_stop()
|
||||
|
||||
// Wait for stop
|
||||
var waited = 0
|
||||
while arti_is_running() != 0 && waited < 40 {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
waited += 1
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.bootstrapMonitorStarted = false
|
||||
self.didStart = false
|
||||
}
|
||||
|
||||
await MainActor.run { self.startIfNeeded() }
|
||||
}
|
||||
|
||||
private func recomputeReady() {
|
||||
let ready = socksReady && bootstrapProgress >= 100
|
||||
if ready != isReady {
|
||||
if !ready {
|
||||
SecureLogger.debug("TorManager: isReady -> false (socksReady=\(socksReady), bootstrap=\(bootstrapProgress))", category: .session)
|
||||
}
|
||||
isReady = ready
|
||||
if ready {
|
||||
NotificationCenter.default.post(name: .TorDidBecomeReady, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startPathMonitorIfNeeded() {
|
||||
#if canImport(Network)
|
||||
guard pathMonitor == nil else { return }
|
||||
let monitor = NWPathMonitor()
|
||||
pathMonitor = monitor
|
||||
let queue = DispatchQueue(label: "TorPathMonitor")
|
||||
monitor.pathUpdateHandler = { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
if self.isAppForeground {
|
||||
self.pokeTorOnPathChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func pokeTorOnPathChange() {
|
||||
// Skip if we recently restarted
|
||||
if let last = lastRestartAt, Date().timeIntervalSince(last) < 3.0 {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - recent restart", category: .session)
|
||||
return
|
||||
}
|
||||
// Skip during initial startup grace period (15s) to avoid race conditions
|
||||
if let started = startedAt, Date().timeIntervalSince(started) < 15.0 {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - startup grace period (\(Int(Date().timeIntervalSince(started)))s)", category: .session)
|
||||
return
|
||||
}
|
||||
if isStarting || restarting {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - isStarting=\(isStarting) restarting=\(restarting)", category: .session)
|
||||
return
|
||||
}
|
||||
if isReady { return }
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() - Arti not ready, initiating recovery", category: .session)
|
||||
ensureRunningOnForeground()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Start policy configuration
|
||||
extension TorManager {
|
||||
@MainActor
|
||||
public func setAutoStartAllowed(_ allow: Bool) {
|
||||
allowAutoStart = allow
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func isAutoStartAllowed() -> Bool { allowAutoStart }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
public extension Notification.Name {
|
||||
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
|
||||
static let TorWillRestart = Notification.Name("TorWillRestart")
|
||||
static let TorWillStart = Notification.Name("TorWillStart")
|
||||
static let TorUserPreferenceChanged = Notification.Name("TorUserPreferenceChanged")
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import CFNetwork
|
||||
#endif
|
||||
|
||||
/// Provides a shared URLSession that routes traffic via Tor's SOCKS5 proxy
|
||||
/// when Tor is enforced/ready. Allows swapping between proxied and direct
|
||||
/// sessions so UI can toggle Tor usage at runtime.
|
||||
public final class TorURLSession {
|
||||
public static let shared = TorURLSession()
|
||||
|
||||
// Default (no proxy) session for direct Nostr access when Tor is disabled.
|
||||
private var defaultSession: URLSession = TorURLSession.makeDefaultSession()
|
||||
|
||||
// Proxied (SOCKS5) session that routes through Tor.
|
||||
private var torSession: URLSession = TorURLSession.makeTorSession()
|
||||
private var useTorProxy: Bool = true
|
||||
|
||||
public var session: URLSession {
|
||||
useTorProxy ? torSession : defaultSession
|
||||
}
|
||||
|
||||
// Recreate sessions so new clients bind to the fresh SOCKS/control ports after a Tor restart.
|
||||
public func rebuild() {
|
||||
defaultSession = TorURLSession.makeDefaultSession()
|
||||
torSession = TorURLSession.makeTorSession()
|
||||
}
|
||||
|
||||
public func setProxyMode(useTor: Bool) {
|
||||
guard useTorProxy != useTor else { return }
|
||||
useTorProxy = useTor
|
||||
rebuild()
|
||||
}
|
||||
|
||||
private static func makeTorSession() -> URLSession {
|
||||
let cfg = URLSessionConfiguration.ephemeral
|
||||
cfg.waitsForConnectivity = true
|
||||
// Keep in sync with TorManager defaults
|
||||
let host = "127.0.0.1"
|
||||
let port = 39050
|
||||
#if os(macOS)
|
||||
cfg.connectionProxyDictionary = [
|
||||
kCFNetworkProxiesSOCKSEnable as String: 1,
|
||||
kCFNetworkProxiesSOCKSProxy as String: host,
|
||||
kCFNetworkProxiesSOCKSPort as String: port
|
||||
]
|
||||
#else
|
||||
// iOS: CFNetwork SOCKS proxy keys are unavailable at compile time.
|
||||
cfg.connectionProxyDictionary = [
|
||||
"SOCKSEnable": 1,
|
||||
"SOCKSProxy": host,
|
||||
"SOCKSPort": port
|
||||
]
|
||||
#endif
|
||||
return URLSession(configuration: cfg)
|
||||
}
|
||||
|
||||
private static func makeDefaultSession() -> URLSession {
|
||||
let cfg = URLSessionConfiguration.default
|
||||
cfg.waitsForConnectivity = true
|
||||
return URLSession(configuration: cfg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "arti-bitchat"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Arti core - minimal features for client-only SOCKS proxy
|
||||
arti-client = { version = "0.38", default-features = false, features = [
|
||||
"tokio",
|
||||
"rustls",
|
||||
] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"rt-multi-thread",
|
||||
"net",
|
||||
"sync",
|
||||
"time",
|
||||
"macros",
|
||||
] }
|
||||
|
||||
# Tor runtime compatibility
|
||||
tor-rtcompat = { version = "0.38", default-features = false, features = ["tokio"] }
|
||||
|
||||
# FFI utilities
|
||||
libc = "0.2"
|
||||
once_cell = "1"
|
||||
|
||||
# Logging (minimal)
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -0,0 +1,13 @@
|
||||
language = "C"
|
||||
include_guard = "ARTI_H"
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stdbool.h"]
|
||||
|
||||
[export]
|
||||
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
|
||||
|
||||
[fn]
|
||||
args = "Auto"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
@@ -0,0 +1,315 @@
|
||||
//! arti-bitchat: Minimal FFI wrapper around arti-client for BitChat
|
||||
//!
|
||||
//! Provides a C-compatible interface for embedding Arti (Rust Tor) in iOS/macOS apps.
|
||||
//! Exposes a SOCKS5 proxy on localhost that Swift code can route traffic through.
|
||||
|
||||
use std::ffi::{c_char, c_int, CStr};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use arti_client::TorClient;
|
||||
use once_cell::sync::OnceCell;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::oneshot;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
mod socks;
|
||||
|
||||
/// Global state for the Arti instance
|
||||
struct ArtiState {
|
||||
/// Tokio runtime (owned, single instance)
|
||||
runtime: Runtime,
|
||||
/// Shutdown signal sender
|
||||
shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
/// TorClient handle for status queries
|
||||
client: Option<Arc<TorClient<PreferredRuntime>>>,
|
||||
}
|
||||
|
||||
static ARTI_STATE: OnceCell<Mutex<ArtiState>> = OnceCell::new();
|
||||
static BOOTSTRAP_PROGRESS: AtomicI32 = AtomicI32::new(0);
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
static BOOTSTRAP_SUMMARY: Mutex<String> = Mutex::new(String::new());
|
||||
|
||||
/// Initialize the global state with a new runtime
|
||||
fn init_state() -> Result<(), &'static str> {
|
||||
ARTI_STATE.get_or_try_init(|| -> Result<Mutex<ArtiState>, &'static str> {
|
||||
let runtime = Runtime::new().map_err(|_| "Failed to create tokio runtime")?;
|
||||
Ok(Mutex::new(ArtiState {
|
||||
runtime,
|
||||
shutdown_tx: None,
|
||||
client: None,
|
||||
}))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start Arti with a SOCKS5 proxy.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data_dir` - Path to data directory for Tor state (C string)
|
||||
/// * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if already running
|
||||
/// * -2 if data_dir is invalid
|
||||
/// * -3 if runtime initialization failed
|
||||
/// * -4 if bootstrap failed
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_start(data_dir: *const c_char, socks_port: u16) -> c_int {
|
||||
// Check if already running
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse data directory
|
||||
let data_path = match unsafe { CStr::from_ptr(data_dir) }.to_str() {
|
||||
Ok(s) => PathBuf::from(s),
|
||||
Err(_) => return -2,
|
||||
};
|
||||
|
||||
// Initialize runtime if needed
|
||||
if let Err(_) = init_state() {
|
||||
return -3;
|
||||
}
|
||||
|
||||
let state = match ARTI_STATE.get() {
|
||||
Some(s) => s,
|
||||
None => return -3,
|
||||
};
|
||||
|
||||
let mut guard = match state.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return -3,
|
||||
};
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
guard.shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port)
|
||||
.parse()
|
||||
.expect("valid addr");
|
||||
|
||||
// Spawn the main Arti task
|
||||
let data_path_clone = data_path.clone();
|
||||
guard.runtime.spawn(async move {
|
||||
match run_arti(data_path_clone, socks_addr, shutdown_rx).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Arti shutdown cleanly");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Arti error: {}", e);
|
||||
update_summary(&format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
IS_RUNNING.store(false, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
update_summary("Starting...");
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Stop Arti gracefully.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_stop() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let state = match ARTI_STATE.get() {
|
||||
Some(s) => s,
|
||||
None => return -1,
|
||||
};
|
||||
|
||||
let mut guard = match state.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
// Send shutdown signal
|
||||
if let Some(tx) = guard.shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
// Clear client reference
|
||||
guard.client = None;
|
||||
|
||||
// Give async tasks time to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
|
||||
IS_RUNNING.store(false, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
update_summary("");
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Check if Arti is currently running.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 1 if running
|
||||
/// * 0 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_is_running() -> c_int {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current bootstrap progress (0-100).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_bootstrap_progress() -> c_int {
|
||||
BOOTSTRAP_PROGRESS.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Get the current bootstrap summary string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buf` - Buffer to write the summary into
|
||||
/// * `len` - Length of the buffer
|
||||
///
|
||||
/// # Returns
|
||||
/// * Number of bytes written (not including null terminator)
|
||||
/// * -1 if buffer is null or too small
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_bootstrap_summary(buf: *mut c_char, len: c_int) -> c_int {
|
||||
if buf.is_null() || len <= 0 {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let summary = match BOOTSTRAP_SUMMARY.lock() {
|
||||
Ok(s) => s.clone(),
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
let bytes = summary.as_bytes();
|
||||
let copy_len = std::cmp::min(bytes.len(), (len - 1) as usize);
|
||||
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
|
||||
*buf.add(copy_len) = 0; // null terminator
|
||||
}
|
||||
|
||||
copy_len as c_int
|
||||
}
|
||||
|
||||
/// Signal Arti to go dormant (reduce resource usage).
|
||||
/// This is a hint; Arti may not fully support dormant mode yet.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_go_dormant() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
// Arti doesn't have explicit dormant mode yet, but we can note the intent
|
||||
update_summary("Dormant");
|
||||
0
|
||||
}
|
||||
|
||||
/// Signal Arti to wake from dormant mode.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_wake() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
update_summary("Active");
|
||||
0
|
||||
}
|
||||
|
||||
fn update_summary(s: &str) {
|
||||
if let Ok(mut guard) = BOOTSTRAP_SUMMARY.lock() {
|
||||
guard.clear();
|
||||
guard.push_str(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main async entry point for Arti
|
||||
async fn run_arti(
|
||||
data_dir: PathBuf,
|
||||
socks_addr: SocketAddr,
|
||||
mut shutdown_rx: oneshot::Receiver<()>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Ensure data directory exists
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
|
||||
update_summary("Configuring...");
|
||||
|
||||
// Build Arti configuration with custom directories
|
||||
let cache_dir = data_dir.join("cache");
|
||||
let state_dir = data_dir.join("state");
|
||||
|
||||
// Use from_directories which sets up storage correctly
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
|
||||
.build()?;
|
||||
|
||||
update_summary("Bootstrapping...");
|
||||
|
||||
// Create and bootstrap the Tor client
|
||||
let client = TorClient::create_bootstrapped(config).await?;
|
||||
let client = Arc::new(client);
|
||||
|
||||
// Store client reference for status queries
|
||||
if let Some(state) = ARTI_STATE.get() {
|
||||
if let Ok(mut guard) = state.lock() {
|
||||
guard.client = Some(client.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark bootstrap complete
|
||||
BOOTSTRAP_PROGRESS.store(100, Ordering::SeqCst);
|
||||
update_summary("Ready");
|
||||
|
||||
// Bind SOCKS listener
|
||||
let listener = TcpListener::bind(socks_addr).await?;
|
||||
tracing::info!("SOCKS5 proxy listening on {}", socks_addr);
|
||||
|
||||
// Accept connections until shutdown
|
||||
loop {
|
||||
tokio::select! {
|
||||
accept_result = listener.accept() => {
|
||||
match accept_result {
|
||||
Ok((stream, peer_addr)) => {
|
||||
let client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = socks::handle_socks_connection(stream, peer_addr, client).await {
|
||||
tracing::debug!("SOCKS connection error from {}: {}", peer_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Accept error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut shutdown_rx => {
|
||||
tracing::info!("Shutdown signal received");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_summary("Shutting down...");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! SOCKS5 protocol handler for Arti
|
||||
//!
|
||||
//! Implements a minimal SOCKS5 server that forwards connections through Tor.
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arti_client::{TorClient, IntoTorAddr};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
// SOCKS5 constants
|
||||
const SOCKS5_VERSION: u8 = 0x05;
|
||||
const SOCKS5_AUTH_NONE: u8 = 0x00;
|
||||
const SOCKS5_CMD_CONNECT: u8 = 0x01;
|
||||
const SOCKS5_ATYP_IPV4: u8 = 0x01;
|
||||
const SOCKS5_ATYP_DOMAIN: u8 = 0x03;
|
||||
const SOCKS5_ATYP_IPV6: u8 = 0x04;
|
||||
const SOCKS5_REP_SUCCESS: u8 = 0x00;
|
||||
const SOCKS5_REP_FAILURE: u8 = 0x01;
|
||||
const SOCKS5_REP_CONN_REFUSED: u8 = 0x05;
|
||||
|
||||
/// Handle a single SOCKS5 connection
|
||||
pub async fn handle_socks_connection(
|
||||
mut stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
client: Arc<TorClient<PreferredRuntime>>,
|
||||
) -> io::Result<()> {
|
||||
// --- Greeting ---
|
||||
// Client sends: VER | NMETHODS | METHODS
|
||||
let mut greeting = [0u8; 2];
|
||||
stream.read_exact(&mut greeting).await?;
|
||||
|
||||
if greeting[0] != SOCKS5_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Not SOCKS5",
|
||||
));
|
||||
}
|
||||
|
||||
let nmethods = greeting[1] as usize;
|
||||
let mut methods = vec![0u8; nmethods];
|
||||
stream.read_exact(&mut methods).await?;
|
||||
|
||||
// We only support no-auth
|
||||
if !methods.contains(&SOCKS5_AUTH_NONE) {
|
||||
// Send failure: no acceptable methods
|
||||
stream.write_all(&[SOCKS5_VERSION, 0xFF]).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"No acceptable auth methods",
|
||||
));
|
||||
}
|
||||
|
||||
// Accept no-auth
|
||||
stream.write_all(&[SOCKS5_VERSION, SOCKS5_AUTH_NONE]).await?;
|
||||
|
||||
// --- Request ---
|
||||
// Client sends: VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT
|
||||
let mut request_header = [0u8; 4];
|
||||
stream.read_exact(&mut request_header).await?;
|
||||
|
||||
if request_header[0] != SOCKS5_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid SOCKS5 request version",
|
||||
));
|
||||
}
|
||||
|
||||
let cmd = request_header[1];
|
||||
let atyp = request_header[3];
|
||||
|
||||
if cmd != SOCKS5_CMD_CONNECT {
|
||||
// We only support CONNECT
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Only CONNECT supported",
|
||||
));
|
||||
}
|
||||
|
||||
// Parse destination address
|
||||
let (dest_host, dest_port) = match atyp {
|
||||
SOCKS5_ATYP_IPV4 => {
|
||||
let mut addr = [0u8; 4];
|
||||
stream.read_exact(&mut addr).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
let host = format!("{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3]);
|
||||
(host, port)
|
||||
}
|
||||
SOCKS5_ATYP_DOMAIN => {
|
||||
let mut len_buf = [0u8; 1];
|
||||
stream.read_exact(&mut len_buf).await?;
|
||||
let len = len_buf[0] as usize;
|
||||
let mut domain = vec![0u8; len];
|
||||
stream.read_exact(&mut domain).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
let host = String::from_utf8_lossy(&domain).to_string();
|
||||
(host, port)
|
||||
}
|
||||
SOCKS5_ATYP_IPV6 => {
|
||||
let mut addr = [0u8; 16];
|
||||
stream.read_exact(&mut addr).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
// Format IPv6 address
|
||||
let segments: Vec<String> = addr
|
||||
.chunks(2)
|
||||
.map(|c| format!("{:02x}{:02x}", c[0], c[1]))
|
||||
.collect();
|
||||
let host = format!("[{}]", segments.join(":"));
|
||||
(host, port)
|
||||
}
|
||||
_ => {
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Unsupported address type",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("SOCKS5 CONNECT from {} to {}:{}", peer_addr, dest_host, dest_port);
|
||||
|
||||
// Connect through Tor
|
||||
let tor_addr = format!("{}:{}", dest_host, dest_port);
|
||||
let tor_addr = match tor_addr.as_str().into_tor_addr() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::debug!("Invalid Tor address: {}", e);
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("Invalid Tor address: {}", e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let tor_stream = match client.connect(tor_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::debug!("Tor connect failed: {}", e);
|
||||
send_reply(&mut stream, SOCKS5_REP_CONN_REFUSED).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Send success reply
|
||||
// Reply: VER | REP | RSV | ATYP | BND.ADDR | BND.PORT
|
||||
// We use 0.0.0.0:0 as the bound address since we're proxying
|
||||
let reply = [
|
||||
SOCKS5_VERSION,
|
||||
SOCKS5_REP_SUCCESS,
|
||||
0x00, // RSV
|
||||
SOCKS5_ATYP_IPV4,
|
||||
0, 0, 0, 0, // BND.ADDR
|
||||
0, 0, // BND.PORT
|
||||
];
|
||||
stream.write_all(&reply).await?;
|
||||
|
||||
// Bidirectional copy
|
||||
let (mut client_read, mut client_write) = stream.into_split();
|
||||
let (mut tor_read, mut tor_write) = tor_stream.split();
|
||||
|
||||
let client_to_tor = async {
|
||||
tokio::io::copy(&mut client_read, &mut tor_write).await
|
||||
};
|
||||
let tor_to_client = async {
|
||||
tokio::io::copy(&mut tor_read, &mut client_write).await
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
result = client_to_tor => {
|
||||
if let Err(e) = result {
|
||||
tracing::debug!("Client to Tor copy error: {}", e);
|
||||
}
|
||||
}
|
||||
result = tor_to_client => {
|
||||
if let Err(e) = result {
|
||||
tracing::debug!("Tor to client copy error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_reply(stream: &mut TcpStream, rep: u8) -> io::Result<()> {
|
||||
let reply = [
|
||||
SOCKS5_VERSION,
|
||||
rep,
|
||||
0x00, // RSV
|
||||
SOCKS5_ATYP_IPV4,
|
||||
0, 0, 0, 0, // BND.ADDR
|
||||
0, 0, // BND.PORT
|
||||
];
|
||||
stream.write_all(&reply).await
|
||||
}
|
||||
Executable
+311
@@ -0,0 +1,311 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build arti-bitchat for iOS/macOS with aggressive size optimization
|
||||
#
|
||||
# Output: Frameworks/arti.xcframework containing static libraries for:
|
||||
# - aarch64-apple-ios (iOS device)
|
||||
# - aarch64-apple-ios-sim (iOS simulator, Apple Silicon)
|
||||
# - x86_64-apple-ios (iOS simulator, Intel - optional)
|
||||
# - aarch64-apple-darwin (macOS)
|
||||
#
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Configuration
|
||||
CRATE_NAME="arti-bitchat"
|
||||
LIB_NAME="libarti_bitchat.a"
|
||||
FRAMEWORK_NAME="arti"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/Frameworks"
|
||||
|
||||
# Targets to build
|
||||
TARGETS=(
|
||||
"aarch64-apple-ios" # iOS device
|
||||
"aarch64-apple-ios-sim" # iOS simulator (Apple Silicon)
|
||||
"aarch64-apple-darwin" # macOS
|
||||
)
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
if ! command -v rustc &> /dev/null; then
|
||||
log_error "Rust is not installed. Please install via rustup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
log_error "Cargo is not installed. Please install via rustup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check/install targets
|
||||
for target in "${TARGETS[@]}"; do
|
||||
if ! rustup target list --installed | grep -q "$target"; then
|
||||
log_info "Installing target: $target"
|
||||
rustup target add "$target"
|
||||
fi
|
||||
done
|
||||
|
||||
# Install cbindgen if needed
|
||||
if ! command -v cbindgen &> /dev/null; then
|
||||
log_info "Installing cbindgen..."
|
||||
cargo install cbindgen
|
||||
fi
|
||||
|
||||
log_info "Prerequisites OK"
|
||||
}
|
||||
|
||||
# Set up aggressive size optimization flags and deployment targets
|
||||
setup_rustflags() {
|
||||
local target="$1"
|
||||
|
||||
# Base flags for size optimization
|
||||
export RUSTFLAGS="-C opt-level=z -C lto=fat -C codegen-units=1 -C panic=abort -C strip=symbols"
|
||||
|
||||
# Set deployment targets to suppress linker warnings about version mismatches
|
||||
case "$target" in
|
||||
*-apple-ios-sim*)
|
||||
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
|
||||
# Simulator uses iPhone SDK but needs the sim target
|
||||
;;
|
||||
*-apple-ios*)
|
||||
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
|
||||
;;
|
||||
*-apple-darwin*)
|
||||
export MACOSX_DEPLOYMENT_TARGET="13.0"
|
||||
;;
|
||||
esac
|
||||
|
||||
log_info "RUSTFLAGS: $RUSTFLAGS"
|
||||
log_info "Deployment target: MACOSX=$MACOSX_DEPLOYMENT_TARGET IPHONEOS=$IPHONEOS_DEPLOYMENT_TARGET"
|
||||
}
|
||||
|
||||
# Build for a single target
|
||||
build_target() {
|
||||
local target="$1"
|
||||
log_info "Building for target: $target"
|
||||
|
||||
setup_rustflags "$target"
|
||||
|
||||
# Build release
|
||||
cargo build --release --target "$target" -p "$CRATE_NAME"
|
||||
|
||||
# Check output
|
||||
local lib_path="target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
local size=$(du -h "$lib_path" | cut -f1)
|
||||
log_info "Built $lib_path ($size)"
|
||||
else
|
||||
log_error "Build failed: $lib_path not found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create xcframework from built libraries
|
||||
create_xcframework() {
|
||||
log_info "Creating xcframework..."
|
||||
|
||||
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
|
||||
# Remove existing xcframework
|
||||
rm -rf "$xcframework_path"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Build the xcodebuild command
|
||||
local cmd="xcodebuild -create-xcframework"
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
# Strip the library for additional size reduction
|
||||
log_info "Stripping $target library..."
|
||||
strip -x "$lib_path" 2>/dev/null || true
|
||||
|
||||
cmd="$cmd -library $lib_path"
|
||||
|
||||
# Add headers if they exist
|
||||
local header_dir="$OUTPUT_DIR/include"
|
||||
if [[ -d "$header_dir" ]]; then
|
||||
cmd="$cmd -headers $header_dir"
|
||||
fi
|
||||
else
|
||||
log_warn "Skipping missing library: $lib_path"
|
||||
fi
|
||||
done
|
||||
|
||||
cmd="$cmd -output $xcframework_path"
|
||||
|
||||
log_info "Running: $cmd"
|
||||
eval "$cmd"
|
||||
|
||||
if [[ -d "$xcframework_path" ]]; then
|
||||
local size=$(du -sh "$xcframework_path" | cut -f1)
|
||||
log_info "Created $xcframework_path ($size)"
|
||||
else
|
||||
log_error "Failed to create xcframework"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate C header using cbindgen
|
||||
generate_header() {
|
||||
log_info "Generating C header..."
|
||||
|
||||
local header_dir="$OUTPUT_DIR/include"
|
||||
local header_path="$header_dir/arti.h"
|
||||
|
||||
mkdir -p "$header_dir"
|
||||
|
||||
# Create cbindgen.toml if it doesn't exist
|
||||
if [[ ! -f "$CRATE_NAME/cbindgen.toml" ]]; then
|
||||
cat > "$CRATE_NAME/cbindgen.toml" << 'EOF'
|
||||
language = "C"
|
||||
include_guard = "ARTI_H"
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stdbool.h"]
|
||||
|
||||
[export]
|
||||
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
|
||||
|
||||
[fn]
|
||||
args = "Auto"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
EOF
|
||||
fi
|
||||
|
||||
cbindgen --config "$CRATE_NAME/cbindgen.toml" \
|
||||
--crate "$CRATE_NAME" \
|
||||
--output "$header_path"
|
||||
|
||||
if [[ -f "$header_path" ]]; then
|
||||
log_info "Generated $header_path"
|
||||
cat "$header_path"
|
||||
else
|
||||
log_warn "cbindgen did not generate header, creating manually..."
|
||||
# Fallback: create header manually
|
||||
cat > "$header_path" << 'EOF'
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* @param data_dir Path to data directory for Tor state (C string)
|
||||
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
|
||||
* @return 0 on success, negative on error
|
||||
*/
|
||||
int32_t arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* @return 1 if running, 0 if not running
|
||||
*/
|
||||
int32_t arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*
|
||||
* @return Progress percentage
|
||||
*/
|
||||
int32_t arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* @param buf Buffer to write the summary into
|
||||
* @param len Length of the buffer
|
||||
* @return Number of bytes written, -1 on error
|
||||
*/
|
||||
int32_t arti_bootstrap_summary(char *buf, int32_t len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_wake(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ARTI_H */
|
||||
EOF
|
||||
log_info "Created manual header at $header_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print size report
|
||||
print_size_report() {
|
||||
log_info "=== Size Report ==="
|
||||
for target in "${TARGETS[@]}"; do
|
||||
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
local size=$(du -h "$lib_path" | cut -f1)
|
||||
echo " $target: $size"
|
||||
fi
|
||||
done
|
||||
|
||||
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
if [[ -d "$xcframework_path" ]]; then
|
||||
local total_size=$(du -sh "$xcframework_path" | cut -f1)
|
||||
echo " xcframework total: $total_size"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
log_info "Building arti-bitchat for iOS/macOS"
|
||||
log_info "=================================="
|
||||
|
||||
check_prerequisites
|
||||
generate_header
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
build_target "$target"
|
||||
done
|
||||
|
||||
create_xcframework
|
||||
print_size_report
|
||||
|
||||
log_info "Build complete!"
|
||||
log_info "xcframework: $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
}
|
||||
|
||||
# Run
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user