This commit is contained in:
anlicheng 2026-01-28 14:03:41 +08:00
parent cbfbbc9ac6
commit 599a047f5c
2 changed files with 73 additions and 88 deletions

View File

@ -21,168 +21,153 @@ actor SDLNATProberActor {
}
// MARK: - Internal State
private enum State {
case idle
case waiting
case finished
class ProbeSession {
var cookieId: UInt32
// step -> SDLStunProbeReply
var replies: [UInt32: SDLStunProbeReply]
var timeoutTask: Task<Void, Never>?
var continuation: CheckedContinuation<NatType, Never>
private var isFinished: Bool = false
init(cookieId: UInt32, timeoutTask: Task<Void, Never>? = nil, continuation: CheckedContinuation<NatType, Never>) {
self.cookieId = cookieId
self.replies = [:]
self.timeoutTask = timeoutTask
self.continuation = continuation
}
func finished(with type: NatType) {
guard !isFinished else {
return
}
self.continuation.resume(returning: type)
//
self.timeoutTask?.cancel()
self.isFinished = true
}
}
private var state: State = .idle
// MARK: - Dependencies
private let udpHole: SDLUDPHoleActor
private let config: SDLConfiguration
private let addressArray: [[SocketAddress]]
private let logger: SDLLogger
// MARK: - Probe Data
private var natAddress1: SocketAddress?
private var natAddress2: SocketAddress?
// MARK: - Completion
private var onFinished: ((NatType) -> Void)?
private var cookieId: UInt32 = 1
private var currentCookieId: UInt32?
private var sessions: [UInt32: ProbeSession] = [:]
// step -> SDLStunProbeReply
private var replies: [UInt32: SDLStunProbeReply] = [:]
private var timeoutTask: Task<Void, Never>?
// MARK: - Init
init(udpHole: SDLUDPHoleActor, config: SDLConfiguration, logger: SDLLogger) {
init(udpHole: SDLUDPHoleActor, addressArray: [[SocketAddress]], logger: SDLLogger) {
self.udpHole = udpHole
self.config = config
self.addressArray = addressArray
self.logger = logger
}
// MARK: - Public API
func probeNatType() async -> NatType {
let cookieId = self.cookieId
self.cookieId &+= 1
return await withCheckedContinuation { continuation in
let timeoutTask = Task {
try? await Task.sleep(nanoseconds: 5_000_000_000)
await self.handleTimeout(cookie: cookieId)
}
let session = ProbeSession(
cookieId: cookieId,
timeoutTask: timeoutTask,
continuation: continuation
)
self.sessions[cookieId] = session
Task {
await self.start { natType in
continuation.resume(returning: natType)
}
await self.sendProbe(cookie: cookieId)
}
}
}
/// NAT
private func start(onFinished: @escaping (NatType) -> Void) async {
guard case .idle = state else {
return
}
self.onFinished = onFinished
transition(to: .waiting)
let cookieId = self.cookieId
self.cookieId &+= 1
self.currentCookieId = cookieId
await self.sendProbe(cookie: cookieId)
self.timeoutTask = Task {
try? await Task.sleep(nanoseconds: 5_000_000_000)
await self.handleTimeout()
}
}
/// UDP STUN
func handleProbeReply(from address: SocketAddress, reply: SDLStunProbeReply) async {
guard case .waiting = state, let cookieId = self.currentCookieId, cookieId == reply.cookie else {
guard let session = self.sessions[reply.cookie] else {
return
}
replies[reply.step] = reply
session.replies[reply.step] = reply
// 退nat
if let step1 = replies[1] {
if let step1 = session.replies[1] {
let localAddress = await self.udpHole.getLocalAddress()
if address == localAddress {
finish(.noNat)
finish(cookie: session.cookieId, .noNat)
return
}
}
if let step1 = replies[1], let step2 = replies[2] {
if let step1 = session.replies[1], let step2 = session.replies[2] {
// natAddress2 IPIPNAT;
// ip{dstIp, dstPort, srcIp, srcPort}, ip
if let addr1 = step1.socketAddress(), let addr2 = step2.socketAddress(), addr1 != addr2 {
finish(.symmetric)
finish(cookie: session.cookieId, .symmetric)
return
}
}
// ,
if replies[1] != nil && replies[2] != nil && replies[3] != nil && replies[4] != nil {
if session.replies[1] != nil && session.replies[2] != nil && session.replies[3] != nil && session.replies[4] != nil {
// step3: ip2:port2 <---- ip1:port1 (ipport)
// IPNAT
if let step3 = replies[3] {
finish(.fullCone)
if let step3 = session.replies[3] {
finish(cookie: session.cookieId, .fullCone)
return
}
// step3: ip1:port1 <---- ip1:port2 (port)
// IPNAT
if let step4 = replies[4] {
finish(.coneRestricted)
if let step4 = session.replies[4] {
finish(cookie: session.cookieId, .coneRestricted)
return
}
}
}
/// Timer / Task
private func handleTimeout() async {
guard case .waiting = state else {
private func handleTimeout(cookie: UInt32) async {
guard let session = self.sessions[cookie] else {
return
}
if replies[1] == nil {
finish(.blocked)
} else if replies[3] != nil {
finish(.fullCone)
} else if replies[4] != nil {
finish(.coneRestricted)
if session.replies[1] == nil {
finish(cookie: cookie, .blocked)
} else if session.replies[3] != nil {
finish(cookie: cookie, .fullCone)
} else if session.replies[4] != nil {
finish(cookie: cookie, .coneRestricted)
} else {
finish(.portRestricted)
finish(cookie: cookie, .portRestricted)
}
}
private func finish(cookie: UInt32, _ type: NatType) {
if let session = self.sessions.removeValue(forKey: cookie) {
session.finished(with: type)
}
}
// MARK: - Internal helpers
private func sendProbe(cookie: UInt32) async {
let addressArray = config.stunProbeSocketAddressArray
await self.udpHole.send(type: .stunProbe, data: makeProbePacket(cookieId: cookie, step: 1, attr: .none), remoteAddress: addressArray[0][0])
await self.udpHole.send(type: .stunProbe, data: makeProbePacket(cookieId: cookie, step: 2, attr: .none), remoteAddress: addressArray[1][1])
await self.udpHole.send(type: .stunProbe, data: makeProbePacket(cookieId: cookie, step: 3, attr: .peer), remoteAddress: addressArray[0][0])
await self.udpHole.send(type: .stunProbe, data: makeProbePacket(cookieId: cookie, step: 4, attr: .port), remoteAddress: addressArray[0][0])
}
private func finish(_ type: NatType) {
guard case .waiting = state else {
return
}
transition(to: .finished)
onFinished?(type)
onFinished = nil
//
self.timeoutTask?.cancel()
self.timeoutTask = nil
}
private func transition(to newState: State) {
state = newState
}
private func makeProbePacket(cookieId: UInt32, step: UInt32, attr: SDLProbeAttr) -> Data {
var stunProbe = SDLStunProbe()

View File

@ -267,7 +267,7 @@ public class SDLContext {
// nat
if let udpHoleActor = self.udpHoleActor {
self.proberActor = SDLNATProberActor(udpHole: udpHoleActor, config: self.config, logger: self.logger)
self.proberActor = SDLNATProberActor(udpHole: udpHoleActor, addressArray: self.config.stunProbeSocketAddressArray, logger: self.logger)
self.natType = await self.proberActor!.probeNatType()
}