Gate Tor/Nostr start behind permissions or mutual favorites; add clean shutdown + robust gating (#619)

- Add NetworkActivationService to permit Tor/Nostr only when location is authorized OR at least one mutual favorite exists.
- Gate TorManager.startIfNeeded/ensureRunningOnForeground behind a global allowAutoStart flag.
- Always stop Tor on background for deterministic restarts; rebuild sessions on foreground when allowed.
- NostrRelayManager respects the gate in connect/ensureConnections/subscribe/send/connectToRelay and skips reconnection when disallowed.
- Symmetric shutdown when conditions become disallowed: disconnect relays and stop Tor.
- Fix double-start by avoiding restart if Tor is already ready; prevent background thrash.
- Improve UX: post "starting tor…" via TorWillStart, and "tor started…" on initial ready; keep existing restart messages.

Rationale: Avoid starting Tor/relays when the user has no location permission and no mutual favorites, and ensure a clean, predictable lifecycle (no stale sockets, no double starts).

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-09-15 16:46:49 +02:00
committed by GitHub
co-authored by jack
parent d1682db79b
commit 9cbdb0a764
6 changed files with 183 additions and 76 deletions
@@ -0,0 +1,70 @@
import Foundation
import BitLogger
import Combine
/// Coordinates when the app is allowed to start Tor and connect to Nostr relays.
/// Policy: permit start when either location permissions are authorized OR
/// there exists at least one mutual favorite. Otherwise, do not start.
@MainActor
final class NetworkActivationService: ObservableObject {
static let shared = NetworkActivationService()
@Published private(set) var activationAllowed: Bool = false
private var cancellables = Set<AnyCancellable>()
private var started = false
private init() {}
func start() {
guard !started else { return }
started = true
// Initial compute
activationAllowed = Self.computeAllowed()
TorManager.shared.setAutoStartAllowed(activationAllowed)
// React to location permission changes
LocationChannelManager.shared.$permissionState
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.reevaluate()
}
.store(in: &cancellables)
// React to mutual favorites changes
FavoritesPersistenceService.shared.$mutualFavorites
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.reevaluate()
}
.store(in: &cancellables)
}
private func reevaluate() {
let allowed = Self.computeAllowed()
if allowed != activationAllowed {
SecureLogger.info("NetworkActivationService: activationAllowed -> \(allowed)", category: .session)
activationAllowed = allowed
TorManager.shared.setAutoStartAllowed(allowed)
if allowed {
// Kick Tor + relays if we're now permitted
TorManager.shared.startIfNeeded()
// If app is in foreground, begin relay connections
if TorManager.shared.isForeground() {
NostrRelayManager.shared.connect()
}
} else {
// Transitioned to disallowed: disconnect relays and shut down Tor
NostrRelayManager.shared.disconnect()
TorManager.shared.goDormantOnBackground()
}
}
}
private static func computeAllowed() -> Bool {
let permOK = LocationChannelManager.shared.permissionState == .authorized
let hasMutual = !FavoritesPersistenceService.shared.mutualFavorites.isEmpty
return permOK || hasMutual
}
}
+19
View File
@@ -69,18 +69,24 @@ final class TorManager: ObservableObject {
private var pathMonitor: NWPathMonitor?
private var isAppForeground: Bool = true
private var lastRestartAt: Date? = nil
// Global policy gate: only allow Tor to start when true
private(set) var allowAutoStart: Bool = false
private init() {}
// MARK: - Public API
func startIfNeeded() {
// Respect global start policy
guard allowAutoStart else { return }
// Do not start in background; caller should wait for foreground
guard isAppForeground else { return }
guard !didStart else { return }
didStart = true
isStarting = true
lastError = nil
// Announce initial start so UI can show a status message
NotificationCenter.default.post(name: .TorWillStart, object: nil)
ensureFilesystemLayout()
startTor()
startPathMonitorIfNeeded()
@@ -473,6 +479,8 @@ final class TorManager: ObservableObject {
// MARK: - Foreground recovery and control helpers
func ensureRunningOnForeground() {
// Respect global start policy
if !allowAutoStart { return }
// 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
@@ -636,3 +644,14 @@ final class TorManager: ObservableObject {
return resultText
}
}
// MARK: - Start policy configuration
extension TorManager {
@MainActor
func setAutoStartAllowed(_ allow: Bool) {
allowAutoStart = allow
}
@MainActor
func isAutoStartAllowed() -> Bool { allowAutoStart }
}
@@ -3,4 +3,5 @@ import Foundation
extension Notification.Name {
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
static let TorWillRestart = Notification.Name("TorWillRestart")
static let TorWillStart = Notification.Name("TorWillStart")
}