From a69777018701c21a4cbb95bbb889020588881338 Mon Sep 17 00:00:00 2001 From: anlicheng <244108715@qq.com> Date: Thu, 9 Apr 2026 17:31:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3dns=E7=9A=84=E5=9B=9E?= =?UTF-8?q?=E8=B7=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tun/PacketTunnelProvider.swift | 25 +++- Tun/Punchnet/Actors/SDLContextActor.swift | 27 +++-- Tun/Punchnet/Actors/SDLQuicClient.swift | 1 + Tun/Punchnet/SDLConfiguration.swift | 2 +- Tun/Punchnet/SDLDNSClient.swift | 128 ++++++++++++-------- Tun/Punchnet/TunMessage.pb.swift | 52 ++++++++ punchnet/Networking/SDLAPIClient+User.swift | 2 +- punchnet/Views/Network/NetworkView.swift | 17 +++ 8 files changed, 189 insertions(+), 65 deletions(-) diff --git a/Tun/PacketTunnelProvider.swift b/Tun/PacketTunnelProvider.swift index 0615c23..4474be6 100644 --- a/Tun/PacketTunnelProvider.swift +++ b/Tun/PacketTunnelProvider.swift @@ -60,8 +60,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { // Add code here to handle the message. - if let handler = completionHandler { - handler(messageData) + + Task { + if let message = try? NEMessage(serializedBytes: messageData) { + switch message.message { + case .exitNodeIpChanged(let exitNodeIpChanged): + let exitNodeIp = exitNodeIpChanged.ip + do { + try await self.contextActor?.updateExitNode(exitNodeIp: exitNodeIp) + var reply = NEReply() + reply.code = 0 + reply.message = "操作成功" + completionHandler?(try reply.serializedData()) + + } catch let err { + var reply = NEReply() + reply.code = 1 + reply.message = err.localizedDescription + completionHandler?(try reply.serializedData()) + } + case .none: + () + } + } } } diff --git a/Tun/Punchnet/Actors/SDLContextActor.swift b/Tun/Punchnet/Actors/SDLContextActor.swift index fe00c3b..7311949 100644 --- a/Tun/Punchnet/Actors/SDLContextActor.swift +++ b/Tun/Punchnet/Actors/SDLContextActor.swift @@ -134,8 +134,13 @@ actor SDLContextActor { } } - public func updateSDLConfiguration(config: SDLConfiguration) async throws { - self.config = config + // 取消出口节点的时候,ip地址为: 0.0.0.0 + public func updateExitNode(exitNodeIp: String) async throws { + if let ip = SDLUtil.ipv4StrToInt32(exitNodeIp), ip > 0 { + self.config.exitNode = .init(exitNodeIp: ip) + } else { + self.config.exitNode = nil + } try await self.setNetworkSettings(config: config, dnsServer: SDLDNSClient.Helper.dnsServer) } @@ -225,9 +230,8 @@ actor SDLContextActor { self.dnsWorker = nil // 启动dns服务 - let dnsSocketAddress = try SocketAddress.makeAddressResolvingHost(self.config.serverHost, port: 15353) - let dnsClient = try await SDLDNSClient(dnsServerAddress: dnsSocketAddress, logger: SDLLogger.shared) - try dnsClient.start() + let dnsClient = SDLDNSClient(host: self.config.serverHost, port: 15353, logger: SDLLogger.shared) + dnsClient.start() SDLLogger.shared.log("[SDLContext] dnsClient started") self.dnsClient = dnsClient self.dnsWorker = Task.detached { @@ -662,7 +666,7 @@ actor SDLContextActor { let networkAddr = self.config.networkAddress if SDLDNSClient.Helper.isDnsRequestPacket(ipPacket: packet) { - self.dnsClient?.forward(ipPacket: packet) + self.dnsClient?.forward(ipPacketData: packet.data) return } @@ -768,7 +772,7 @@ actor SDLContextActor { ] // 如果存在出口节点配置,则接管系统默认留有 - if let exitNode = config.exitNode { + if config.exitNode != nil { routes.append(.default()) } @@ -779,15 +783,22 @@ actor SDLContextActor { // 设置网卡的DNS解析 let networkDomain = networkAddress.networkDomain let dnsSettings = NEDNSSettings(servers: [dnsServer]) + dnsSettings.searchDomains = [networkDomain] dnsSettings.matchDomains = [networkDomain] + // 必须设置为 false,否则它会尝试接管全局解析 dnsSettings.matchDomainsNoSearch = false + networkSettings.dnsSettings = dnsSettings let ipv4Settings = NEIPv4Settings(addresses: [networkAddress.ipAddress], subnetMasks: [networkAddress.maskAddress]) // 设置路由表 - //NEIPv4Route.default() ipv4Settings.includedRoutes = routes + // TODO 要排除的路由表 + ipv4Settings.excludedRoutes = [ + + ] + networkSettings.ipv4Settings = ipv4Settings // 网卡配置设置必须成功 try await self.provider.setTunnelNetworkSettings(networkSettings) diff --git a/Tun/Punchnet/Actors/SDLQuicClient.swift b/Tun/Punchnet/Actors/SDLQuicClient.swift index 9112494..a34e934 100644 --- a/Tun/Punchnet/Actors/SDLQuicClient.swift +++ b/Tun/Punchnet/Actors/SDLQuicClient.swift @@ -239,6 +239,7 @@ final class SDLQUICClient { case .event: guard let bytes = buffer.readBytes(length: buffer.readableBytes), let event = try? SDLEvent(serializedBytes: bytes) else { + SDLLogger.shared.log("SDLQUICClient decode Event Error") return nil } return .event(event) diff --git a/Tun/Punchnet/SDLConfiguration.swift b/Tun/Punchnet/SDLConfiguration.swift index bdcb3f5..7b05db4 100644 --- a/Tun/Punchnet/SDLConfiguration.swift +++ b/Tun/Punchnet/SDLConfiguration.swift @@ -74,7 +74,7 @@ public class SDLConfiguration { let accessToken: String let identityId: UInt32 - let exitNode: ExitNode? + var exitNode: ExitNode? public init(version: Int, serverHost: String, diff --git a/Tun/Punchnet/SDLDNSClient.swift b/Tun/Punchnet/SDLDNSClient.swift index e001d14..8afd96e 100644 --- a/Tun/Punchnet/SDLDNSClient.swift +++ b/Tun/Punchnet/SDLDNSClient.swift @@ -1,84 +1,106 @@ // -// DNSClient.swift -// Tun +// SDLDNSClient 2.swift +// punchnet // -// Created by 安礼成 on 2025/12/10. +// Created by 安礼成 on 2026/4/9. // - import Foundation -import NIOCore -import NIOPosix +import Network -// 处理和sn-server服务器之间的通讯 -final class SDLDNSClient: ChannelInboundHandler { - typealias InboundIn = AddressedEnvelope - - private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - private var channel: Channel? +final class SDLDNSClient { + private var connection: NWConnection? private let logger: SDLLogger - private let dnsServerAddress: SocketAddress + private let dnsServerAddress: NWEndpoint + // 用于对外输出收到的 DNS 响应包 public let packetFlow: AsyncStream private let packetContinuation: AsyncStream.Continuation - // 启动函数 - init(dnsServerAddress: SocketAddress, logger: SDLLogger) async throws { - self.dnsServerAddress = dnsServerAddress + // 用来处理关闭事件 + private let (closeStream, closeContinuation) = AsyncStream.makeStream(of: Void.self) + + /// - Parameter host: 你的 sn-server 地址 (如 "8.8.8.8") + /// - Parameter port: 端口 (如 53) + init(host: String, port: UInt16, logger: SDLLogger) { self.logger = logger - (self.packetFlow, self.packetContinuation) = AsyncStream.makeStream(of: Data.self, bufferingPolicy: .unbounded) + self.dnsServerAddress = .hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port)) + + let (stream, continuation) = AsyncStream.makeStream(of: Data.self, bufferingPolicy: .unbounded) + self.packetFlow = stream + self.packetContinuation = continuation } - func start() throws { - let bootstrap = DatagramBootstrap(group: group) - .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) - .channelInitializer { channel in - channel.pipeline.addHandler(self) + func start() { + // 1. 配置参数:这是解决环路的关键 + let parameters = NWParameters.udp + + // 禁止此连接走 TUN 网卡(在 NE 中 TUN 通常被归类为 .other) + parameters.prohibitedInterfaceTypes = [.other] + // 2. 增强健壮性:启用多路径切换(替代 pathSelectionOptions 的意图) + parameters.multipathServiceType = .handover + + // 2. 创建连接 + let connection = NWConnection(to: self.dnsServerAddress, using: parameters) + self.connection = connection + + connection.stateUpdateHandler = { [weak self] state in + switch state { + case .ready: + self?.logger.log("[DNSClient] Connection ready", level: .debug) + self?.receiveLoop() // 开始循环接收数据 + case .failed(let error): + self?.logger.log("[DNSClient] Connection failed: \(error)", level: .error) + self?.stop() + case .cancelled: + self?.packetContinuation.finish() + self?.closeContinuation.finish() + default: + break } + } - let channel = try bootstrap.bind(host: "0.0.0.0", port: 0).wait() - self.logger.log("[DNSClient] started", level: .debug) - self.channel = channel + // 启动连接队列 + connection.start(queue: .global()) } - func waitClose() async throws { - try await self.channel?.closeFuture.get() + public func waitClose() async { + for await _ in closeStream { } } - // --MARK: ChannelInboundHandler delegate - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let envelope = unwrapInboundIn(data) - - var buffer = envelope.data - let remoteAddress = envelope.remoteAddress - self.logger.log("[DNSClient] read data: \(buffer), from: \(remoteAddress)", level: .debug) - - let len = buffer.readableBytes - if let bytes = buffer.readBytes(length: len) { - self.packetContinuation.yield(Data(bytes)) + /// 接收数据的递归循环 + private func receiveLoop() { + connection?.receiveMessage { [weak self] content, _, isComplete, error in + if let data = content, !data.isEmpty { + // 将收到的 DNS 响应写回 AsyncStream + self?.packetContinuation.yield(data) + } + + if error == nil && self?.connection?.state == .ready { + self?.receiveLoop() // 继续监听下一个包 + } } } - func channelInactive(context: ChannelHandlerContext) { - self.packetContinuation.finish() - } - - func forward(ipPacket: IPPacket) { - guard let channel = self.channel else { + /// 发送 DNS 查询包(由 TUN 拦截到的原始 IP 包数据) + func forward(ipPacketData: Data) { + guard let connection = self.connection, connection.state == .ready else { return } - let buffer = channel.allocator.buffer(bytes: ipPacket.data) - let envelope = AddressedEnvelope(remoteAddress: self.dnsServerAddress, data: buffer) - channel.pipeline.eventLoop.execute { - channel.writeAndFlush(envelope, promise: nil) - } + connection.send(content: ipPacketData, completion: .contentProcessed { [weak self] error in + if let error = error { + self?.logger.log("[DNSClient] Send error: \(error)", level: .error) + } + }) + } + + func stop() { + connection?.cancel() + connection = nil } deinit { - try? self.group.syncShutdownGracefully() - self.packetContinuation.finish() + stop() } } diff --git a/Tun/Punchnet/TunMessage.pb.swift b/Tun/Punchnet/TunMessage.pb.swift index fd46a20..e9d316f 100644 --- a/Tun/Punchnet/TunMessage.pb.swift +++ b/Tun/Punchnet/TunMessage.pb.swift @@ -58,6 +58,20 @@ struct NEMessage: Sendable { init() {} } +struct NEReply: 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 code: Int32 = 0 + + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. extension NEMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -139,3 +153,41 @@ extension NEMessage.ExitNodeIpChanged: SwiftProtobuf.Message, SwiftProtobuf._Mes return true } } + +extension NEReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "NEReply" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "code"), + 2: .same(proto: "message"), + ] + + 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.decodeSingularInt32Field(value: &self.code) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.code != 0 { + try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NEReply, rhs: NEReply) -> Bool { + if lhs.code != rhs.code {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/punchnet/Networking/SDLAPIClient+User.swift b/punchnet/Networking/SDLAPIClient+User.swift index c565269..f70b9ea 100644 --- a/punchnet/Networking/SDLAPIClient+User.swift +++ b/punchnet/Networking/SDLAPIClient+User.swift @@ -16,7 +16,7 @@ extension SDLAPIClient { let nodeName: String enum CodingKeys: String, CodingKey { - case nnid + case nnid = "node_id" case nodeName = "node_name" } } diff --git a/punchnet/Views/Network/NetworkView.swift b/punchnet/Views/Network/NetworkView.swift index 3b73c81..b451337 100644 --- a/punchnet/Views/Network/NetworkView.swift +++ b/punchnet/Views/Network/NetworkView.swift @@ -103,6 +103,8 @@ struct NetworkView: View { struct NetworkStatusBar: View { @Environment(AppContext.self) private var appContext @State private var vpnManger = VPNManager.shared + + @State private var exitNodeIp: String = "" var body: some View { let isOnBinding = Binding( @@ -154,6 +156,20 @@ struct NetworkStatusBar: View { Toggle("", isOn: isOnBinding) .toggleStyle(.switch) .controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸 + + + TextField("出口节点:", text: $exitNodeIp) + + Button { + Task { + let result = try await self.appContext.changeExitNodeIp(exitNodeIp: self.exitNodeIp) + let reply = try NEReply(serializedBytes: result) + NSLog("change exit node ip: \(reply)") + } + } label: { + Text("启动出口节点") + } + } .padding(.vertical, 5) } @@ -239,6 +255,7 @@ struct NetworkDisconnectedView: View { do { try await self.appContext.connectNetwork() + try await self.appContext.startTun() } catch let err as SDLAPIError { self.showAlert = true self.errorMessage = err.message