mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 00:25:19 +00:00
Embed Tor via tor_api; deterministic restart + Nostr gating; add Tor notifications
- Run Tor via tor_api in a dedicated thread with OwningControllerFD - Cleanly stop Tor on background; restart on .active (single instance) - Avoid fallback to tor_main/dlopen; add is-running check to prevent duplicates - Fix argv lifetime in C glue to avoid strcmp crash on start - Gate Nostr connect/subscribe/send until Tor is fully ready - Rebuild URLSession + reset relays after Tor readiness (scene-based) - Remove TorDidBecomeReady double-reset and appDidBecomeActive resubscribe - Add TorWillRestart/TorDidBecomeReady notifications and chat system messages - Debounce path-change restarts; ACTIVE poke first; coalesce subs; cancel stale reconnect timers - Project: add CTorHost.c and TorNotifications.swift to targets; fix libz.tbd path
This commit is contained in:
@@ -39,6 +39,10 @@
|
||||
0481A3592E6D929E00FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */; };
|
||||
0481A35B2E6D9BEF00FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35A2E6D9BEF00FC845E /* libz.tbd */; };
|
||||
0481A35D2E6DA18600FC845E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
||||
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */; };
|
||||
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */; };
|
||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
048A4BE92E5CCCC300162C4B /* TransportConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A4BE62E5CCCC300162C4A /* TransportConfig.swift */; };
|
||||
@@ -220,10 +224,12 @@
|
||||
047502B82E560F690083520F /* RelayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayController.swift; sourceTree = "<group>"; };
|
||||
0481A3432E6D869F00FC845E /* TorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorManager.swift; sourceTree = "<group>"; };
|
||||
0481A3442E6D869F00FC845E /* TorURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorURLSession.swift; sourceTree = "<group>"; };
|
||||
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CTorHost.c; sourceTree = "<group>"; };
|
||||
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TorNotifications.swift; sourceTree = "<group>"; };
|
||||
0481A3532E6D877600FC845E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
0481A3572E6D929E00FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
||||
0481A35A2E6D9BEF00FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.5.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
048A4BE62E5CCCC300162C4A /* TransportConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportConfig.swift; sourceTree = "<group>"; };
|
||||
048A4C272E5FCD6600162C4A /* GeohashBookmarksStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStore.swift; sourceTree = "<group>"; };
|
||||
048A4C2A2E5FCE0300162C4A /* GeohashBookmarksStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeohashBookmarksStoreTests.swift; sourceTree = "<group>"; };
|
||||
@@ -343,6 +349,8 @@
|
||||
children = (
|
||||
0481A3432E6D869F00FC845E /* TorManager.swift */,
|
||||
0481A3442E6D869F00FC845E /* TorURLSession.swift */,
|
||||
0C0EFA102E6EAAAA00ABCDEF /* CTorHost.c */,
|
||||
0C0EFA132E6EAABB00ABCDF1 /* TorNotifications.swift */,
|
||||
);
|
||||
path = Tor;
|
||||
sourceTree = "<group>";
|
||||
@@ -794,6 +802,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C0EFA122E6EAAAA00ABCDF0 /* CTorHost.c in Sources */,
|
||||
0C0EFA172E6EAABB00ABCDF5 /* TorNotifications.swift in Sources */,
|
||||
048A4BE72E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
||||
1234567890ABCDEFFEDCBA13 /* PeerDisplayNameResolver.swift in Sources */,
|
||||
AA77BB12CC22DD33EE44FF56 /* VerificationService.swift in Sources */,
|
||||
@@ -856,6 +866,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C0EFA112E6EAAAA00ABCDEF /* CTorHost.c in Sources */,
|
||||
0C0EFA162E6EAABB00ABCDF4 /* TorNotifications.swift in Sources */,
|
||||
048A4BE82E5CCCC300162C4A /* TransportConfig.swift in Sources */,
|
||||
1234567890ABCDEFFEDCBA14 /* PeerDisplayNameResolver.swift in Sources */,
|
||||
AA77BB11CC22DD33EE44FF55 /* VerificationService.swift in Sources */,
|
||||
|
||||
@@ -57,17 +57,26 @@ struct BitchatApp: App {
|
||||
case .background:
|
||||
// Keep BLE mesh running in background; BLEService adapts scanning automatically
|
||||
// Optionally nudge Tor to dormant to save power
|
||||
TorManager.shared.setAppForeground(false)
|
||||
TorManager.shared.goDormantOnBackground()
|
||||
// Proactively disconnect Nostr to avoid spurious socket errors while Tor is down
|
||||
NostrRelayManager.shared.disconnect()
|
||||
break
|
||||
case .active:
|
||||
// Restart services when becoming active
|
||||
chatViewModel.meshService.startServices()
|
||||
// Ensure Tor is healthy; restart if suspended; wake ACTIVE
|
||||
TorManager.shared.setAppForeground(true)
|
||||
// Ensure Tor is healthy; restart deterministically, then wait until ready
|
||||
TorManager.shared.ensureRunningOnForeground()
|
||||
// Rebuild proxied sessions to bind to live Tor
|
||||
TorURLSession.shared.rebuild()
|
||||
// Reconnect Nostr via fresh sessions; will gate until Tor 100%
|
||||
NostrRelayManager.shared.resetAllConnections()
|
||||
Task.detached {
|
||||
let _ = await TorManager.shared.awaitReady(timeout: 60)
|
||||
await MainActor.run {
|
||||
// Rebuild proxied sessions to bind to the live Tor after readiness
|
||||
TorURLSession.shared.rebuild()
|
||||
// Reconnect Nostr via fresh sessions; will gate until Tor 100%
|
||||
NostrRelayManager.shared.resetAllConnections()
|
||||
}
|
||||
}
|
||||
checkForSharedContent()
|
||||
case .inactive:
|
||||
break
|
||||
|
||||
@@ -42,6 +42,8 @@ class NostrRelayManager: ObservableObject {
|
||||
private var subscriptions: [String: Set<String>] = [:] // relay URL -> active subscription IDs
|
||||
private var pendingSubscriptions: [String: [String: String]] = [:] // relay URL -> (subscription id -> encoded REQ JSON)
|
||||
private var messageHandlers: [String: (NostrEvent) -> Void] = [:]
|
||||
// Coalesce duplicate subscribe requests for the same id within a short window
|
||||
private var subscribeCoalesce: [String: Date] = [:]
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// Message queue for reliability
|
||||
@@ -63,6 +65,8 @@ class NostrRelayManager: ObservableObject {
|
||||
|
||||
// Reconnection timer
|
||||
private var reconnectionTimer: Timer?
|
||||
// Bump generation to invalidate scheduled reconnects when we reset/disconnect
|
||||
private var connectionGeneration: Int = 0
|
||||
|
||||
init() {
|
||||
// Initialize with default relays
|
||||
@@ -91,15 +95,28 @@ class NostrRelayManager: ObservableObject {
|
||||
|
||||
/// Disconnect from all relays
|
||||
func disconnect() {
|
||||
connectionGeneration &+= 1
|
||||
for (_, task) in connections {
|
||||
task.cancel(with: .goingAway, reason: nil)
|
||||
}
|
||||
connections.removeAll()
|
||||
// Clear known subscriptions and any queued subs since connections are gone
|
||||
subscriptions.removeAll()
|
||||
pendingSubscriptions.removeAll()
|
||||
updateConnectionStatus()
|
||||
}
|
||||
|
||||
/// Ensure connections exist to the given relay URLs (idempotent).
|
||||
func ensureConnections(to relayUrls: [String]) {
|
||||
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
||||
// Defer until Tor is fully ready; avoid queuing connection attempts early
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let ready = await TorManager.shared.awaitReady()
|
||||
await MainActor.run { if ready { self.ensureConnections(to: relayUrls) } }
|
||||
}
|
||||
return
|
||||
}
|
||||
let existing = Set(relays.map { $0.url })
|
||||
for url in Set(relayUrls) {
|
||||
if !existing.contains(url) {
|
||||
@@ -113,6 +130,15 @@ class NostrRelayManager: ObservableObject {
|
||||
|
||||
/// Send an event to specified relays (or all if none specified)
|
||||
func sendEvent(_ event: NostrEvent, to relayUrls: [String]? = nil) {
|
||||
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
||||
// Defer sends until Tor is ready to avoid premature queueing
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let ready = await TorManager.shared.awaitReady()
|
||||
await MainActor.run { if ready { self.sendEvent(event, to: relayUrls) } }
|
||||
}
|
||||
return
|
||||
}
|
||||
let targetRelays = relayUrls ?? Self.defaultRelays
|
||||
ensureConnections(to: targetRelays)
|
||||
|
||||
@@ -177,6 +203,27 @@ class NostrRelayManager: ObservableObject {
|
||||
relayUrls: [String]? = nil,
|
||||
handler: @escaping (NostrEvent) -> Void
|
||||
) {
|
||||
// Coalesce rapid duplicate subscribe requests only if a handler already exists
|
||||
let now = Date()
|
||||
if messageHandlers[id] != nil {
|
||||
if let last = subscribeCoalesce[id], now.timeIntervalSince(last) < 1.0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
subscribeCoalesce[id] = now
|
||||
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
||||
// Defer subscription setup until Tor is ready; avoid queuing subs early
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let ready = await TorManager.shared.awaitReady()
|
||||
await MainActor.run {
|
||||
if ready {
|
||||
self.subscribe(filter: filter, id: id, relayUrls: relayUrls, handler: handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
messageHandlers[id] = handler
|
||||
|
||||
SecureLogger.log("📡 Subscribing to Nostr filter id=\(id) kinds=\(filter.kinds ?? []) since=\(filter.since ?? 0)",
|
||||
@@ -248,6 +295,8 @@ class NostrRelayManager: ObservableObject {
|
||||
/// Unsubscribe from a subscription
|
||||
func unsubscribe(id: String) {
|
||||
messageHandlers.removeValue(forKey: id)
|
||||
// Allow immediate re-subscription by clearing coalescer timestamp
|
||||
subscribeCoalesce.removeValue(forKey: id)
|
||||
|
||||
let req = NostrRequest.close(id: id)
|
||||
let message = try? encoder.encode(req)
|
||||
@@ -274,6 +323,11 @@ class NostrRelayManager: ObservableObject {
|
||||
SecureLogger.log("Invalid relay URL: \(urlString)", category: SecureLogger.session, level: .warning)
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid initiating connections while app is backgrounded; we'll reconnect on foreground
|
||||
if TorManager.shared.torEnforced && !TorManager.shared.isForeground() {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if we already have a connection object
|
||||
if connections[urlString] != nil {
|
||||
@@ -528,9 +582,11 @@ class NostrRelayManager: ObservableObject {
|
||||
|
||||
|
||||
// Schedule reconnection with exponential backoff
|
||||
let gen = connectionGeneration
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInterval) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Ignore stale scheduled reconnects from a previous generation
|
||||
guard gen == self.connectionGeneration else { return }
|
||||
// Check if we should still reconnect (relay might have been removed)
|
||||
if self.relays.contains(where: { $0.url == relayUrl }) {
|
||||
self.connectToRelay(relayUrl)
|
||||
@@ -571,6 +627,8 @@ class NostrRelayManager: ObservableObject {
|
||||
/// Reset all relay connections
|
||||
func resetAllConnections() {
|
||||
disconnect()
|
||||
// New generation begins now
|
||||
connectionGeneration &+= 1
|
||||
|
||||
// Reset all relay states
|
||||
for index in relays.indices {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Forward declarations from Tor's public embedding API (tor_api.h)
|
||||
typedef struct tor_main_configuration_t tor_main_configuration_t;
|
||||
tor_main_configuration_t *tor_main_configuration_new(void);
|
||||
int tor_main_configuration_set_command_line(tor_main_configuration_t *cfg,
|
||||
int argc,
|
||||
char **argv);
|
||||
void tor_main_configuration_free(tor_main_configuration_t *cfg);
|
||||
int tor_run_main(const tor_main_configuration_t *);
|
||||
|
||||
static pthread_t tor_thread;
|
||||
static int tor_thread_started = 0;
|
||||
static int owning_fd_app = -1; // App side; close to trigger shutdown
|
||||
static int owning_fd_tor = -1; // Tor side; closed when tor exits
|
||||
|
||||
// Persisted copies of args for restart convenience
|
||||
static char data_dir_copy[PATH_MAX] = {0};
|
||||
static char socks_arg_copy[64] = {0};
|
||||
static char control_arg_copy[64] = {0};
|
||||
static char fd_arg_copy[16] = {0};
|
||||
|
||||
// Keep argv array alive as required by tor_api.h until tor_run_main finishes
|
||||
static char **argv_owned = NULL;
|
||||
static int argv_owned_argc = 0;
|
||||
|
||||
// Public: return nonzero if tor thread appears to be running
|
||||
int tor_host_is_running(void) {
|
||||
return (tor_thread_started != 0) && (owning_fd_tor != -1);
|
||||
}
|
||||
|
||||
static void *tor_thread_main(void *arg) {
|
||||
tor_main_configuration_t *cfg = (tor_main_configuration_t *)arg;
|
||||
int rc = tor_run_main(cfg); // blocks until tor exits
|
||||
tor_main_configuration_free(cfg);
|
||||
if (argv_owned) { free(argv_owned); argv_owned = NULL; argv_owned_argc = 0; }
|
||||
if (owning_fd_tor != -1) { close(owning_fd_tor); owning_fd_tor = -1; }
|
||||
return (void*)(intptr_t)rc;
|
||||
}
|
||||
|
||||
static int build_cfg(tor_main_configuration_t **out_cfg) {
|
||||
tor_main_configuration_t *cfg = tor_main_configuration_new();
|
||||
if (!cfg) return -1;
|
||||
|
||||
// Create a private socketpair; give one end to tor as its owning controller
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) {
|
||||
tor_main_configuration_free(cfg);
|
||||
return -2;
|
||||
}
|
||||
owning_fd_app = sv[0];
|
||||
owning_fd_tor = sv[1];
|
||||
|
||||
snprintf(fd_arg_copy, sizeof(fd_arg_copy), "%d", owning_fd_tor);
|
||||
|
||||
// Preferred: configure via command-line to avoid stale torrc issues
|
||||
const char *args[] = {
|
||||
"tor",
|
||||
"--DataDirectory", data_dir_copy,
|
||||
"--ClientOnly", "1",
|
||||
"--SocksPort", socks_arg_copy,
|
||||
"--ControlPort", control_arg_copy,
|
||||
"--CookieAuthentication", "1",
|
||||
"--AvoidDiskWrites", "1",
|
||||
"--MaxClientCircuitsPending", "8",
|
||||
"--__OwningControllerFD", fd_arg_copy,
|
||||
// Make client shutdown fast; don't linger
|
||||
"--ShutdownWaitLength", "0",
|
||||
NULL
|
||||
};
|
||||
int argc = 0; while (args[argc]) argc++;
|
||||
|
||||
// Allocate a stable argv array as required by tor_api.h
|
||||
char **argv = (char **)malloc(sizeof(char*) * (argc + 1));
|
||||
if (!argv) {
|
||||
tor_main_configuration_free(cfg);
|
||||
close(owning_fd_app); close(owning_fd_tor);
|
||||
owning_fd_app = owning_fd_tor = -1;
|
||||
return -3;
|
||||
}
|
||||
for (int i = 0; i <= argc; ++i) argv[i] = (char *)args[i];
|
||||
argv_owned = argv;
|
||||
argv_owned_argc = argc;
|
||||
|
||||
if (tor_main_configuration_set_command_line(cfg, argc, argv) != 0) {
|
||||
tor_main_configuration_free(cfg);
|
||||
free(argv);
|
||||
close(owning_fd_app); close(owning_fd_tor);
|
||||
owning_fd_app = owning_fd_tor = -1;
|
||||
return -3;
|
||||
}
|
||||
|
||||
*out_cfg = cfg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Starts tor in a dedicated thread using Tor's embedding API.
|
||||
// - data_dir: absolute path to DataDirectory
|
||||
// - socks_addr: e.g., "127.0.0.1:39050"
|
||||
// - control_addr: e.g., "127.0.0.1:39051"
|
||||
// - shutdown_fast: if nonzero, we configure a faster shutdown (already default here)
|
||||
int tor_host_start(const char *data_dir, const char *socks_addr, const char *control_addr, int shutdown_fast) {
|
||||
if (!data_dir || !socks_addr || !control_addr) return -1;
|
||||
if (strlen(data_dir) >= sizeof(data_dir_copy) || strlen(socks_addr) >= sizeof(socks_arg_copy) || strlen(control_addr) >= sizeof(control_arg_copy))
|
||||
return -2;
|
||||
|
||||
// Don't start if an instance appears to be running
|
||||
if (owning_fd_app != -1 || owning_fd_tor != -1) return -3;
|
||||
|
||||
// Store copies for potential restart
|
||||
strncpy(data_dir_copy, data_dir, sizeof(data_dir_copy)-1);
|
||||
data_dir_copy[sizeof(data_dir_copy)-1] = '\0';
|
||||
strncpy(socks_arg_copy, socks_addr, sizeof(socks_arg_copy)-1);
|
||||
socks_arg_copy[sizeof(socks_arg_copy)-1] = '\0';
|
||||
strncpy(control_arg_copy, control_addr, sizeof(control_arg_copy)-1);
|
||||
control_arg_copy[sizeof(control_arg_copy)-1] = '\0';
|
||||
|
||||
tor_main_configuration_t *cfg = NULL;
|
||||
int rc = build_cfg(&cfg);
|
||||
if (rc != 0) return rc;
|
||||
|
||||
(void)shutdown_fast; // already configured via argv
|
||||
|
||||
int prc = pthread_create(&tor_thread, NULL, tor_thread_main, cfg);
|
||||
if (prc != 0) {
|
||||
tor_main_configuration_free(cfg);
|
||||
if (argv_owned) { free(argv_owned); argv_owned = NULL; argv_owned_argc = 0; }
|
||||
close(owning_fd_app); close(owning_fd_tor);
|
||||
owning_fd_app = owning_fd_tor = -1;
|
||||
return -4;
|
||||
}
|
||||
tor_thread_started = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Triggers a clean shutdown by closing the app-side owning controller fd,
|
||||
// then joins the tor thread to ensure complete exit before returning.
|
||||
int tor_host_shutdown(void) {
|
||||
if (owning_fd_app != -1) { close(owning_fd_app); owning_fd_app = -1; }
|
||||
void *ret = NULL;
|
||||
if (tor_thread_started) {
|
||||
// Wait for tor_run_main to return
|
||||
pthread_join(tor_thread, &ret);
|
||||
tor_thread_started = 0;
|
||||
}
|
||||
return (int)(intptr_t)ret;
|
||||
}
|
||||
|
||||
// Convenience: shutdown then start again with the stored parameters.
|
||||
int tor_host_restart(const char *data_dir, const char *socks_addr, const char *control_addr, int shutdown_fast) {
|
||||
(void)data_dir; (void)socks_addr; (void)control_addr; (void)shutdown_fast;
|
||||
// If caller provided new args, refresh our copies
|
||||
if (data_dir && data_dir[0]) {
|
||||
strncpy(data_dir_copy, data_dir, sizeof(data_dir_copy)-1);
|
||||
data_dir_copy[sizeof(data_dir_copy)-1] = '\0';
|
||||
}
|
||||
if (socks_addr && socks_addr[0]) {
|
||||
strncpy(socks_arg_copy, socks_addr, sizeof(socks_arg_copy)-1);
|
||||
socks_arg_copy[sizeof(socks_arg_copy)-1] = '\0';
|
||||
}
|
||||
if (control_addr && control_addr[0]) {
|
||||
strncpy(control_arg_copy, control_addr, sizeof(control_arg_copy)-1);
|
||||
control_arg_copy[sizeof(control_arg_copy)-1] = '\0';
|
||||
}
|
||||
(void)tor_host_shutdown();
|
||||
return tor_host_start(data_dir_copy, socks_arg_copy, control_arg_copy, shutdown_fast);
|
||||
}
|
||||
@@ -6,6 +6,16 @@ import Darwin
|
||||
@_silgen_name("tor_main")
|
||||
private func tor_main_c(_ argc: Int32, _ argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?) -> Int32
|
||||
|
||||
// Preferred: tiny C glue that uses Tor's embedding API (tor_api.h)
|
||||
@_silgen_name("tor_host_start")
|
||||
private func tor_host_start(_ dataDir: UnsafePointer<CChar>, _ socksAddr: UnsafePointer<CChar>, _ controlAddr: UnsafePointer<CChar>, _ shutdownFast: Int32) -> Int32
|
||||
@_silgen_name("tor_host_shutdown")
|
||||
private func tor_host_shutdown() -> Int32
|
||||
@_silgen_name("tor_host_restart")
|
||||
private func tor_host_restart(_ dataDir: UnsafePointer<CChar>, _ socksAddr: UnsafePointer<CChar>, _ controlAddr: UnsafePointer<CChar>, _ shutdownFast: Int32) -> Int32
|
||||
@_silgen_name("tor_host_is_running")
|
||||
private func tor_host_is_running() -> Int32
|
||||
|
||||
/// Minimal Tor integration scaffold.
|
||||
/// - Boots a local Tor client (once integrated) and exposes a SOCKS5 proxy
|
||||
/// on 127.0.0.1:socksPort. All app networking should await readiness and
|
||||
@@ -56,12 +66,16 @@ final class TorManager: ObservableObject {
|
||||
private var didStart = false
|
||||
private var controlMonitorStarted = false
|
||||
private var pathMonitor: NWPathMonitor?
|
||||
private var isAppForeground: Bool = true
|
||||
private var lastRestartAt: Date? = nil
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
func startIfNeeded() {
|
||||
// Do not start in background; caller should wait for foreground
|
||||
guard isAppForeground else { return }
|
||||
guard !didStart else { return }
|
||||
didStart = true
|
||||
isStarting = true
|
||||
@@ -71,10 +85,21 @@ final class TorManager: ObservableObject {
|
||||
startPathMonitorIfNeeded()
|
||||
}
|
||||
|
||||
/// Called by app lifecycle to indicate foreground/background state.
|
||||
func setAppForeground(_ foreground: Bool) {
|
||||
isAppForeground = foreground
|
||||
}
|
||||
|
||||
/// Foreground state accessor for other @MainActor clients.
|
||||
func isForeground() -> Bool { isAppForeground }
|
||||
|
||||
/// Await Tor bootstrap to readiness. Returns true if network is permitted (Tor ready or dev bypass).
|
||||
/// Nonisolated to avoid blocking the main actor during waits.
|
||||
nonisolated func awaitReady(timeout: TimeInterval = 25.0) async -> Bool {
|
||||
await MainActor.run { self.startIfNeeded() }
|
||||
// Only start Tor if we're in foreground; otherwise just wait for it
|
||||
await MainActor.run {
|
||||
if self.isAppForeground { self.startIfNeeded() }
|
||||
}
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
// Early exit if network already permitted
|
||||
if await MainActor.run(body: { self.networkPermitted }) { return true }
|
||||
@@ -131,6 +156,7 @@ final class TorManager: ObservableObject {
|
||||
lines.append("CookieAuthentication 1")
|
||||
lines.append("AvoidDiskWrites 1")
|
||||
lines.append("MaxClientCircuitsPending 8")
|
||||
lines.append("ShutdownWaitLength 0")
|
||||
// Keep defaults for guard/exit selection to preserve anonymity properties
|
||||
return lines.joined(separator: "\n") + "\n"
|
||||
}
|
||||
@@ -140,11 +166,9 @@ final class TorManager: ObservableObject {
|
||||
/// Start the embedded Tor. This stub intentionally compiles without any Tor dependency.
|
||||
/// Integrate your Tor framework here and set `isReady = true` once bootstrapped.
|
||||
private func startTor() {
|
||||
// If linked statically (xcframework with static framework), call tor_run_main directly.
|
||||
if startTorViaLinkedSymbol() { return }
|
||||
|
||||
// Dynamic loading path is intended for dynamic frameworks only.
|
||||
if startTorViaDlopen() { return }
|
||||
// Prefer the embedding API wrapper which cleanly supports restart.
|
||||
// Avoid fallback to prevent accidental second instances in-process.
|
||||
if startTorViaEmbedAPI() { return }
|
||||
|
||||
#if BITCHAT_DEV_ALLOW_CLEARNET
|
||||
// Dev bypass: permit network immediately (no Tor). Use ONLY for local development.
|
||||
@@ -156,6 +180,50 @@ final class TorManager: ObservableObject {
|
||||
self.isStarting = false
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Embed API path (owning controller FD + clean restart)
|
||||
private func startTorViaEmbedAPI() -> Bool {
|
||||
guard let dir = dataDirectoryURL()?.path else { return false }
|
||||
let socks = "\(socksHost):\(socksPort)"
|
||||
let control = "\(controlHost):\(controlPort)"
|
||||
var started = false
|
||||
// If already running (per C glue), treat as started
|
||||
if tor_host_is_running() != 0 {
|
||||
SecureLogger.log("TorManager: embed reports already running", category: SecureLogger.session, level: .info)
|
||||
return true
|
||||
}
|
||||
dir.withCString { dptr in
|
||||
socks.withCString { sptr in
|
||||
control.withCString { cptr in
|
||||
let rc = tor_host_start(dptr, sptr, cptr, 1)
|
||||
started = (rc == 0)
|
||||
if rc != 0 {
|
||||
SecureLogger.log("TorManager: tor_host_start failed rc=\(rc)", category: SecureLogger.session, level: .error)
|
||||
} else {
|
||||
SecureLogger.log("TorManager: tor_host_start OK (\(socks), control \(control))", category: SecureLogger.session, level: .info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !started { return false }
|
||||
|
||||
// Start monitors and probe readiness
|
||||
startControlMonitorIfNeeded()
|
||||
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.log("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort) [embed]", category: SecureLogger.session, level: .info)
|
||||
} else {
|
||||
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "Tor SOCKS not reachable after embed start"])
|
||||
SecureLogger.log("TorManager: SOCKS not reachable (timeout) [embed]", category: SecureLogger.session, level: .error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
/// Probe the local SOCKS port until it's ready or a timeout elapses.
|
||||
private func waitForSocksReady(timeout: TimeInterval) async -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
@@ -404,48 +472,69 @@ final class TorManager: ObservableObject {
|
||||
// MARK: - Foreground recovery and control helpers
|
||||
|
||||
func ensureRunningOnForeground() {
|
||||
// If we can talk to ControlPort, wake Tor and verify bootstrap; else restart.
|
||||
// iOS can suspend Tor harshly; the most reliable approach for
|
||||
// embedding is to restart Tor every time we become active.
|
||||
Task.detached(priority: .userInitiated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// Avoid restarts while starting/restarting
|
||||
if await MainActor.run(body: { self.isStarting || self.restarting }) { return }
|
||||
let ok = await self.controlPingBootstrap()
|
||||
if ok {
|
||||
_ = await self.controlSendSignal("ACTIVE")
|
||||
return
|
||||
// Claim the restart under MainActor to avoid races
|
||||
let claimed: Bool = await MainActor.run {
|
||||
if self.isStarting || self.restarting { return false }
|
||||
self.restarting = true
|
||||
return true
|
||||
}
|
||||
// If Tor is still bootstrapping (SOCKS up but progress < 100), don't thrash; let monitor update
|
||||
let stillBootstrapping = await MainActor.run(body: { self.socksReady && self.bootstrapProgress < 100 })
|
||||
if stillBootstrapping { return }
|
||||
if !claimed { return }
|
||||
await self.restartTor()
|
||||
await MainActor.run { self.restarting = false }
|
||||
}
|
||||
}
|
||||
|
||||
func goDormantOnBackground() {
|
||||
// Stricter model: fully stop Tor when app backgrounds to save power
|
||||
// and avoid half-suspended states. We'll restart cleanly on .active.
|
||||
Task.detached { [weak self] in
|
||||
_ = await self?.controlSendSignal("DORMANT")
|
||||
guard let self = self else { return }
|
||||
_ = tor_host_shutdown()
|
||||
await MainActor.run {
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = false
|
||||
self.didStart = false
|
||||
// Allow control monitor to start anew on next start
|
||||
self.controlMonitorStarted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restartTor() async {
|
||||
await MainActor.run { self.restarting = true; self.isReady = false; self.socksReady = false; self.bootstrapProgress = 0; self.bootstrapSummary = ""; self.isStarting = true }
|
||||
// Try graceful shutdown if control is reachable
|
||||
_ = await controlSendSignal("SHUTDOWN")
|
||||
// Wait for SOCKS to go down
|
||||
let downDeadline = Date().addingTimeInterval(5)
|
||||
while Date() < downDeadline {
|
||||
if await !probeSocksOnce() { break }
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
await MainActor.run {
|
||||
// Announce restart so UI can notify the user
|
||||
NotificationCenter.default.post(name: .TorWillRestart, object: nil)
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = true
|
||||
self.lastRestartAt = Date()
|
||||
}
|
||||
// Prefer clean shutdown via owning controller FD; join the tor thread
|
||||
_ = tor_host_shutdown()
|
||||
// As a fallback, try control signal if needed (harmless if tor already down)
|
||||
_ = await controlSendSignal("SHUTDOWN")
|
||||
// Now start fresh
|
||||
await MainActor.run { self.didStart = false }
|
||||
await MainActor.run { self.startIfNeeded() }
|
||||
await MainActor.run { self.restarting = false }
|
||||
}
|
||||
|
||||
private func recomputeReady() {
|
||||
let ready = socksReady && bootstrapProgress >= 100
|
||||
if ready != isReady {
|
||||
isReady = ready
|
||||
if ready {
|
||||
// Broadcast readiness so clients can rebuild sessions and reconnect
|
||||
NotificationCenter.default.post(name: .TorDidBecomeReady, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,12 +546,37 @@ final class TorManager: ObservableObject {
|
||||
monitor.pathUpdateHandler = { [weak self] _ in
|
||||
// On any path change, poke Tor/recover (hop to main actor).
|
||||
Task { @MainActor in
|
||||
self?.ensureRunningOnForeground()
|
||||
guard let self = self else { return }
|
||||
// Avoid waking Tor while app is backgrounded; we'll handle on .active
|
||||
if self.isAppForeground {
|
||||
self.pokeTorOnPathChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
private func pokeTorOnPathChange() {
|
||||
// If a restart just happened, avoid thrashing
|
||||
if let last = lastRestartAt, Date().timeIntervalSince(last) < 3.0 {
|
||||
return
|
||||
}
|
||||
// If we're starting or restarting, do nothing and let that complete
|
||||
if isStarting || restarting { return }
|
||||
Task.detached(priority: .userInitiated) {
|
||||
let ok = await self.controlPingBootstrap()
|
||||
if ok {
|
||||
_ = await self.controlSendSignal("ACTIVE")
|
||||
return
|
||||
}
|
||||
// If bootstrapping is underway, let it finish
|
||||
let stillBootstrapping = await MainActor.run { self.socksReady && self.bootstrapProgress < 100 }
|
||||
if stillBootstrapping { return }
|
||||
// As a last resort, restart
|
||||
await self.ensureRunningOnForeground()
|
||||
}
|
||||
}
|
||||
|
||||
// Lightweight control: authenticate and GETINFO bootstrap-phase.
|
||||
private func controlPingBootstrap(timeout: TimeInterval = 3.0) async -> Bool {
|
||||
let data = await controlExchange(lines: ["GETINFO status/bootstrap-phase"], timeout: timeout)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
|
||||
static let TorWillRestart = Notification.Name("TorWillRestart")
|
||||
}
|
||||
@@ -362,6 +362,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private var torStatusAnnounced = false
|
||||
private var torProgressCancellable: AnyCancellable?
|
||||
private var lastTorProgressAnnounced = -1
|
||||
// Queue geohash-only system messages if user isn't on a location channel yet
|
||||
private var pendingGeohashSystemMessages: [String] = []
|
||||
// Track whether a Tor restart is pending so we only announce
|
||||
// "tor restarted" after an actual restart, not the first launch.
|
||||
private var torRestartPending: Bool = false
|
||||
|
||||
// MARK: - Caches
|
||||
|
||||
@@ -754,6 +759,19 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
name: NSApplication.willTerminateNotification,
|
||||
object: nil
|
||||
)
|
||||
// Tor lifecycle notifications: inform user when Tor restarts and when ready (macOS)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTorWillRestart),
|
||||
name: .TorWillRestart,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTorDidBecomeReady),
|
||||
name: .TorDidBecomeReady,
|
||||
object: nil
|
||||
)
|
||||
#else
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@@ -785,6 +803,19 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
name: UIApplication.willTerminateNotification,
|
||||
object: nil
|
||||
)
|
||||
// Tor lifecycle notifications: inform user when Tor restarts and when ready
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTorWillRestart),
|
||||
name: .TorWillRestart,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTorDidBecomeReady),
|
||||
name: .TorDidBecomeReady,
|
||||
object: nil
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -793,6 +824,24 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
deinit {
|
||||
// No need to force UserDefaults synchronization
|
||||
}
|
||||
|
||||
// MARK: - Tor notifications
|
||||
@objc private func handleTorWillRestart() {
|
||||
Task { @MainActor in
|
||||
self.torRestartPending = true
|
||||
self.addPublicSystemMessage("tor restarting to recover connectivity…")
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleTorDidBecomeReady() {
|
||||
Task { @MainActor in
|
||||
// Only announce "restarted" if we actually restarted this session
|
||||
if self.torRestartPending {
|
||||
self.addPublicSystemMessage("tor restarted. network routing restored.")
|
||||
self.torRestartPending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resubscribe to the active geohash channel without clearing timeline
|
||||
@MainActor
|
||||
@@ -1470,6 +1519,11 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
SecureLogger.log("RenderGuard: geohash \(ch.geohash) timeline has \(emptyGeo) empty messages after sanitize", category: SecureLogger.session, level: .debug)
|
||||
}
|
||||
}
|
||||
// If switching to a location channel, flush any pending geohash-only system messages
|
||||
if case .location = channel, !pendingGeohashSystemMessages.isEmpty {
|
||||
for m in pendingGeohashSystemMessages { addPublicSystemMessage(m) }
|
||||
pendingGeohashSystemMessages.removeAll(keepingCapacity: false)
|
||||
}
|
||||
// Unsubscribe previous
|
||||
if let sub = geoSubscriptionID {
|
||||
NostrRelayManager.shared.unsubscribe(id: sub)
|
||||
@@ -2630,8 +2684,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
self.markPrivateMessagesAsRead(from: peerID)
|
||||
}
|
||||
}
|
||||
// Also resubscribe the current geohash channel if active
|
||||
resubscribeCurrentGeohash()
|
||||
// Subscriptions will be resent after connections come back up
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -4925,6 +4978,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
func addGeohashOnlySystemMessage(_ content: String) {
|
||||
if case .location = activeChannel {
|
||||
addPublicSystemMessage(content)
|
||||
} else {
|
||||
// Not on a location channel yet: queue to show when user switches
|
||||
pendingGeohashSystemMessages.append(content)
|
||||
}
|
||||
}
|
||||
// Send a public message without adding a local user echo.
|
||||
|
||||
Reference in New Issue
Block a user