diff --git a/Tun/PacketTunnelProvider.swift b/Tun/PacketTunnelProvider.swift index c554528..9960772 100644 --- a/Tun/PacketTunnelProvider.swift +++ b/Tun/PacketTunnelProvider.swift @@ -21,6 +21,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac") let msg = shared?.string(forKey: "test_msg") SDLLogger.log("NE read message: \(msg ?? "failed")", for: .debug) + SDLTunnelAppNotifier.shared.clear() SDLNotificationCenter.shared.post(.vpnStatusChanged) diff --git a/Tun/Punchnet/Actors/SDLContextActor.swift b/Tun/Punchnet/Actors/SDLContextActor.swift index 43261cc..7ed60e1 100644 --- a/Tun/Punchnet/Actors/SDLContextActor.swift +++ b/Tun/Punchnet/Actors/SDLContextActor.swift @@ -70,7 +70,7 @@ actor SDLContextActor { private var monitorWorker: Task? // 内部socket通讯 - private var noticeClient: SDLNoticeClient? + // 改为基于 App Group + Darwin Notification 的通知 // 流量统计 nonisolated private let flowTracer = SDLFlowTracer() @@ -114,6 +114,7 @@ actor SDLContextActor { } self.readyState = .starting + self.prepareTunnelNotifier() self.startMonitor() // 启动arp的定时清理任务 @@ -129,14 +130,7 @@ actor SDLContextActor { await quicClient.waitClose() SDLLogger.log("[SDLContext] quicClient closed!!!!") } - - await self.supervisor.addWorker(name: "noticeClient") { - let noticeClient = try self.startNoticeClient() - SDLLogger.log("[SDLContext] noticeClient running!!!!") - try await noticeClient.waitClose() - SDLLogger.log("[SDLContext] noticeClient closed!!!!") - } - + await self.supervisor.addWorker(name: "udpHole") { let udpHole = try await self.startUDPHole() SDLLogger.log("[SDLContext] udp running!!!!") @@ -217,15 +211,12 @@ actor SDLContextActor { return quicClient } - private func startNoticeClient() throws -> SDLNoticeClient { + private func prepareTunnelNotifier() { // 启动noticeClient - let noticeClient = try SDLNoticeClient(noticePort: self.config.noticePort) - noticeClient.start() + // 旧的 UDP NoticeClient 已移除,改为初始化基于 App Group 的通知通道。 + SDLTunnelAppNotifier.shared.clear() - SDLLogger.log("[SDLContext] noticeClient started") - self.noticeClient = noticeClient - - return noticeClient + SDLLogger.log("[SDLContext] tunnelAppNotifier ready") } private func startMonitor() { @@ -386,11 +377,15 @@ actor SDLContextActor { self.updatePolicyTask?.cancel() self.updatePolicyTask = nil - self.noticeClient?.stop() - self.noticeClient = nil self.sessionToken = nil self.dataCipher = nil } + + private func publishTunnelEvent(level: SDLTunnelAppNotifier.Event.Level = .error, + code: Int? = nil, + message: String) { + SDLTunnelAppNotifier.shared.publish(level: level, code: code, message: message) + } private func setNatType(natType: SDLNATProberActor.NatType) { self.natType = natType @@ -711,8 +706,7 @@ extension SDLContextActor { switch errorCode { case .invalidToken, .nodeDisabled: self.superRegistrationStateMachine.handleFailure() - let alertNotice = NoticeMessage.alert(alert: errorMessage) - self.noticeClient?.send(data: alertNotice) + self.publishTunnelEvent(level: .fatal, code: Int(errorCode.rawValue), message: errorMessage) // 报告错误并退出 let error = NSError(domain: "com.jihe.punchnet.tun", code: -1) self.failReady(error) @@ -720,8 +714,7 @@ extension SDLContextActor { case .noIpAddress, .networkFault, .internalFault: self.superRegistrationStateMachine.handleRetryableNak() - let alertNotice = NoticeMessage.alert(alert: errorMessage) - self.noticeClient?.send(data: alertNotice) + self.publishTunnelEvent(level: .warning, code: Int(errorCode.rawValue), message: errorMessage) } SDLLogger.log("[SDLContext] Get a SuperNak message exit") @@ -742,8 +735,7 @@ extension SDLContextActor { self.sendPeerPacket(type: .register, data: registerData, remoteAddress: remoteAddress) } case .shutdown(let message): - let alertNotice = NoticeMessage.alert(alert: message) - self.noticeClient?.send(data: alertNotice) + self.publishTunnelEvent(level: .fatal, message: message) // 报告错误并退出 let error = NSError(domain: "com.jihe.punchnet.tun", code: -2) diff --git a/Tun/Punchnet/SDLConfiguration.swift b/Tun/Punchnet/SDLConfiguration.swift index b763705..17c3d1f 100644 --- a/Tun/Punchnet/SDLConfiguration.swift +++ b/Tun/Punchnet/SDLConfiguration.swift @@ -52,8 +52,6 @@ public class SDLConfiguration { let serverIp: String let stunServers: [String] - let noticePort: Int - lazy var stunSocketAddress: SocketAddress = { let stunServer = stunServers[0] return try! SocketAddress.makeAddressResolvingHost(stunServer, port: 1365) @@ -84,7 +82,6 @@ public class SDLConfiguration { clientId: String, networkAddress: NetworkAddress, hostname: String, - noticePort: Int, accessToken: String, identityId: UInt32, exitNode: ExitNode?) { @@ -94,7 +91,6 @@ public class SDLConfiguration { self.stunServers = stunServers self.clientId = clientId self.networkAddress = networkAddress - self.noticePort = noticePort self.accessToken = accessToken self.identityId = identityId self.hostname = hostname @@ -110,7 +106,6 @@ extension SDLConfiguration { guard let version = options["version"] as? Int, let serverHost = options["server_host"] as? String, let stunAssistHost = options["stun_assist_host"] as? String, - let noticePort = options["notice_port"] as? Int, let accessToken = options["access_token"] as? String, let identityId = options["identity_id"] as? UInt32, let clientId = options["client_id"] as? String, @@ -141,7 +136,6 @@ extension SDLConfiguration { clientId: clientId, networkAddress: networkAddress, hostname: hostname, - noticePort: noticePort, accessToken: accessToken, identityId: identityId, exitNode: exitNode) diff --git a/punchnet/Core/SDLTunnelAppEventStore.swift b/punchnet/Core/SDLTunnelAppEventStore.swift new file mode 100644 index 0000000..dc1b4f3 --- /dev/null +++ b/punchnet/Core/SDLTunnelAppEventStore.swift @@ -0,0 +1,47 @@ +// +// SDLTunnelAppEventStore.swift +// punchnet +// +// Created by 安礼成 on 2026/4/15. +// + +import Foundation + +struct SDLTunnelAppEventStore { + struct Event: Codable, Sendable, Identifiable { + enum Level: String, Codable, Sendable { + case info + case warning + case error + case fatal + } + + let id: String + let timestamp: TimeInterval + let level: Level + let code: Int? + let message: String + } + + static let appGroupSuiteName = "group.com.jihe.punchnetmac" + static let latestEventKey = "tunnel.latestEvent" + + static func loadLatestEvent() -> Event? { + guard let shared = UserDefaults(suiteName: self.appGroupSuiteName), + let data = shared.data(forKey: self.latestEventKey), + let event = try? JSONDecoder().decode(Event.self, from: data) else { + return nil + } + + return event + } + + static func clearLatestEvent() { + guard let shared = UserDefaults(suiteName: self.appGroupSuiteName) else { + return + } + + shared.removeObject(forKey: self.latestEventKey) + shared.synchronize() + } +} diff --git a/punchnet/Core/SystemConfig.swift b/punchnet/Core/SystemConfig.swift index a5088e8..c49e783 100644 --- a/punchnet/Core/SystemConfig.swift +++ b/punchnet/Core/SystemConfig.swift @@ -36,7 +36,6 @@ struct SystemConfig { accessToken: String, identityId: UInt32, hostname: String, - noticePort: Int, exitNodeIp: String?) -> [String: NSObject] { // guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first, // let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else { @@ -54,7 +53,6 @@ struct SystemConfig { "server_host": serverHost as NSObject, "stun_assist_host": stunAssistHost as NSObject, "hostname": hostname as NSObject, - "notice_port": noticePort as NSObject, "network_address": [ "network_id": networkId as NSObject, "ip": ip as NSObject, diff --git a/punchnet/Views/AppContext.swift b/punchnet/Views/AppContext.swift index 495dee6..cb61b53 100644 --- a/punchnet/Views/AppContext.swift +++ b/punchnet/Views/AppContext.swift @@ -15,18 +15,16 @@ struct AppContextError: Error { @Observable class AppContext { private var vpnManager = VPNManager.shared - - var noticePort: Int - + // 调用 "/connect" 之后的网络信息 var networkContext: SDLAPIClient.NetworkContext? = nil - + // 在menu里面需要使用 var vpnOptions: [String: NSObject]? = nil - + // 当前app所处的场景 var appScene: AppScene = .login(username: nil) - + // 当前的场景 enum AppScene: Equatable { case login(username: String?) @@ -34,26 +32,26 @@ class AppContext { case register case resetPassword } - + // 登陆凭证 var loginCredit: Credit? - + // 判断用户是否登陆 var isLogined: Bool { return loginCredit != nil } - + enum Credit { case token(token: String, session: SDLAPIClient.NetworkSession) case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession) } - + @ObservationIgnored var networkSession: SDLAPIClient.NetworkSession? { guard let loginCredit = self.loginCredit else { return nil } - + switch loginCredit { case .token(_, let session): return session @@ -61,11 +59,17 @@ class AppContext { return session } } - - init(noticePort: Int) { - self.noticePort = noticePort + + var tunnelEvent: SDLTunnelAppEventStore.Event? = nil + + @ObservationIgnored + private var lastTunnelEventID: String? + + init() { + self.observeTunnelEvent() + self.consumeLatestTunnelEvent() } - + func loginWith(token: String) async throws { let networkSession = try await SDLAPIClient.loginWithToken(token: token) self.loginCredit = .token(token: token, session: networkSession) @@ -83,44 +87,44 @@ class AppContext { try KeychainStore.shared.save(data, account: "accountAndPasword") } } - + // 连接到对应的网络 func connectNetwork() async throws { guard let session = self.networkSession else { throw AppContextError(message: "未登陆") } - + // 避免重复连接 guard !vpnManager.isConnected else { throw AppContextError(message: "网络已经连接") } - + self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken) } - + func changeExitNodeIp(exitNodeIp: String) async throws -> Data { // 避免重复连接 guard vpnManager.isConnected else { throw AppContextError(message: "网络未连接") } - + var exitNodeIpChanged = NEMessage.ExitNodeIpChanged() exitNodeIpChanged.ip = exitNodeIp - + var neMessage = NEMessage() neMessage.message = .exitNodeIpChanged(exitNodeIpChanged) - + let message = try neMessage.serializedData() - + return try await self.vpnManager.sendMessage(message) } - + // 启动tun func startTun() async throws { guard let session = self.networkSession, let context = self.networkContext else { return } - + let options = SystemConfig.getOptions( networkId: UInt32(session.networkId), networkDomain: session.networkDomain, @@ -129,12 +133,11 @@ class AppContext { accessToken: session.accessToken, identityId: context.identityId, hostname: context.hostname, - noticePort: noticePort, exitNodeIp: self.loadExitNodeIp() ) try await self.vpnManager.enableVpn(options: options) } - + // 断开网络连接 func stopTun() async throws { try await self.vpnManager.disableVpn() @@ -146,14 +149,14 @@ class AppContext { self.networkContext = nil self.loginCredit = nil } - + func loadCacheToken() -> String? { if let data = try? KeychainStore.shared.load(account: "token") { return String(data: data, encoding: .utf8) } return nil } - + func loadCacheUsernameAndPassword() -> (String, String)? { if let data = try? KeychainStore.shared.load(account: "accountAndPasword"), let str = String(data: data, encoding: .utf8) { @@ -164,24 +167,49 @@ class AppContext { } return nil } - + } // 处理网络出口数据 extension AppContext { - + func loadExitNodeIp() -> String? { if let data = try? KeychainStore.shared.load(account: "exitNodeIp") { return String(data: data, encoding: .utf8) } return nil } - + func saveExitNodeIp(exitNodeIp: String) async throws { // 将数据缓存到keychain if let data = exitNodeIp.data(using: .utf8) { try KeychainStore.shared.save(data, account: "exitNodeIp") } } - + + func dismissTunnelEvent() { + self.tunnelEvent = nil + } + + private func observeTunnelEvent() { + SDLNotificationCenter.shared.addObserver(for: .tunnelEventChanged) { [weak self] _ in + self?.consumeLatestTunnelEvent() + } + } + + private func consumeLatestTunnelEvent() { + guard let event = SDLTunnelAppEventStore.loadLatestEvent(), + self.lastTunnelEventID != event.id else { + return + } + + self.lastTunnelEventID = event.id + self.tunnelEvent = event + SDLTunnelAppEventStore.clearLatestEvent() + } + + deinit { + SDLNotificationCenter.shared.removeObserver(for: .tunnelEventChanged) + } + } diff --git a/punchnet/Views/AppRootView.swift b/punchnet/Views/AppRootView.swift index c54f0ac..111893a 100644 --- a/punchnet/Views/AppRootView.swift +++ b/punchnet/Views/AppRootView.swift @@ -10,7 +10,7 @@ import SwiftUI struct AppRootView: View { @Environment(AppContext.self) var appContext: AppContext @State private var updateManager = AppUpdateManager.shared - + var body: some View { ZStack { // 1. 主要界面容器 @@ -35,7 +35,7 @@ struct AppRootView: View { insertion: .move(edge: .trailing).combined(with: .opacity), removal: .move(edge: .leading).combined(with: .opacity) // 修改为 leading 更有“流转”感 )) - + // 自动更新遮罩层级 if updateManager.showUpdateOverlay, let info = updateManager.updateInfo { updateOverlay(info: info) @@ -46,12 +46,19 @@ struct AppRootView: View { .animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay) // 关键:macOS 标准毛玻璃背景 .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) + .alert("提示", isPresented: tunnelEventPresented, presenting: appContext.tunnelEvent) { _ in + Button("确定", role: .cancel) { + appContext.dismissTunnelEvent() + } + } message: { event in + Text(event.message) + } .task { let checkUpdateResult = await updateManager.checkUpdate(isManual: false) NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)") } } - + // 将遮罩抽离,保持 body 清爽 @ViewBuilder private func updateOverlay(info: SDLAPIClient.AppUpgradeInfo) -> some View { @@ -63,7 +70,7 @@ struct AppRootView: View { updateManager.showUpdateOverlay = false } } - + AppUpdateView(info: info) { updateManager.showUpdateOverlay = false } @@ -76,5 +83,16 @@ struct AppRootView: View { )) .zIndex(100) // 确保更新遮罩永远在最上层 } - + + private var tunnelEventPresented: Binding { + Binding( + get: { self.appContext.tunnelEvent != nil }, + set: { isPresented in + if !isPresented { + self.appContext.dismissTunnelEvent() + } + } + ) + } + } diff --git a/punchnet/Views/Login/LoginView.swift b/punchnet/Views/Login/LoginView.swift index 715934d..8bc1d89 100644 --- a/punchnet/Views/Login/LoginView.swift +++ b/punchnet/Views/Login/LoginView.swift @@ -292,5 +292,5 @@ struct CustomSecureField: View { #Preview { LoginView() - .environment(AppContext(noticePort: 0)) + .environment(AppContext()) } diff --git a/punchnet/punchnetApp.swift b/punchnet/punchnetApp.swift index 64ef2c7..e52d6a3 100644 --- a/punchnet/punchnetApp.swift +++ b/punchnet/punchnetApp.swift @@ -29,7 +29,6 @@ struct punchnetApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - private var noticeServer: UDPNoticeCenterServer @State private var appContext: AppContext @AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false // UI 控制状态:是否显示弹窗 @@ -38,9 +37,7 @@ struct punchnetApp: App { @State private var vpnManager = VPNManager.shared init() { - self.noticeServer = UDPNoticeCenterServer() - self.noticeServer.start() - self.appContext = AppContext(noticePort: self.noticeServer.port) + self.appContext = AppContext() } var body: some Scene {