From c8b22188416fb9fdfc1ac6aa9ea26b0073e78c39 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Thu, 9 Apr 2026 10:57:49 +0800 Subject: [PATCH] fix --- Tun/Punchnet/TunMessage.pb.swift | 141 ++++++++++++++++++ punchnet/Core/SystemConfig.swift | 18 ++- .../Networking/SDLAPIClient+Network.swift | 4 - punchnet/Views/AppContext.swift | 61 ++++++-- punchnet/Views/Network/NetworkView.swift | 17 ++- .../Views/Settings/SettingsDeviceView.swift | 14 +- 6 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 Tun/Punchnet/TunMessage.pb.swift diff --git a/Tun/Punchnet/TunMessage.pb.swift b/Tun/Punchnet/TunMessage.pb.swift new file mode 100644 index 0000000..fd46a20 --- /dev/null +++ b/Tun/Punchnet/TunMessage.pb.swift @@ -0,0 +1,141 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: tun_pb.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct NEMessage: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var message: NEMessage.OneOf_Message? = nil + + var exitNodeIpChanged: NEMessage.ExitNodeIpChanged { + get { + if case .exitNodeIpChanged(let v)? = message {return v} + return NEMessage.ExitNodeIpChanged() + } + set {message = .exitNodeIpChanged(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Message: Equatable, Sendable { + case exitNodeIpChanged(NEMessage.ExitNodeIpChanged) + + } + + /// 网络出口ip改变映射变化, 空字符串表示关闭 + struct ExitNodeIpChanged: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var ip: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension NEMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "NEMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "exit_node_ip_changed"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: NEMessage.ExitNodeIpChanged? + var hadOneofValue = false + if let current = self.message { + hadOneofValue = true + if case .exitNodeIpChanged(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.message = .exitNodeIpChanged(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if case .exitNodeIpChanged(let v)? = self.message { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NEMessage, rhs: NEMessage) -> Bool { + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension NEMessage.ExitNodeIpChanged: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = NEMessage.protoMessageName + ".ExitNodeIpChanged" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "ip"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.ip) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.ip.isEmpty { + try visitor.visitSingularStringField(value: self.ip, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NEMessage.ExitNodeIpChanged, rhs: NEMessage.ExitNodeIpChanged) -> Bool { + if lhs.ip != rhs.ip {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/punchnet/Core/SystemConfig.swift b/punchnet/Core/SystemConfig.swift index 693f634..a5088e8 100644 --- a/punchnet/Core/SystemConfig.swift +++ b/punchnet/Core/SystemConfig.swift @@ -29,7 +29,15 @@ struct SystemConfig { return "macOS \(version.majorVersion).\(version.minorVersion)" }() - static func getOptions(networkId: UInt32, networkDomain: String, ip: String, maskLen: UInt8, accessToken: String, identityId: UInt32, hostname: String, noticePort: Int) -> [String: NSObject] { + static func getOptions(networkId: UInt32, + networkDomain: String, + ip: String, + maskLen: UInt8, + 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 { // return nil @@ -38,7 +46,7 @@ struct SystemConfig { let clientId = getClientId() let mac = getMacAddress() - return [ + var options = [ "version": version as NSObject, "client_id": clientId as NSObject, "access_token": accessToken as NSObject, @@ -55,6 +63,12 @@ struct SystemConfig { "network_domain": networkDomain as NSObject ] as NSObject ] + + if let exitNodeIp { + options["exit_node_ip"] = exitNodeIp as NSObject + } + + return options } public static func getClientId() -> String { diff --git a/punchnet/Networking/SDLAPIClient+Network.swift b/punchnet/Networking/SDLAPIClient+Network.swift index a3156da..7126991 100644 --- a/punchnet/Networking/SDLAPIClient+Network.swift +++ b/punchnet/Networking/SDLAPIClient+Network.swift @@ -87,10 +87,6 @@ extension SDLAPIClient { case nodeList = "node_list" } - static func `default`() -> Self { - return .init(ip: "0.0.0.0", maskLen: 24, hostname: "", identityId: 0, resourceList: [], nodeList: []) - } - func getNode(id: Int?) -> Node? { return nodeList.first(where: { $0.id == id }) } diff --git a/punchnet/Views/AppContext.swift b/punchnet/Views/AppContext.swift index 09cb48d..495dee6 100644 --- a/punchnet/Views/AppContext.swift +++ b/punchnet/Views/AppContext.swift @@ -19,9 +19,9 @@ class AppContext { var noticePort: Int // 调用 "/connect" 之后的网络信息 - var networkContext: SDLAPIClient.NetworkContext = .default() + var networkContext: SDLAPIClient.NetworkContext? = nil - // 保存当前登陆vpn使用的配置项 + // 在menu里面需要使用 var vpnOptions: [String: NSObject]? = nil // 当前app所处的场景 @@ -95,7 +95,32 @@ class AppContext { throw AppContextError(message: "网络已经连接") } - let context = try await SDLAPIClient.connectNetwork(accesToken: session.accessToken) + 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, @@ -104,23 +129,21 @@ class AppContext { accessToken: session.accessToken, identityId: context.identityId, hostname: context.hostname, - noticePort: noticePort + noticePort: noticePort, + exitNodeIp: self.loadExitNodeIp() ) - try await self.vpnManager.enableVpn(options: options) - self.networkContext = context - self.vpnOptions = options } // 断开网络连接 - func disconnectNetwork() async throws { + func stopTun() async throws { try await self.vpnManager.disableVpn() } // 退出登陆 func logout() async throws { try await self.vpnManager.disableVpn() - self.networkContext = .default() + self.networkContext = nil self.loginCredit = nil } @@ -141,4 +164,24 @@ 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") + } + } + } diff --git a/punchnet/Views/Network/NetworkView.swift b/punchnet/Views/Network/NetworkView.swift index 14f72b8..3b73c81 100644 --- a/punchnet/Views/Network/NetworkView.swift +++ b/punchnet/Views/Network/NetworkView.swift @@ -110,11 +110,14 @@ struct NetworkStatusBar: View { set: { newValue in if newValue { Task { - try? await self.appContext.connectNetwork() + if self.appContext.networkContext == nil { + try? await self.appContext.connectNetwork() + } + try? await self.appContext.startTun() } } else { Task { - try? await self.appContext.disconnectNetwork() + try? await self.appContext.stopTun() } } } @@ -139,7 +142,7 @@ struct NetworkStatusBar: View { Text(networkSession.networkName) .font(.system(size: 12, weight: .semibold)) - Text("局域网IP: \(appContext.networkContext.ip)") + Text("局域网IP: \(appContext.networkContext?.ip ?? "0.0.0.0")") .font(.system(size: 10, design: .monospaced)) .foregroundColor(.secondary) } @@ -170,7 +173,7 @@ struct NetworkConnectedView: View { GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8) ], spacing: 10) { - ForEach(appContext.networkContext.resourceList, id: \.uuid) { res in + ForEach(appContext.networkContext?.resourceList ?? [], id: \.uuid) { res in ResourceItemCard(resource: res) } } @@ -266,7 +269,7 @@ struct NetworkDeviceGroupView: View { // 如果你的 WindowStyle 是 .hiddenTitleBar,这个 Padding 非常重要 Color.clear.frame(height: 28) - List(appContext.networkContext.nodeList, id: \.id, selection: $selectedId) { node in + List(appContext.networkContext?.nodeList ?? [], id: \.id, selection: $selectedId) { node in NetworkNodeHeadView(node: node) // 技巧:在 HStack 方案中,tag 配合 List 的 selection 依然有效 .tag(node.id) @@ -281,7 +284,7 @@ struct NetworkDeviceGroupView: View { // --- 2. 详情区域 (Detail) --- ZStack { - if let selectedNode = appContext.networkContext.getNode(id: selectedId) { + if let selectedNode = appContext.networkContext?.getNode(id: selectedId) { NetworkNodeDetailView(node: selectedNode) .transition(.opacity.animation(.easeInOut(duration: 0.2))) } else { @@ -298,7 +301,7 @@ struct NetworkDeviceGroupView: View { .ignoresSafeArea() // 真正顶到最上方 .onAppear { if selectedId == nil { - selectedId = appContext.networkContext.firstNodeId() + selectedId = appContext.networkContext?.firstNodeId() } } } diff --git a/punchnet/Views/Settings/SettingsDeviceView.swift b/punchnet/Views/Settings/SettingsDeviceView.swift index c8a52fc..a08ae26 100644 --- a/punchnet/Views/Settings/SettingsDeviceView.swift +++ b/punchnet/Views/Settings/SettingsDeviceView.swift @@ -23,10 +23,14 @@ struct SettingsDeviceView: View { .background(Color.blue.opacity(0.1)) .cornerRadius(12) - // TODO VStack(alignment: .leading, spacing: 4) { - Text(self.appContext.networkContext.hostname) - .font(.title3.bold()) + if let networkContext = self.appContext.networkContext { + Text(networkContext.hostname) + .font(.title3.bold()) + } else { + Text("未知") + .font(.title3.bold()) + } Text(SystemConfig.systemInfo) .font(.subheadline) @@ -38,7 +42,7 @@ struct SettingsDeviceView: View { // MARK: - 详细参数卡片 VStack(alignment: .leading, spacing: 0) { // 设备名称行 - DevicePropertyRow(title: "设备名称", value: self.appContext.networkContext.hostname) { + DevicePropertyRow(title: "设备名称", value: self.appContext.networkContext?.hostname ?? "未知") { Button { // 修改逻辑 } label: { @@ -55,7 +59,7 @@ struct SettingsDeviceView: View { Divider().padding(.leading, 16) // IPv4 行 - DevicePropertyRow(title: "虚拟 IPv4", value: self.appContext.networkContext.ip) { + DevicePropertyRow(title: "虚拟 IPv4", value: self.appContext.networkContext?.ip ?? "0.0.0.0") { Image(systemName: "info.circle") .foregroundColor(.secondary) }