punchnet-macos/Tun/DNS/DNSCloudClient.swift
2026-05-28 13:20:18 +08:00

195 lines
6.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
}
}