220 lines
7.4 KiB
Swift
Executable File
220 lines
7.4 KiB
Swift
Executable File
//
|
||
// VPNManager.swift
|
||
// Runner
|
||
//
|
||
// Created by GFWFighter on 7/25/1402 AP.
|
||
//
|
||
|
||
import Foundation
|
||
import Combine
|
||
import NetworkExtension
|
||
|
||
enum VPNManagerAlertType: String {
|
||
case RequestVPNPermission
|
||
case RequestNotificationPermission
|
||
case EmptyConfiguration
|
||
case StartCommandServer
|
||
case CreateService
|
||
case StartService
|
||
}
|
||
|
||
struct VPNManagerAlert {
|
||
let alert: VPNManagerAlertType?
|
||
let message: String?
|
||
}
|
||
|
||
class VPNManager: ObservableObject {
|
||
private var cancelBag: Set<AnyCancellable> = []
|
||
|
||
private var observer: NSObjectProtocol?
|
||
private var manager = NEVPNManager.shared()
|
||
private var loaded: Bool = false
|
||
private var timer: Timer?
|
||
|
||
static let shared: VPNManager = VPNManager()
|
||
|
||
@Published private(set) var state: NEVPNStatus = .invalid
|
||
@Published private(set) var alert: VPNManagerAlert = .init(alert: nil, message: nil)
|
||
|
||
@Published private(set) var upload: Int64 = 0
|
||
@Published private(set) var download: Int64 = 0
|
||
@Published private(set) var elapsedTime: TimeInterval = 0
|
||
|
||
private var _connectTime: Date?
|
||
private var connectTime: Date? {
|
||
set {
|
||
UserDefaults(suiteName: FilePath.groupName)?.set(newValue?.timeIntervalSince1970, forKey: "SingBoxConnectTime")
|
||
_connectTime = newValue
|
||
}
|
||
get {
|
||
if let _connectTime {
|
||
return _connectTime
|
||
}
|
||
guard let interval = UserDefaults(suiteName: FilePath.groupName)?.value(forKey: "SingBoxConnectTime") as? TimeInterval else {
|
||
return nil
|
||
}
|
||
return Date(timeIntervalSince1970: interval)
|
||
}
|
||
}
|
||
private var readingWS: Bool = false
|
||
|
||
@Published var isConnectedToAnyVPN: Bool = false
|
||
|
||
init() {
|
||
observer = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: nil) { [weak self] notification in
|
||
guard let connection = notification.object as? NEVPNConnection else { return }
|
||
self?.state = connection.status
|
||
}
|
||
|
||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||
guard let self else { return }
|
||
updateStats()
|
||
elapsedTime = -1 * (connectTime?.timeIntervalSinceNow ?? 0)
|
||
}
|
||
}
|
||
|
||
deinit {
|
||
if let observer {
|
||
NotificationCenter.default.removeObserver(observer)
|
||
}
|
||
timer?.invalidate()
|
||
}
|
||
|
||
func setup() async throws {
|
||
// guard !loaded else { return }
|
||
loaded = true
|
||
do {
|
||
try await loadVPNPreference()
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
private func loadVPNPreference() async throws {
|
||
do {
|
||
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
||
if let manager = managers.first {
|
||
self.manager = manager
|
||
return
|
||
}
|
||
let newManager = NETunnelProviderManager()
|
||
let `protocol` = NETunnelProviderProtocol()
|
||
`protocol`.providerBundleIdentifier = Bundle.main.baseBundleIdentifier + ".PacketTunnel"
|
||
`protocol`.serverAddress = "localhost"
|
||
newManager.protocolConfiguration = `protocol`
|
||
newManager.localizedDescription = "HiFastVPN"
|
||
try await newManager.saveToPreferences()
|
||
try await newManager.loadFromPreferences()
|
||
self.manager = newManager
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
private func enableVPNManager() async throws {
|
||
manager.isEnabled = true
|
||
do {
|
||
try await manager.saveToPreferences()
|
||
try await manager.loadFromPreferences()
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
@MainActor private func set(upload: Int64, download: Int64) {
|
||
self.upload = upload
|
||
self.download = download
|
||
}
|
||
|
||
var isAnyVPNConnected: Bool {
|
||
guard let cfDict = CFNetworkCopySystemProxySettings() else { return false }
|
||
let nsDict = cfDict.takeRetainedValue() as NSDictionary
|
||
guard let keys = nsDict["__SCOPED__"] as? NSDictionary else {
|
||
return false
|
||
}
|
||
for key: String in keys.allKeys as! [String] {
|
||
if key == "tap" || key == "tun" || key == "ppp" || key == "ipsec" || key == "ipsec0" {
|
||
return true
|
||
} else if key.starts(with: "utun") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func reset() {
|
||
loaded = false
|
||
if state != .disconnected && state != .invalid {
|
||
disconnect()
|
||
}
|
||
$state.filter { $0 == .disconnected || $0 == .invalid }.first().sink { [weak self] _ in
|
||
Task { [weak self] () in
|
||
self?.manager = .shared()
|
||
do {
|
||
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
||
for manager in managers ?? [] {
|
||
try await manager.removeFromPreferences()
|
||
}
|
||
try await self?.loadVPNPreference()
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
}
|
||
}.store(in: &cancelBag)
|
||
|
||
}
|
||
|
||
private func updateStats() {
|
||
let isAnyVPNConnected = self.isAnyVPNConnected
|
||
if isConnectedToAnyVPN != isAnyVPNConnected {
|
||
isConnectedToAnyVPN = isAnyVPNConnected
|
||
}
|
||
guard state == .connected else { return }
|
||
guard let connection = manager.connection as? NETunnelProviderSession else { return }
|
||
do {
|
||
try connection.sendProviderMessage("stats".data(using: .utf8)!) { [weak self] response in
|
||
guard
|
||
let response,
|
||
let response = String(data: response, encoding: .utf8)
|
||
else { return }
|
||
let responseComponents = response.components(separatedBy: ",")
|
||
guard
|
||
responseComponents.count == 2,
|
||
let upload = Int64(responseComponents[0]),
|
||
let download = Int64(responseComponents[1])
|
||
else { return }
|
||
Task { [upload, download, weak self] () in
|
||
await self?.set(upload: upload, download: download)
|
||
}
|
||
}
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
func connect(with config: String, disableMemoryLimit: Bool = false) async throws {
|
||
await set(upload: 0, download: 0)
|
||
// 修复:放宽前置条件,允许在非 connected 状态下连接
|
||
// 只要不是已连接状态,都允许尝试连接(包括 invalid、reasserting 等状态)
|
||
guard state != .connected else { return }
|
||
do {
|
||
try await enableVPNManager()
|
||
try manager.connection.startVPNTunnel(options: [
|
||
"Config": config as NSString,
|
||
"DisableMemoryLimit": (disableMemoryLimit ? "YES" : "NO") as NSString,
|
||
])
|
||
} catch {
|
||
print(error.localizedDescription)
|
||
}
|
||
connectTime = .now
|
||
}
|
||
|
||
func disconnect() {
|
||
if state != .disconnected && state != .invalid {
|
||
manager.connection.stopVPNTunnel()
|
||
} else {
|
||
alert = VPNManagerAlert(alert: nil, message: nil)
|
||
}
|
||
}
|
||
}
|