195 lines
6.2 KiB
Swift
195 lines
6.2 KiB
Swift
//
|
||
// 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<Data, Error>
|
||
private let packetContinuation: AsyncThrowingStream<Data, Error>.Continuation
|
||
private var isPacketContinuationFinished: Bool = false
|
||
private let readySignal = AsyncOneShot<Void>()
|
||
|
||
/// - 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)
|
||
}
|
||
|
||
}
|