Files
sing-box-for-apple/Library/Network/ExtensionProfile.swift
T
世界 81d6827334 Refactor to eliminate hardcoded identifiers
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.
2026-01-03 02:46:52 +08:00

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()
}
}