Read package identifier and app group ID from Info.plist at runtime via AppConfiguration. This enables SFM.System to use TeamID-prefixed app group format required by macOS Sequoia, fixing the privacy prompt when accessing app group data.
175 lines
5.9 KiB
Swift
175 lines
5.9 KiB
Swift
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()
|
|
}
|
|
}
|