Files
umbrix/ios/Runner/VPN/VPNManager.swift
problematicconsumer b1d6c56a3e Add ios command client
2024-02-03 21:23:04 +03:30

233 lines
7.8 KiB
Swift

//
// 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
@Published private(set) var logList: [String] = []
@Published private(set) var logCallback = false
private(set) var logClient: CommandClient?
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)
}
logClient = CommandClient(.log)
}
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 {
onServiceWriteLog(message: 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 + ".SingBoxPacketTunnel"
`protocol`.serverAddress = "localhost"
newManager.protocolConfiguration = `protocol`
newManager.localizedDescription = "Hiddify"
try await newManager.saveToPreferences()
try await newManager.loadFromPreferences()
self.manager = newManager
} catch {
onServiceWriteLog(message: error.localizedDescription)
}
}
private func enableVPNManager() async throws {
manager.isEnabled = true
do {
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
} catch {
onServiceWriteLog(message: 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 {
onServiceWriteLog(message: 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 {
onServiceWriteLog(message: error.localizedDescription)
}
}
func connect(with config: String, disableMemoryLimit: Bool = false) async throws {
await set(upload: 0, download: 0)
guard state == .disconnected else { return }
do {
try await enableVPNManager()
try manager.connection.startVPNTunnel(options: [
"Config": config as NSString,
"DisableMemoryLimit": (disableMemoryLimit ? "YES" : "NO") as NSString,
])
} catch {
onServiceWriteLog(message: error.localizedDescription)
}
connectTime = .now
}
func disconnect() {
guard state == .connected else { return }
manager.connection.stopVPNTunnel()
}
func onServiceWriteLog(message: String) {
logCallback = true
if logList.count > 300 {
logList.removeFirst()
}
logList.append(message)
}
func onServiceResetLogs() {
logCallback = false
logList.removeAll()
}
}