punchnet-macos/Tun/Punchnet/Actors/SDLNATProberActor.swift

197 lines
6.0 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.

//
// SDLNATProberActor.swift
// punchnet
//
// Created by on 2026/1/28.
//
import Foundation
import NIOCore
actor SDLNATProberActor {
// MARK: - NAT Type
enum NatType: UInt8, Encodable {
case blocked = 0
case noNat = 1
case fullCone = 2
case portRestricted = 3
case coneRestricted = 4
case symmetric = 5
}
// MARK: - Internal State
private enum State {
case idle
case waiting
case finished
}
private var state: State = .idle
// MARK: - Dependencies
private let udpHole: SDLUDPHoleActor
private let config: SDLConfiguration
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?
// step -> SDLStunProbeReply
private var replies: [UInt32: SDLStunProbeReply] = [:]
private var timeoutTask: Task<Void, Never>?
// MARK: - Init
init(udpHole: SDLUDPHoleActor, config: SDLConfiguration, logger: SDLLogger) {
self.udpHole = udpHole
self.config = config
self.logger = logger
}
// MARK: - Public API
func probeNatType() async -> NatType {
return await withCheckedContinuation { continuation in
Task {
await self.start { natType in
continuation.resume(returning: natType)
}
}
}
}
/// 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 {
return
}
replies[reply.step] = reply
// 退nat
if let step1 = replies[1] {
let localAddress = await self.udpHole.getLocalAddress()
if address == localAddress {
finish(.noNat)
return
}
}
if let step1 = replies[1], let step2 = replies[2] {
// natAddress2 IPIPNAT;
// ip{dstIp, dstPort, srcIp, srcPort}, ip
if let addr1 = step1.socketAddress(), let addr2 = step2.socketAddress(), addr1 != addr2 {
finish(.symmetric)
return
}
}
// ,
if replies[1] != nil && replies[2] != nil && replies[3] != nil && replies[4] != nil {
// step3: ip2:port2 <---- ip1:port1 (ipport)
// IPNAT
if let step3 = replies[3] {
finish(.fullCone)
return
}
// step3: ip1:port1 <---- ip1:port2 (port)
// IPNAT
if let step4 = replies[4] {
finish(.coneRestricted)
return
}
}
}
/// Timer / Task
private func handleTimeout() async {
guard case .waiting = state else {
return
}
if replies[1] == nil {
finish(.blocked)
} else if replies[3] != nil {
finish(.fullCone)
} else if replies[4] != nil {
finish(.coneRestricted)
} else {
finish(.portRestricted)
}
}
// 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()
stunProbe.cookie = cookieId
stunProbe.step = step
stunProbe.attr = UInt32(attr.rawValue)
return try! stunProbe.serializedData()
}
}