punchnet-macos/Tun/DNS/DNSCloudClient.swift
2026-05-27 16:08:51 +08:00

175 lines
5.4 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
final class DNSCloudClient {
enum DNSCloudError: Error {
case failed(Error)
case cancelled
case sendFailed(Error)
}
private enum State {
case idle
case running
case stopped
}
private var state: State = .idle
private var connection: NWConnection?
private let dnsServerAddress: NWEndpoint
// DNS
public let packetFlow: AsyncThrowingStream<Data, Error>
private let packetContinuation: AsyncThrowingStream<Data, Error>.Continuation
private var isPacketContinuationFinished: Bool = false
/// - 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
}
// 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
self?.handleConnectionStateUpdate(state, for: connection)
}
//
connection.start(queue: .global())
self.connection = connection
defer {
self.stop()
}
let stream = Self.makeReceiveStream(for: connection)
try await withTaskCancellationHandler {
for await data in stream {
try Task.checkCancellation()
self.packetContinuation.yield(data)
}
} onCancel: {
self.stop()
}
}
/// DNS TUN IP
func forward(ipPacketData: Data) {
guard let connection = self.connection, connection.state == .ready else {
return
}
connection.send(content: ipPacketData, completion: .contentProcessed { error in
if let error = error {
self.finishPacketContinuationIfNeed(throwing: .sendFailed(error))
}
})
}
func stop() {
guard self.state != .stopped else {
return
}
self.state = .stopped
self.connection?.cancel()
self.connection = nil
self.finishPacketContinuationIfNeed(throwing: nil)
SDLLogger.log("[SDLCloudClient] stopped")
}
private func handleConnectionStateUpdate(_ state: NWConnection.State, for connection: NWConnection) {
switch state {
case .ready:
SDLLogger.log("[DNSClient] Connection ready", for: .debug)
self.state = .running
case .failed(let error):
self.finishPacketContinuationIfNeed(throwing: .failed(error))
case .cancelled:
self.finishPacketContinuationIfNeed(throwing: .cancelled)
default:
break
}
}
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 static func makeReceiveStream(for connection: NWConnection) -> AsyncStream<Data> {
return AsyncStream(bufferingPolicy: .bufferingNewest(256)) { continuation in
func receiveNext() {
connection.receiveMessage { content, _, _, error in
if let data = content, !data.isEmpty {
// DNS AsyncStream
continuation.yield(data)
}
if error == nil && connection.state == .ready {
receiveNext() //
} else {
continuation.finish()
}
}
}
receiveNext()
}
}
deinit {
SDLLogger.log("[DNSCloudClient] deinit", for: .debug)
}
}