punchnet-macos/Tun/Punchnet/Actors/SDLPuncherActor.swift
2026-04-14 18:45:27 +08:00

163 lines
4.9 KiB
Swift

//
// SDLPuncherActor.swift
// Tun
//
// Created by on 2026/1/7.
//
import Foundation
import NIOCore
actor SDLPuncherActor {
// 10
nonisolated private let cooldownInterval: TimeInterval = 10
// peerInfo
nonisolated private let peerInfoTimeout: TimeInterval = 3
struct RegisterRequest {
let srcMac: Data
let dstMac: Data
let networkId: UInt32
}
private enum RequestPhase {
case waitingPeerInfo(deadline: Date)
case coolingDown
}
private struct RequestEntry {
let request: RegisterRequest
let cooldownUntil: Date
var phase: RequestPhase
func canSubmit(at now: Date) -> Bool {
return cooldownUntil <= now
}
func isWaitingPeerInfo(at now: Date) -> Bool {
guard case .waitingPeerInfo(let deadline) = self.phase else {
return false
}
return deadline > now
}
mutating func markCoolingDown() {
self.phase = .coolingDown
}
}
// dstMac
private var requestEntries: [Data: RequestEntry] = [:]
private var cleanupTask: Task<Void, Never>?
func start() {
guard self.cleanupTask == nil else {
return
}
self.cleanupTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
await self?.cleanupExpiredEntries()
}
}
}
func submitRegisterRequest(quicClient: SDLQUICClient?, request: RegisterRequest) {
guard let quicClient else {
return
}
let now = Date()
self.cleanupExpiredEntries(now: now)
if let entry = self.requestEntries[request.dstMac], !entry.canSubmit(at: now) {
return
}
var queryInfo = SDLQueryInfo()
queryInfo.dstMac = request.dstMac
guard let queryData = try? queryInfo.serializedData() else {
SDLLogger.log("[SDLPuncherActor] failed to encode queryInfo", for: .debug)
return
}
self.requestEntries[request.dstMac] = RequestEntry(
request: request,
cooldownUntil: now.addingTimeInterval(self.cooldownInterval),
phase: .waitingPeerInfo(deadline: now.addingTimeInterval(self.peerInfoTimeout))
)
quicClient.send(type: .queryInfo, data: queryData)
}
func handlePeerInfo(using udpHole: SDLUDPHole?, peerInfo: SDLPeerInfo) async {
let now = Date()
self.cleanupExpiredEntries(now: now)
guard var entry = self.requestEntries[peerInfo.dstMac] else {
return
}
guard entry.isWaitingPeerInfo(at: now) else {
return
}
entry.markCoolingDown()
self.requestEntries[peerInfo.dstMac] = entry
guard let udpHole else {
SDLLogger.log("[SDLPuncherActor] udpHole is nil when peerInfo arrived", for: .debug)
return
}
var register = SDLRegister()
register.networkID = entry.request.networkId
register.srcMac = entry.request.srcMac
register.dstMac = entry.request.dstMac
guard let registerData = try? register.serializedData() else {
SDLLogger.log("[SDLPuncherActor] failed to encode register", for: .debug)
return
}
// register
if peerInfo.hasV4Info {
if let remoteAddress = try? await peerInfo.v4Info.socketAddress() {
SDLLogger.log("[SDLContext] hole sock address: \(remoteAddress)", for: .punchnet)
udpHole.send(type: .register, data: registerData, remoteAddress: remoteAddress)
} else {
SDLLogger.log("[SDLPuncherActor] failed to resolve peerInfo.v4Info", for: .debug)
}
}
if peerInfo.hasV6Info {
if let remoteAddress = try? await peerInfo.v6Info.socketAddress() {
SDLLogger.log("[SDLContext] hole sock address v6: \(remoteAddress)", for: .punchnet)
udpHole.send(type: .register, data: registerData, remoteAddress: remoteAddress)
} else {
SDLLogger.log("[SDLPuncherActor] failed to resolve peerInfo.v6Info", for: .debug)
}
}
}
func stop() {
self.cleanupTask?.cancel()
self.cleanupTask = nil
self.requestEntries.removeAll()
}
private func cleanupExpiredEntries(now: Date = Date()) {
self.requestEntries = self.requestEntries.filter { _, entry in
!entry.canSubmit(at: now)
}
}
deinit {
self.cleanupTask?.cancel()
}
}