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

180 lines
4.8 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(step: Step)
case finished
}
private enum Step: Int {
case step1 = 1
case step2 = 2
case step3 = 3
case step4 = 4
}
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
// MARK: - Init
init(udpHole: SDLUDPHoleActor, config: SDLConfiguration, logger: SDLLogger) {
self.udpHole = udpHole
self.config = config
self.logger = logger
}
// MARK: - Public API
/// NAT
func start(onFinished: @escaping (NatType) -> Void) async {
guard case .idle = state else {
logger.log("[NAT] probe already started", level: .warning)
return
}
self.onFinished = onFinished
transition(to: .waiting(step: .step1))
await self.sendProbe(step: .step1)
}
/// UDP STUN
func handleProbeReply(from address: SocketAddress, reply: SDLStunProbeReply) async {
guard case .waiting(let currentStep) = state else {
return
}
switch currentStep {
case .step1:
let localAddress = await self.udpHole.getLocalAddress()
if address == localAddress {
finish(.noNat)
return
}
natAddress1 = address
transition(to: .waiting(step: .step2))
await self.sendProbe(step: .step2)
case .step2:
natAddress2 = address
// natAddress2 IPIPNAT;
// ip{dstIp, dstPort, srcIp, srcPort}, ip
if let ip1 = natAddress1?.ipAddress, let ip2 = natAddress2?.ipAddress, ip1 != ip2 {
finish(.symmetric)
return
}
transition(to: .waiting(step: .step3))
await self.sendProbe(step: .step3)
case .step3:
// step3: ip1:port1 <---- ip2:port2 (ipport)
// IPNAT
finish(.fullCone)
case .step4:
finish(.coneRestricted)
}
}
/// Timer / Task
func handleTimeout() async {
guard case .waiting(let currentStep) = state else {
return
}
switch currentStep {
case .step3:
transition(to: .waiting(step: .step4))
await sendProbe(step: .step4)
case .step4:
finish(.portRestricted)
default:
finish(.blocked)
}
}
// MARK: - Internal helpers
private func sendProbe(step: Step) async {
let addressArray = config.stunProbeSocketAddressArray
let remote: SocketAddress
let attr: SDLProbeAttr
switch step {
case .step1:
remote = addressArray[0][0]
attr = .none
case .step2:
remote = addressArray[1][1]
attr = .none
case .step3:
remote = addressArray[0][0]
attr = .peer
case .step4:
remote = addressArray[0][0]
attr = .port
}
var stunProbe = SDLStunProbe()
stunProbe.cookie = self.cookieId
stunProbe.attr = UInt32(attr.rawValue)
self.cookieId &+= 1
await self.udpHole.send(type: .stunProbe, data: try! stunProbe.serializedData(), remoteAddress: remote)
}
private func finish(_ type: NatType) {
guard case .finished = state else {
transition(to: .finished)
logger.log("[NAT] finished with \(type)", level: .info)
onFinished?(type)
onFinished = nil
return
}
}
private func transition(to newState: State) {
state = newState
}
}