diff --git a/Tun/Punchnet/Actors/SDLContextActor.swift b/Tun/Punchnet/Actors/SDLContextActor.swift index c00306d..c9d81b2 100644 --- a/Tun/Punchnet/Actors/SDLContextActor.swift +++ b/Tun/Punchnet/Actors/SDLContextActor.swift @@ -142,14 +142,14 @@ actor SDLContextActor { self.quicClient?.stop() // 启动monitor - let quicClient = SDLQUICClient(host: self.config.serverIp, port: 443) + let quicClient = SDLQUICClient(host: self.config.serverHost, port: 443) quicClient.start() // 等待quic准备好 try await quicClient.waitReady() // 这里必须等待quic的协商完成 try await Task.sleep(for: .seconds(0.2)) - SDLLogger.shared.log("[SDLContext] start quic client ready") + SDLLogger.shared.log("[SDLContext] start quic client: \(self.config.serverHost)") self.quicWorker = Task.detached { for await message in quicClient.messageStream { diff --git a/Tun/Punchnet/Actors/SDLQuicClient.swift b/Tun/Punchnet/Actors/SDLQuicClient.swift index 8cf80fa..c5c9eab 100644 --- a/Tun/Punchnet/Actors/SDLQuicClient.swift +++ b/Tun/Punchnet/Actors/SDLQuicClient.swift @@ -8,6 +8,8 @@ import Foundation import NIOCore import Network +import CryptoKit +import Security // 定义错误类型,便于上层处理 enum SDLQUICError: Error { @@ -47,8 +49,8 @@ final class SDLQUICClient { sec_protocol_options_set_verify_block( options.securityProtocolOptions, { metadata, trust, complete in - // 你可以自己决定是否信任 - complete(true) // true = 接受证书 + // 执行公钥校验 + complete(QUICVerifier.verify(trust: trust)) }, self.queue ) @@ -277,3 +279,50 @@ final class SDLQUICClient { self.messageCont.finish() } } + +extension SDLQUICClient { + + enum QUICVerifier { + // 你的 Base64 公钥指纹 + static let pinnedPublicKeyHashes = [ + "Z2S0VNhlCqelkqTXxZNY39icMu622SfKhxXi3qi5fFA=" + ] + + static func verify(trust: sec_trust_t) -> Bool { + let secTrust = sec_trust_copy_ref(trust).takeRetainedValue() + + // --- 修复部分:使用 macOS 12+ 的新 API --- + // SecTrustCopyCertificateChain 会返回一个 CFArray,包含整个证书链 + guard let chain = SecTrustCopyCertificateChain(secTrust) as? [SecCertificate], + let leafCertificate = chain.first else { + SDLLogger.shared.log("❌ 无法获取证书链或叶子证书") + return false + } + + // 提取公钥 + guard let publicKey = SecCertificateCopyKey(leafCertificate) else { + SDLLogger.shared.log("❌ 无法从证书中提取公钥") + return false + } + + // 导出公钥原始数据 (SubjectPublicKeyInfo) + guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else { + SDLLogger.shared.log("❌ 无法导出公钥数据") + return false + } + + // 计算哈希并比对 + let hash = SHA256.hash(data: publicKeyData) + let hashBase64 = Data(hash).base64EncodedString() + + if pinnedPublicKeyHashes.contains(hashBase64) { + SDLLogger.shared.log("✅ 公钥校验通过") + return true + } else { + SDLLogger.shared.log("⚠️ 公钥不匹配! 收到: \(hashBase64)") + return false + } + } + + } +} diff --git a/Tun/Punchnet/SDLConfiguration.swift b/Tun/Punchnet/SDLConfiguration.swift index ba5d25b..2763f0f 100644 --- a/Tun/Punchnet/SDLConfiguration.swift +++ b/Tun/Punchnet/SDLConfiguration.swift @@ -42,6 +42,7 @@ public class SDLConfiguration { let version: Int let serverIp: String + let serverHost: String let stunServers: [String] let noticePort: Int @@ -69,6 +70,7 @@ public class SDLConfiguration { public init(version: Int, serverIp: String, + serverHost: String, stunServers: [String], clientId: String, networkAddress: NetworkAddress, @@ -79,6 +81,7 @@ public class SDLConfiguration { self.version = version self.serverIp = serverIp + self.serverHost = serverHost self.stunServers = stunServers self.clientId = clientId self.networkAddress = networkAddress @@ -95,6 +98,7 @@ extension SDLConfiguration { static func parse(options: [String: NSObject]) -> SDLConfiguration? { guard let version = options["version"] as? Int, let serverIp = options["server_ip"] as? String, + let serverHost = options["server_host"] as? String, let stunAssistIp = options["stun_assist_ip"] as? String, let noticePort = options["notice_port"] as? Int, let accessToken = options["access_token"] as? String, @@ -116,6 +120,7 @@ extension SDLConfiguration { return SDLConfiguration(version: version, serverIp: serverIp, + serverHost: serverHost, stunServers: stunServers, clientId: clientId, networkAddress: networkAddress, diff --git a/punchnet/Core/SystemConfig.swift b/punchnet/Core/SystemConfig.swift index 1b8d02a..1d61403 100644 --- a/punchnet/Core/SystemConfig.swift +++ b/punchnet/Core/SystemConfig.swift @@ -18,10 +18,10 @@ struct SystemConfig { // 渠道相关 static let channel = "appstore" - static let serverHost = "punchnet.s5s8.com" + static let serverHost = "root.punchsky.com" // stun探测辅助服务器ip - static let stunAssistHost = "punchnet.s5s8.com" + static let stunAssistHost = "root.punchsky.com" // 获取系统信息 static let systemInfo: String = { @@ -30,10 +30,13 @@ struct SystemConfig { }() static func getOptions(networkId: UInt32, networkDomain: String, ip: String, maskLen: UInt8, accessToken: String, identityId: UInt32, hostname: String, noticePort: Int) -> [String: NSObject]? { - guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first else { + guard let serverIp = DNSResolver.resolveAddrInfos(serverHost).first, + let stunAssistIp = DNSResolver.resolveAddrInfos(stunAssistHost).first else { return nil } + NSLog("[SystemConfig] server_ip: \(serverIp), stun_assist_ip: \(stunAssistIp)") + let clientId = getClientId() let mac = getMacAddress() @@ -42,8 +45,9 @@ struct SystemConfig { "client_id": clientId as NSObject, "access_token": accessToken as NSObject, "identity_id": identityId as NSObject, - "server_ip": "118.178.229.213" as NSObject, - "stun_assist_ip": "118.178.229.213" as NSObject, + "server_ip": serverIp as NSObject, + "server_host": serverHost as NSObject, + "stun_assist_ip": stunAssistIp as NSObject, "hostname": hostname as NSObject, "notice_port": noticePort as NSObject, "network_address": [ diff --git a/punchnet/Networking/SDLAPIClient.swift b/punchnet/Networking/SDLAPIClient.swift index 188096f..3ebe160 100644 --- a/punchnet/Networking/SDLAPIClient.swift +++ b/punchnet/Networking/SDLAPIClient.swift @@ -20,7 +20,10 @@ struct SDLAPIError: Error, Decodable { struct SDLAPIClient { - static var baseUrl: String = "https://punchnet.s5s8.com/api" + static var baseUrl: String { + return "https://\(SystemConfig.serverHost)/api" + } + static private let token: String = "H6p*2RfEu4ITcL" // 基础参数信息 @@ -73,8 +76,6 @@ struct SDLAPIClient { return "\(key)=\(str)" }.joined(separator: "&") - NSLog("sign qs is: \(qs), sign: \(SDLUtil.hmacMD5(key: token, data: qs))") - return SDLUtil.hmacMD5(key: token, data: qs) } diff --git a/punchnet/Views/AppContext.swift b/punchnet/Views/AppContext.swift index 31b8c5b..8290308 100644 --- a/punchnet/Views/AppContext.swift +++ b/punchnet/Views/AppContext.swift @@ -15,6 +15,9 @@ class AppContext { // 调用 "/connect" 之后的网络信息 var networkContext: SDLAPIClient.NetworkContext = .default() + // 保存当前登陆vpn使用的配置项 + var vpnOptions: [String: NSObject]? = nil + // 当前app所处的场景 var loginScene: LoginScene = .login(username: nil) @@ -88,6 +91,7 @@ class AppContext { ) { try await VPNManager.shared.enableVpn(options: options) self.networkContext = context + self.vpnOptions = options } } diff --git a/punchnet/Views/Login/LoginView.swift b/punchnet/Views/Login/LoginView.swift index 8757f4b..7b3ce6f 100644 --- a/punchnet/Views/Login/LoginView.swift +++ b/punchnet/Views/Login/LoginView.swift @@ -89,6 +89,10 @@ struct LoginAccountView: View { @State private var password: String = "" @State private var isLoading = false + // 1. 引入环境操作 + @Environment(\.dismiss) private var dismiss + @Environment(\.openWindow) private var openWindow + // 错误提示 @State private var showAlert: Bool = false @State private var errorMessage: String = "" @@ -169,8 +173,12 @@ struct LoginAccountView: View { do { _ = try await appContext.loginWith(username: username, password: password) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { - //self.appContext.appScene = .logined + // 2. 先打开新窗口(顺序通常不影响,但先打开更稳妥) + openWindow(id: "logined") + // 3. 销毁当前窗口 + dismiss() } + } catch let err as SDLAPIError { self.showAlert = true self.errorMessage = err.message @@ -184,6 +192,10 @@ struct LoginAccountView: View { // MARK: - 密钥登录组件 struct LoginTokenView: View { @Environment(AppContext.self) var appContext: AppContext + + // 1. 引入环境操作 + @Environment(\.dismiss) private var dismiss + @Environment(\.openWindow) private var openWindow @State private var token = "" @State private var isLoading = false @@ -230,7 +242,10 @@ struct LoginTokenView: View { do { _ = try await appContext.loginWith(token: token) withAnimation(.spring(duration: 0.6, bounce: 0.2)) { - //self.appContext.appScene = .logined + // 2. 先打开新窗口(顺序通常不影响,但先打开更稳妥) + openWindow(id: "logined") + // 3. 销毁当前窗口 + dismiss() } } catch let err as SDLAPIError { self.showAlert = true diff --git a/punchnet/punchnetApp.swift b/punchnet/punchnetApp.swift index 2fb5f1f..e618aac 100644 --- a/punchnet/punchnetApp.swift +++ b/punchnet/punchnetApp.swift @@ -36,6 +36,8 @@ struct punchnetApp: App { // UI 控制状态:是否显示弹窗 @State private var showPrivacy: Bool = false + @State private var vpnManager = VPNManager.shared + init() { self.noticeServer = UDPNoticeCenterServer() self.noticeServer.start() @@ -52,15 +54,15 @@ struct punchnetApp: App { } .sheet(isPresented: $showPrivacy) { PrivacyDetailView(showPrivacy: $showPrivacy) - //.interactiveDismissDisabled() // 强制阅读 + .interactiveDismissDisabled() // 强制阅读 } } - .windowResizability(.contentSize) .windowToolbarStyle(.unified) + .windowResizability(.contentSize) .defaultPosition(.center) Window("网络", id: "logined") { - SettingsView() + NetworkView() .environment(self.appContext) .frame(width: 750, height: 500) } @@ -74,33 +76,52 @@ struct punchnetApp: App { } .windowResizability(.contentSize) .defaultPosition(.center) - -// -// MenuBarExtra("punchnet", image: "logo_32") { -// VStack { -// Button(action: { -// self.menuClick() -// }, label: { -// Text("启动") -// }) -// -// Divider() -// -// Button(action: { -// NSApplication.shared.terminate(nil) -// }, label: { Text("退出") }) -// -// } -// } -// .menuBarExtraStyle(.menu) + + MenuBarExtra("punchnet", image: "logo_32") { + VStack { + + switch self.vpnManager.vpnStatus { + case .connected: + Button(action: { + Task { @MainActor in + try await vpnManager.disableVpn() + } + }, label: { + Text("停止") + }) + case .disconnected: + Button(action: { + Task { @MainActor in + await self.startVPN() + } + }, label: { + Text("启动") + }) + } + + Divider() + + Button(action: { + NSApplication.shared.terminate(nil) + }, label: { + Text("退出应用") + }) + + } + } + .menuBarExtraStyle(.menu) } - private func menuClick() { - + private func startVPN() async { + if let options = appContext.vpnOptions { + try? await vpnManager.enableVpn(options: options) + } else { + openWindow(id: "login") + } } + } - // 处理APP的生命周期 class AppDelegate: NSObject, NSApplicationDelegate { @@ -113,12 +134,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { Task { do { try await VPNManager.shared.disableVpn() + NSLog("vpn disabled") } catch let err { - print("退出时关闭 VPN 失败: \(err)") + NSLog("退出时关闭 VPN 失败: \(err)") } - print("call me here termi") - await MainActor.run { sender.reply(toApplicationShouldTerminate: true) }