import Foundation import Libbox import NetworkExtension @MainActor public class ExtensionProfile: ObservableObject { public static let controlKind = AppConfiguration.widgetControlKind private let manager: NEVPNManager private var connection: NEVPNConnection private var observer: Any? @Published public var status: NEVPNStatus @Published public var connectedDate: Date? public init(_ manager: NEVPNManager) { self.manager = manager connection = manager.connection status = manager.connection.status connectedDate = manager.connection.connectedDate } public func register() { observer = NotificationCenter.default.addObserver( forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: .main ) { [weak self] notification in guard let self else { return } guard let connection = notification.object as? NEVPNConnection else { return } self.connection = connection self.status = connection.status self.connectedDate = connection.connectedDate } } private func unregister() { if let observer { NotificationCenter.default.removeObserver(observer) } } nonisolated deinit { if let observer { NotificationCenter.default.removeObserver(observer) } } private static func makeDefaultOnDemandRules() -> [NEOnDemandRule] { let rule = NEOnDemandRuleConnect() rule.interfaceTypeMatch = .any rule.probeURL = URL(string: "http://captive.apple.com") return [rule] } private func setOnDemandRules(useDefaultRules: Bool) async { if useDefaultRules { manager.onDemandRules = Self.makeDefaultOnDemandRules() } else { let rules = await SharedPreferences.onDemandRules.get() manager.onDemandRules = rules.isEmpty ? Self.makeDefaultOnDemandRules() : rules.map { $0.toNERule() } } } public func updateOnDemand(enabled: Bool, useDefaultRules: Bool) async throws { manager.isOnDemandEnabled = enabled await setOnDemandRules(useDefaultRules: useDefaultRules) try await manager.saveToPreferences() } @available(iOS 16.0, macOS 13.0, tvOS 17.0, *) public func fetchLastDisconnectError() async throws { try await connection.fetchLastDisconnectError() } public func start() async throws { await fetchProfile() manager.isEnabled = true let alwaysOn = await SharedPreferences.alwaysOn.get() let onDemandEnabled = await SharedPreferences.onDemandEnabled.get() if alwaysOn || onDemandEnabled { manager.isOnDemandEnabled = true await setOnDemandRules(useDefaultRules: alwaysOn) } #if !os(tvOS) if let protocolConfiguration = manager.protocolConfiguration { let includeAllNetworks = await SharedPreferences.includeAllNetworks.get() protocolConfiguration.includeAllNetworks = includeAllNetworks if #available(iOS 16.4, macOS 13.3, *) { protocolConfiguration.excludeCellularServices = !includeAllNetworks } } #endif try await manager.saveToPreferences() #if os(macOS) if Variant.useSystemExtension { try manager.connection.startVPNTunnel(options: [ "username": NSString(string: NSUserName()), "manualStart": NSNumber(value: true), ]) return } #endif try manager.connection.startVPNTunnel(options: [ "manualStart": NSNumber(value: true), ]) } public func fetchProfile() async { do { if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) { if profile.type == .icloud { _ = try profile.read() } } } catch { NSLog("fetchProfile error: \(error.localizedDescription)") } } public func stop() async throws { if manager.isOnDemandEnabled { manager.isOnDemandEnabled = false try await manager.saveToPreferences() } do { try LibboxNewStandaloneCommandClient()!.serviceClose() } catch { NSLog("serviceClose error: \(error.localizedDescription)") } manager.connection.stopVPNTunnel() } public func restart() async throws { try await stop() var waitSeconds = 0 while status != .disconnected { try await Task.sleep(nanoseconds: NSEC_PER_SEC) waitSeconds += 1 if waitSeconds >= 5 { throw NSError(domain: "Restart service timeout", code: 0) } } try await start() } public static func load() async throws -> ExtensionProfile? { let managers = try await NETunnelProviderManager.loadAllFromPreferences() if managers.isEmpty { return nil } let profile = ExtensionProfile(managers[0]) return profile } public static func install() async throws { let manager = NETunnelProviderManager() manager.localizedDescription = Variant.applicationName let tunnelProtocol = NETunnelProviderProtocol() if Variant.useSystemExtension { tunnelProtocol.providerBundleIdentifier = AppConfiguration.systemExtensionBundleID } else { tunnelProtocol.providerBundleIdentifier = AppConfiguration.extensionBundleID } tunnelProtocol.serverAddress = "sing-box" manager.protocolConfiguration = tunnelProtocol manager.isEnabled = true try await manager.saveToPreferences() } }