// // SDLUDPHoleSession.swift // punchnet // // Created by 安礼成 on 2026/5/27. // import Foundation import NIOCore actor SDLUDPHoleSession { private let proberActor: SDLNATProberActor private let includeV6: Bool private let onEvent: SDLUDPHoleService.EventHandler private let onData: SDLUDPHoleService.DataHandler private var udpHole: SDLUDPHole? private var udpHoleV6: SDLUDPHoleV6? private var localAddress: SocketAddress? init( proberActor: SDLNATProberActor, includeV6: Bool, onEvent: @escaping SDLUDPHoleService.EventHandler, onData: @escaping SDLUDPHoleService.DataHandler ) { self.proberActor = proberActor self.includeV6 = includeV6 self.onEvent = onEvent self.onData = onData } func run() async throws { do { try await withThrowingTaskGroup(of: Void.self) { group in defer { group.cancelAll() } group.addTask { try await self.runV4() } if self.includeV6 { group.addTask { try await self.runV6() } } try await group.waitForAll() } await self.stop() } catch { await self.stop() throw error } } func stop() async { let udpHole = self.udpHole self.udpHole = nil self.localAddress = nil let udpHoleV6 = self.udpHoleV6 self.udpHoleV6 = nil await self.proberActor.cancelAll() await udpHole?.stop() udpHoleV6?.stop() } func send(type: SDLPacketType, data: Data, remoteAddress: SocketAddress) async { switch remoteAddress { case .v4: guard let udpHole else { SDLLogger.log("[SDLUDPHoleSession] udpHole is nil for remoteAddress: \(remoteAddress)", for: .debug) return } await udpHole.send(type: type, data: data, remoteAddress: remoteAddress) case .v6: guard let udpHoleV6 else { SDLLogger.log("[SDLUDPHoleSession] udpHoleV6 is nil for remoteAddress: \(remoteAddress)", for: .debug) return } udpHoleV6.send(type: type, data: data, remoteAddress: remoteAddress) default: SDLLogger.log("[SDLUDPHoleSession] unsupported socket family: \(remoteAddress)", for: .debug) } } private func runV4() async throws { let udpHole = try SDLUDPHole() let localAddress = try await udpHole.start() self.udpHole = udpHole self.localAddress = localAddress SDLLogger.log("[SDLUDPHoleSession] udpHole started, on address: \(localAddress)") await self.onEvent(.ready(localAddress)) do { try await withThrowingTaskGroup(of: Void.self) { group in defer { group.cancelAll() } group.addTask { try await self.readV4Loop(udpHole: udpHole) } group.addTask { await self.probeNatType(udpHole: udpHole) } try await group.waitForAll() } } catch { await udpHole.stop() if self.udpHole === udpHole { self.udpHole = nil self.localAddress = nil } throw error } } private func readV4Loop(udpHole: SDLUDPHole) async throws { for try await datagram in await udpHole.messageStream() { try Task.checkCancellation() try await self.handleV4Message(remoteAddress: datagram.remoteAddress, message: datagram.message) } } private func probeNatType(udpHole: SDLUDPHole) async { if Task.isCancelled { return } let natType = await self.proberActor.probeNatType(using: udpHole) if Task.isCancelled { return } await self.onEvent(.natType(natType)) } private func handleV4Message(remoteAddress: SocketAddress, message: SDLHoleMessage) async throws { switch message { case .control(let control): switch control { case .stunProbeReply(let probeReply): await self.proberActor.handleProbeReply(localAddress: self.localAddress, reply: probeReply) default: await self.onEvent(.packet(remoteAddress, control, source: .v4)) } case .data(let data): await self.onData(data) } } private func runV6() async throws { let udpHoleV6 = try SDLUDPHoleV6() let localAddress = try udpHoleV6.start() self.udpHoleV6 = udpHoleV6 if let localAddress { SDLLogger.log("[SDLUDPHoleSession] udpHoleV6 started, on address: \(localAddress)") } else { SDLLogger.log("[SDLUDPHoleSession] udpHoleV6 started, no local address") } do { try await withThrowingTaskGroup(of: Void.self) { group in defer { group.cancelAll() } let onEvent = self.onEvent let onData = self.onData group.addTask { for await (remoteAddress, message) in udpHoleV6.messageStream { try Task.checkCancellation() switch message { case .control(let control): await onEvent(.packet(remoteAddress, control, source: .v6)) case .data(let data): await onData(data) } } } group.addTask { for await event in udpHoleV6.eventStream { try Task.checkCancellation() switch event { case .ready: SDLLogger.log("[SDLUDPHoleSession] udpHoleV6 ready") case .closed, .errorCaught: throw SDLContextError.udpHoleClosed } } } _ = try await group.next() } } catch { udpHoleV6.stop() if self.udpHoleV6 === udpHoleV6 { self.udpHoleV6 = nil } throw error } } }