import Foundation /// Lightweight IPC between the main app and the packet tunnel extension /// using Darwin notifications and a shared database-backed configuration model. public final class IPCManager: @unchecked Sendable { public static let shared = IPCManager() public enum Notification: String, Sendable { case newTrafficCaptured = "com.treyt.proxyapp.newTraffic" case configurationChanged = "com.treyt.proxyapp.configChanged" case extensionStarted = "com.treyt.proxyapp.extensionStarted" case extensionStopped = "com.treyt.proxyapp.extensionStopped" } private let configurationRepo = ConfigurationRepository() private init() { ProxyLogger.ipc.info("IPCManager using shared database-backed configuration") } // MARK: - Darwin Notifications public func post(_ notification: Notification) { ProxyLogger.ipc.debug("POST Darwin: \(notification.rawValue)") let name = CFNotificationName(notification.rawValue as CFString) CFNotificationCenterPostNotification( CFNotificationCenterGetDarwinNotifyCenter(), name, nil, nil, true ) } public func observe(_ notification: Notification, callback: @escaping @Sendable () -> Void) { let didInstall = DarwinCallbackStore.shared.register(name: notification.rawValue, callback: callback) if !didInstall { ProxyLogger.ipc.info("OBSERVE Darwin reuse: \(notification.rawValue)") return } ProxyLogger.ipc.info("OBSERVE Darwin install: \(notification.rawValue)") let name = notification.rawValue as CFString let center = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterAddObserver( center, nil, { _, _, name, _, _ in guard let cfName = name?.rawValue as? String else { return } DarwinCallbackStore.shared.fire(name: cfName) }, name, nil, .deliverImmediately ) } // MARK: - Shared Config (file-based, reliable cross-process) public var isSSLProxyingEnabled: Bool { get { get(\.sslProxyingEnabled) } set { set(\.sslProxyingEnabled, newValue) } } public var isBlockListEnabled: Bool { get { get(\.blockListEnabled) } set { set(\.blockListEnabled, newValue) } } public var isBreakpointEnabled: Bool { get { get(\.breakpointEnabled) } set { set(\.breakpointEnabled, newValue) } } public var isNoCachingEnabled: Bool { get { get(\.noCachingEnabled) } set { set(\.noCachingEnabled, newValue) } } public var isDNSSpoofingEnabled: Bool { get { get(\.dnsSpoofingEnabled) } set { set(\.dnsSpoofingEnabled, newValue) } } public var hideSystemTraffic: Bool { get { get(\.hideSystemTraffic) } set { set(\.hideSystemTraffic, newValue) } } // MARK: - Configuration private func get(_ keyPath: KeyPath) -> Bool { do { return try configurationRepo.current()[keyPath: keyPath] } catch { ProxyLogger.ipc.error("Config READ failed: \(error.localizedDescription)") return false } } private func set(_ keyPath: WritableKeyPath, _ value: Bool) { do { try configurationRepo.update { $0[keyPath: keyPath] = value } ProxyLogger.ipc.info("Config SET value=\(value)") } catch { ProxyLogger.ipc.error("Config WRITE failed: \(error.localizedDescription)") } } } // MARK: - Darwin Callback Storage private final class DarwinCallbackStore: @unchecked Sendable { static let shared = DarwinCallbackStore() private var callbacks: [String: @Sendable () -> Void] = [:] private var installedObserverNames: Set = [] private var fireCounts: [String: Int] = [:] private let lock = NSLock() func register(name: String, callback: @escaping @Sendable () -> Void) -> Bool { lock.lock() defer { lock.unlock() } callbacks[name] = callback let isNew = installedObserverNames.insert(name).inserted return isNew } func fire(name: String) { lock.lock() let cb = callbacks[name] fireCounts[name, default: 0] += 1 let count = fireCounts[name] ?? 0 lock.unlock() ProxyLogger.ipc.debug("FIRE Darwin callback: \(name) count=\(count)") cb?() } }