解决NE到主App的通讯问题

This commit is contained in:
anlicheng 2026-04-15 11:01:17 +08:00
parent 98386ded25
commit 7e2e744bdb
9 changed files with 150 additions and 75 deletions

View File

@ -21,6 +21,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac") let shared = UserDefaults(suiteName: "group.com.jihe.punchnetmac")
let msg = shared?.string(forKey: "test_msg") let msg = shared?.string(forKey: "test_msg")
SDLLogger.log("NE read message: \(msg ?? "failed")", for: .debug) SDLLogger.log("NE read message: \(msg ?? "failed")", for: .debug)
SDLTunnelAppNotifier.shared.clear()
SDLNotificationCenter.shared.post(.vpnStatusChanged) SDLNotificationCenter.shared.post(.vpnStatusChanged)

View File

@ -70,7 +70,7 @@ actor SDLContextActor {
private var monitorWorker: Task<Void, Never>? private var monitorWorker: Task<Void, Never>?
// socket // socket
private var noticeClient: SDLNoticeClient? // App Group + Darwin Notification
// //
nonisolated private let flowTracer = SDLFlowTracer() nonisolated private let flowTracer = SDLFlowTracer()
@ -114,6 +114,7 @@ actor SDLContextActor {
} }
self.readyState = .starting self.readyState = .starting
self.prepareTunnelNotifier()
self.startMonitor() self.startMonitor()
// arp // arp
@ -129,14 +130,7 @@ actor SDLContextActor {
await quicClient.waitClose() await quicClient.waitClose()
SDLLogger.log("[SDLContext] quicClient closed!!!!") 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") { await self.supervisor.addWorker(name: "udpHole") {
let udpHole = try await self.startUDPHole() let udpHole = try await self.startUDPHole()
SDLLogger.log("[SDLContext] udp running!!!!") SDLLogger.log("[SDLContext] udp running!!!!")
@ -217,15 +211,12 @@ actor SDLContextActor {
return quicClient return quicClient
} }
private func startNoticeClient() throws -> SDLNoticeClient { private func prepareTunnelNotifier() {
// noticeClient // noticeClient
let noticeClient = try SDLNoticeClient(noticePort: self.config.noticePort) // UDP NoticeClient App Group
noticeClient.start() SDLTunnelAppNotifier.shared.clear()
SDLLogger.log("[SDLContext] noticeClient started") SDLLogger.log("[SDLContext] tunnelAppNotifier ready")
self.noticeClient = noticeClient
return noticeClient
} }
private func startMonitor() { private func startMonitor() {
@ -386,11 +377,15 @@ actor SDLContextActor {
self.updatePolicyTask?.cancel() self.updatePolicyTask?.cancel()
self.updatePolicyTask = nil self.updatePolicyTask = nil
self.noticeClient?.stop()
self.noticeClient = nil
self.sessionToken = nil self.sessionToken = nil
self.dataCipher = 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) { private func setNatType(natType: SDLNATProberActor.NatType) {
self.natType = natType self.natType = natType
@ -711,8 +706,7 @@ extension SDLContextActor {
switch errorCode { switch errorCode {
case .invalidToken, .nodeDisabled: case .invalidToken, .nodeDisabled:
self.superRegistrationStateMachine.handleFailure() self.superRegistrationStateMachine.handleFailure()
let alertNotice = NoticeMessage.alert(alert: errorMessage) self.publishTunnelEvent(level: .fatal, code: Int(errorCode.rawValue), message: errorMessage)
self.noticeClient?.send(data: alertNotice)
// 退 // 退
let error = NSError(domain: "com.jihe.punchnet.tun", code: -1) let error = NSError(domain: "com.jihe.punchnet.tun", code: -1)
self.failReady(error) self.failReady(error)
@ -720,8 +714,7 @@ extension SDLContextActor {
case .noIpAddress, .networkFault, .internalFault: case .noIpAddress, .networkFault, .internalFault:
self.superRegistrationStateMachine.handleRetryableNak() self.superRegistrationStateMachine.handleRetryableNak()
let alertNotice = NoticeMessage.alert(alert: errorMessage) self.publishTunnelEvent(level: .warning, code: Int(errorCode.rawValue), message: errorMessage)
self.noticeClient?.send(data: alertNotice)
} }
SDLLogger.log("[SDLContext] Get a SuperNak message exit") SDLLogger.log("[SDLContext] Get a SuperNak message exit")
@ -742,8 +735,7 @@ extension SDLContextActor {
self.sendPeerPacket(type: .register, data: registerData, remoteAddress: remoteAddress) self.sendPeerPacket(type: .register, data: registerData, remoteAddress: remoteAddress)
} }
case .shutdown(let message): case .shutdown(let message):
let alertNotice = NoticeMessage.alert(alert: message) self.publishTunnelEvent(level: .fatal, message: message)
self.noticeClient?.send(data: alertNotice)
// 退 // 退
let error = NSError(domain: "com.jihe.punchnet.tun", code: -2) let error = NSError(domain: "com.jihe.punchnet.tun", code: -2)

View File

@ -52,8 +52,6 @@ public class SDLConfiguration {
let serverIp: String let serverIp: String
let stunServers: [String] let stunServers: [String]
let noticePort: Int
lazy var stunSocketAddress: SocketAddress = { lazy var stunSocketAddress: SocketAddress = {
let stunServer = stunServers[0] let stunServer = stunServers[0]
return try! SocketAddress.makeAddressResolvingHost(stunServer, port: 1365) return try! SocketAddress.makeAddressResolvingHost(stunServer, port: 1365)
@ -84,7 +82,6 @@ public class SDLConfiguration {
clientId: String, clientId: String,
networkAddress: NetworkAddress, networkAddress: NetworkAddress,
hostname: String, hostname: String,
noticePort: Int,
accessToken: String, accessToken: String,
identityId: UInt32, identityId: UInt32,
exitNode: ExitNode?) { exitNode: ExitNode?) {
@ -94,7 +91,6 @@ public class SDLConfiguration {
self.stunServers = stunServers self.stunServers = stunServers
self.clientId = clientId self.clientId = clientId
self.networkAddress = networkAddress self.networkAddress = networkAddress
self.noticePort = noticePort
self.accessToken = accessToken self.accessToken = accessToken
self.identityId = identityId self.identityId = identityId
self.hostname = hostname self.hostname = hostname
@ -110,7 +106,6 @@ extension SDLConfiguration {
guard let version = options["version"] as? Int, guard let version = options["version"] as? Int,
let serverHost = options["server_host"] as? String, let serverHost = options["server_host"] as? String,
let stunAssistHost = options["stun_assist_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 accessToken = options["access_token"] as? String,
let identityId = options["identity_id"] as? UInt32, let identityId = options["identity_id"] as? UInt32,
let clientId = options["client_id"] as? String, let clientId = options["client_id"] as? String,
@ -141,7 +136,6 @@ extension SDLConfiguration {
clientId: clientId, clientId: clientId,
networkAddress: networkAddress, networkAddress: networkAddress,
hostname: hostname, hostname: hostname,
noticePort: noticePort,
accessToken: accessToken, accessToken: accessToken,
identityId: identityId, identityId: identityId,
exitNode: exitNode) exitNode: exitNode)

View File

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

View File

@ -36,7 +36,6 @@ struct SystemConfig {
accessToken: String, accessToken: String,
identityId: UInt32, identityId: UInt32,
hostname: String, hostname: String,
noticePort: Int,
exitNodeIp: String?) -> [String: NSObject] { exitNodeIp: String?) -> [String: NSObject] {
// guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first, // guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first,
// let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else { // let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else {
@ -54,7 +53,6 @@ struct SystemConfig {
"server_host": serverHost as NSObject, "server_host": serverHost as NSObject,
"stun_assist_host": stunAssistHost as NSObject, "stun_assist_host": stunAssistHost as NSObject,
"hostname": hostname as NSObject, "hostname": hostname as NSObject,
"notice_port": noticePort as NSObject,
"network_address": [ "network_address": [
"network_id": networkId as NSObject, "network_id": networkId as NSObject,
"ip": ip as NSObject, "ip": ip as NSObject,

View File

@ -15,18 +15,16 @@ struct AppContextError: Error {
@Observable @Observable
class AppContext { class AppContext {
private var vpnManager = VPNManager.shared private var vpnManager = VPNManager.shared
var noticePort: Int
// "/connect" // "/connect"
var networkContext: SDLAPIClient.NetworkContext? = nil var networkContext: SDLAPIClient.NetworkContext? = nil
// menu使 // menu使
var vpnOptions: [String: NSObject]? = nil var vpnOptions: [String: NSObject]? = nil
// app // app
var appScene: AppScene = .login(username: nil) var appScene: AppScene = .login(username: nil)
// //
enum AppScene: Equatable { enum AppScene: Equatable {
case login(username: String?) case login(username: String?)
@ -34,26 +32,26 @@ class AppContext {
case register case register
case resetPassword case resetPassword
} }
// //
var loginCredit: Credit? var loginCredit: Credit?
// //
var isLogined: Bool { var isLogined: Bool {
return loginCredit != nil return loginCredit != nil
} }
enum Credit { enum Credit {
case token(token: String, session: SDLAPIClient.NetworkSession) case token(token: String, session: SDLAPIClient.NetworkSession)
case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession) case accountAndPasword(account: String, password: String, session: SDLAPIClient.NetworkSession)
} }
@ObservationIgnored @ObservationIgnored
var networkSession: SDLAPIClient.NetworkSession? { var networkSession: SDLAPIClient.NetworkSession? {
guard let loginCredit = self.loginCredit else { guard let loginCredit = self.loginCredit else {
return nil return nil
} }
switch loginCredit { switch loginCredit {
case .token(_, let session): case .token(_, let session):
return session return session
@ -61,11 +59,17 @@ class AppContext {
return session return session
} }
} }
init(noticePort: Int) { var tunnelEvent: SDLTunnelAppEventStore.Event? = nil
self.noticePort = noticePort
@ObservationIgnored
private var lastTunnelEventID: String?
init() {
self.observeTunnelEvent()
self.consumeLatestTunnelEvent()
} }
func loginWith(token: String) async throws { func loginWith(token: String) async throws {
let networkSession = try await SDLAPIClient.loginWithToken(token: token) let networkSession = try await SDLAPIClient.loginWithToken(token: token)
self.loginCredit = .token(token: token, session: networkSession) self.loginCredit = .token(token: token, session: networkSession)
@ -83,44 +87,44 @@ class AppContext {
try KeychainStore.shared.save(data, account: "accountAndPasword") try KeychainStore.shared.save(data, account: "accountAndPasword")
} }
} }
// //
func connectNetwork() async throws { func connectNetwork() async throws {
guard let session = self.networkSession else { guard let session = self.networkSession else {
throw AppContextError(message: "未登陆") throw AppContextError(message: "未登陆")
} }
// //
guard !vpnManager.isConnected else { guard !vpnManager.isConnected else {
throw AppContextError(message: "网络已经连接") throw AppContextError(message: "网络已经连接")
} }
self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken) self.networkContext = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken)
} }
func changeExitNodeIp(exitNodeIp: String) async throws -> Data { func changeExitNodeIp(exitNodeIp: String) async throws -> Data {
// //
guard vpnManager.isConnected else { guard vpnManager.isConnected else {
throw AppContextError(message: "网络未连接") throw AppContextError(message: "网络未连接")
} }
var exitNodeIpChanged = NEMessage.ExitNodeIpChanged() var exitNodeIpChanged = NEMessage.ExitNodeIpChanged()
exitNodeIpChanged.ip = exitNodeIp exitNodeIpChanged.ip = exitNodeIp
var neMessage = NEMessage() var neMessage = NEMessage()
neMessage.message = .exitNodeIpChanged(exitNodeIpChanged) neMessage.message = .exitNodeIpChanged(exitNodeIpChanged)
let message = try neMessage.serializedData() let message = try neMessage.serializedData()
return try await self.vpnManager.sendMessage(message) return try await self.vpnManager.sendMessage(message)
} }
// tun // tun
func startTun() async throws { func startTun() async throws {
guard let session = self.networkSession, let context = self.networkContext else { guard let session = self.networkSession, let context = self.networkContext else {
return return
} }
let options = SystemConfig.getOptions( let options = SystemConfig.getOptions(
networkId: UInt32(session.networkId), networkId: UInt32(session.networkId),
networkDomain: session.networkDomain, networkDomain: session.networkDomain,
@ -129,12 +133,11 @@ class AppContext {
accessToken: session.accessToken, accessToken: session.accessToken,
identityId: context.identityId, identityId: context.identityId,
hostname: context.hostname, hostname: context.hostname,
noticePort: noticePort,
exitNodeIp: self.loadExitNodeIp() exitNodeIp: self.loadExitNodeIp()
) )
try await self.vpnManager.enableVpn(options: options) try await self.vpnManager.enableVpn(options: options)
} }
// //
func stopTun() async throws { func stopTun() async throws {
try await self.vpnManager.disableVpn() try await self.vpnManager.disableVpn()
@ -146,14 +149,14 @@ class AppContext {
self.networkContext = nil self.networkContext = nil
self.loginCredit = nil self.loginCredit = nil
} }
func loadCacheToken() -> String? { func loadCacheToken() -> String? {
if let data = try? KeychainStore.shared.load(account: "token") { if let data = try? KeychainStore.shared.load(account: "token") {
return String(data: data, encoding: .utf8) return String(data: data, encoding: .utf8)
} }
return nil return nil
} }
func loadCacheUsernameAndPassword() -> (String, String)? { func loadCacheUsernameAndPassword() -> (String, String)? {
if let data = try? KeychainStore.shared.load(account: "accountAndPasword"), if let data = try? KeychainStore.shared.load(account: "accountAndPasword"),
let str = String(data: data, encoding: .utf8) { let str = String(data: data, encoding: .utf8) {
@ -164,24 +167,49 @@ class AppContext {
} }
return nil return nil
} }
} }
// //
extension AppContext { extension AppContext {
func loadExitNodeIp() -> String? { func loadExitNodeIp() -> String? {
if let data = try? KeychainStore.shared.load(account: "exitNodeIp") { if let data = try? KeychainStore.shared.load(account: "exitNodeIp") {
return String(data: data, encoding: .utf8) return String(data: data, encoding: .utf8)
} }
return nil return nil
} }
func saveExitNodeIp(exitNodeIp: String) async throws { func saveExitNodeIp(exitNodeIp: String) async throws {
// keychain // keychain
if let data = exitNodeIp.data(using: .utf8) { if let data = exitNodeIp.data(using: .utf8) {
try KeychainStore.shared.save(data, account: "exitNodeIp") 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)
}
} }

View File

@ -10,7 +10,7 @@ import SwiftUI
struct AppRootView: View { struct AppRootView: View {
@Environment(AppContext.self) var appContext: AppContext @Environment(AppContext.self) var appContext: AppContext
@State private var updateManager = AppUpdateManager.shared @State private var updateManager = AppUpdateManager.shared
var body: some View { var body: some View {
ZStack { ZStack {
// 1. // 1.
@ -35,7 +35,7 @@ struct AppRootView: View {
insertion: .move(edge: .trailing).combined(with: .opacity), insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity) // leading removal: .move(edge: .leading).combined(with: .opacity) // leading
)) ))
// //
if updateManager.showUpdateOverlay, let info = updateManager.updateInfo { if updateManager.showUpdateOverlay, let info = updateManager.updateInfo {
updateOverlay(info: info) updateOverlay(info: info)
@ -46,12 +46,19 @@ struct AppRootView: View {
.animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay) .animation(.spring(duration: 0.4), value: updateManager.showUpdateOverlay)
// macOS // macOS
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) .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 { .task {
let checkUpdateResult = await updateManager.checkUpdate(isManual: false) let checkUpdateResult = await updateManager.checkUpdate(isManual: false)
NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)") NSLog("[RootView] checkUpdateResult: \(checkUpdateResult)")
} }
} }
// body // body
@ViewBuilder @ViewBuilder
private func updateOverlay(info: SDLAPIClient.AppUpgradeInfo) -> some View { private func updateOverlay(info: SDLAPIClient.AppUpgradeInfo) -> some View {
@ -63,7 +70,7 @@ struct AppRootView: View {
updateManager.showUpdateOverlay = false updateManager.showUpdateOverlay = false
} }
} }
AppUpdateView(info: info) { AppUpdateView(info: info) {
updateManager.showUpdateOverlay = false updateManager.showUpdateOverlay = false
} }
@ -76,5 +83,16 @@ struct AppRootView: View {
)) ))
.zIndex(100) // .zIndex(100) //
} }
private var tunnelEventPresented: Binding<Bool> {
Binding(
get: { self.appContext.tunnelEvent != nil },
set: { isPresented in
if !isPresented {
self.appContext.dismissTunnelEvent()
}
}
)
}
} }

View File

@ -292,5 +292,5 @@ struct CustomSecureField: View {
#Preview { #Preview {
LoginView() LoginView()
.environment(AppContext(noticePort: 0)) .environment(AppContext())
} }

View File

@ -29,7 +29,6 @@ struct punchnetApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var noticeServer: UDPNoticeCenterServer
@State private var appContext: AppContext @State private var appContext: AppContext
@AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false @AppStorage("hasAcceptedPrivacy") var hasAcceptedPrivacy: Bool = false
// UI // UI
@ -38,9 +37,7 @@ struct punchnetApp: App {
@State private var vpnManager = VPNManager.shared @State private var vpnManager = VPNManager.shared
init() { init() {
self.noticeServer = UDPNoticeCenterServer() self.appContext = AppContext()
self.noticeServer.start()
self.appContext = AppContext(noticePort: self.noticeServer.port)
} }
var body: some Scene { var body: some Scene {