// // SDLDNSClient 2.swift // punchnet // // Created by 安礼成 on 2026/4/9. // import Foundation import Network actor DNSCloudClient { enum DNSCloudError: Error { case failed(Error) case cancelled case sendFailed(Error) case invalidData } private enum State { case idle case starting case running case stopped } private var state: State = .idle private let queue = DispatchQueue(label: "com.sdl.DNSCloudClient.queue") private var connection: NWConnection? private let dnsServerAddress: NWEndpoint // 用于对外输出收到的 DNS 响应包 nonisolated let packetFlow: AsyncThrowingStream private let packetContinuation: AsyncThrowingStream.Continuation private var isPacketContinuationFinished: Bool = false private let readySignal = AsyncOneShot() /// - Parameter serverIP: 你的 sn-server IP 地址 (如 "8.8.8.8") /// - Parameter port: 端口 (如 53) init(serverIP: String, port: UInt16) { self.dnsServerAddress = .hostPort(host: Self.makeEndpointHost(address: serverIP), port: NWEndpoint.Port(integerLiteral: port)) let packetPair = AsyncThrowingStream.makeStream(of: Data.self) self.packetFlow = packetPair.stream self.packetContinuation = packetPair.continuation } private static func makeEndpointHost(address ip: String) -> NWEndpoint.Host { if let ipv4Address = IPv4Address(ip) { return .ipv4(ipv4Address) } if let ipv6Address = IPv6Address(ip) { return .ipv6(ipv6Address) } preconditionFailure("invalid DNS cloud server IP: \(ip)") } func run() async throws { guard self.state == .idle else { return } self.state = .starting // 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) connection.stateUpdateHandler = { [weak self] state in Task { await self?.handleConnectionStateUpdate(state, for: connection) } } self.connection = connection // 启动连接队列 connection.start(queue: self.queue) try await withTaskCancellationHandler { try await self.readySignal.wait() while true { try Task.checkCancellation() let data = try await self.readOnce() self.packetContinuation.yield(data) } } onCancel: { connection.cancel() } } /// 发送 DNS 查询包(由 TUN 拦截到的原始 IP 包数据) func forward(ipPacketData: Data) { guard self.state == .running, let connection = self.connection, connection.state == .ready else { return } connection.send(content: ipPacketData, completion: .contentProcessed { [weak self] error in if let error { Task { await self?.finishPacketContinuationIfNeed(throwing: .sendFailed(error)) } } }) } func stop() async { guard self.state != .stopped else { return } self.state = .stopped let connection = self.connection self.connection = nil connection?.cancel() await self.readySignal.fail(DNSCloudError.cancelled) self.finishPacketContinuationIfNeed(throwing: nil) SDLLogger.log("[SDLCloudClient] stopped") } private func handleConnectionStateUpdate(_ state: NWConnection.State, for connection: NWConnection) async { switch state { case .ready: guard self.state != .stopped, self.isCurrentConnection(connection) else { return } self.state = .running SDLLogger.log("[DNSClient] Connection ready", for: .debug) await self.readySignal.succeed(()) case .failed(let error): await self.readySignal.fail(DNSCloudError.failed(error)) self.finishPacketContinuationIfNeed(throwing: .failed(error)) case .cancelled: await self.readySignal.fail(DNSCloudError.cancelled) self.finishPacketContinuationIfNeed(throwing: .cancelled) default: break } } private func isCurrentConnection(_ connection: NWConnection) -> Bool { guard let currentConnection = self.connection else { return false } return currentConnection === connection } private func finishPacketContinuationIfNeed(throwing error: DNSCloudError?) { guard !self.isPacketContinuationFinished else { return } self.isPacketContinuationFinished = true if let error { self.packetContinuation.finish(throwing: error) } else { self.packetContinuation.finish() } } private func readOnce() async throws -> Data { guard let connection = self.connection, connection.state == .ready else { throw DNSCloudError.cancelled } return try await withCheckedThrowingContinuation { continuation in connection.receiveMessage { content, _, _, error in if let error { continuation.resume(throwing: error) } else if let data = content, !data.isEmpty { continuation.resume(returning: data) } else { continuation.resume(throwing: DNSCloudError.invalidData) } } } } deinit { SDLLogger.log("[DNSCloudClient] deinit", for: .debug) } }