diff --git a/Tun/Punchnet/Actors/ArpServer.swift b/Tun/Punchnet/Arp/ArpServer.swift similarity index 71% rename from Tun/Punchnet/Actors/ArpServer.swift rename to Tun/Punchnet/Arp/ArpServer.swift index ef27215..f0221be 100644 --- a/Tun/Punchnet/Actors/ArpServer.swift +++ b/Tun/Punchnet/Arp/ArpServer.swift @@ -19,11 +19,13 @@ actor ArpServer { private var known_macs: [UInt32: ArpEntry] = [:] private let arpTTL: TimeInterval + nonisolated private let snapshotPublisher: SnapshotPublisher private var cleanupTask: Task? init(arpTTL: TimeInterval = 300) { self.arpTTL = arpTTL + self.snapshotPublisher = SnapshotPublisher(initial: ArpSnapshot.empty()) } func start() { @@ -46,6 +48,7 @@ actor ArpServer { if entry.expireTime < Date().timeIntervalSince1970 { known_macs.removeValue(forKey: ip) + self.publishSnapshot() return nil } @@ -55,19 +58,23 @@ actor ArpServer { func append(ip: UInt32, mac: Data) { let expireAt = Date().timeIntervalSince1970 + arpTTL self.known_macs[ip] = ArpEntry(mac: mac, expireTime: expireAt) + self.publishSnapshot() } func remove(ip: UInt32) { self.known_macs.removeValue(forKey: ip) + self.publishSnapshot() } func dropMacs(macs: [Data]) { self.known_macs = self.known_macs.filter { !macs.contains($0.value.mac) } + self.publishSnapshot() } func clear() { self.known_macs = [:] self.coolingDown = [:] + self.publishSnapshot() } func stop() { @@ -95,12 +102,34 @@ actor ArpServer { if !targetMac.isEmpty { let expireAt = Date().timeIntervalSince1970 + arpTTL self.known_macs[targetIp] = ArpEntry(mac: targetMac, expireTime: expireAt) + self.publishSnapshot() } } + + nonisolated func snapshot() -> ArpSnapshot { + return self.snapshotPublisher.current() + } private func cleanup() { let now = Date() self.coolingDown = self.coolingDown.filter { $0.value > now } + let oldCount = self.known_macs.count + self.known_macs = self.known_macs.filter { $0.value.expireTime >= now.timeIntervalSince1970 } + if self.known_macs.count != oldCount { + self.publishSnapshot() + } + } + + private func publishSnapshot() { + self.snapshotPublisher.publish(self.compileSnapshot()) + } + + private func compileSnapshot() -> ArpSnapshot { + let now = Date().timeIntervalSince1970 + let entries = self.known_macs.compactMapValues { entry in + entry.expireTime >= now ? entry.mac : nil + } + return ArpSnapshot(entries: entries) } deinit { diff --git a/Tun/Punchnet/Arp/ArpSnapshot.swift b/Tun/Punchnet/Arp/ArpSnapshot.swift new file mode 100644 index 0000000..87b3b11 --- /dev/null +++ b/Tun/Punchnet/Arp/ArpSnapshot.swift @@ -0,0 +1,24 @@ +// +// ArpSnapshot.swift +// Tun +// +// Created by Codex on 2026/5/20. +// + +import Foundation + +final class ArpSnapshot: Snapshot { + private let entries: [UInt32: Data] + + init(entries: [UInt32: Data]) { + self.entries = entries + } + + func lookup(_ ip: UInt32) -> Data? { + return self.entries[ip] + } + + static func empty() -> ArpSnapshot { + return ArpSnapshot(entries: [:]) + } +} diff --git a/Tun/Punchnet/Outbound/PacketOutboundActor.swift b/Tun/Punchnet/Outbound/PacketOutboundActor.swift index c02d0b0..bab0920 100644 --- a/Tun/Punchnet/Outbound/PacketOutboundActor.swift +++ b/Tun/Punchnet/Outbound/PacketOutboundActor.swift @@ -195,7 +195,7 @@ actor PacketOutboundActor { SDLLogger.log("[PacketOutboundActor] use exit_node: \(SDLUtil.int32ToIp(ip))", for: .trace) } - if let dstMac = await self.arpServer.query(ip: ip) { + if let dstMac = self.arpServer.snapshot().lookup(ip) { SDLLogger.log("[PacketOutboundActor] dstIp: \(SDLUtil.int32ToIp(ip)), dst_mac is: \(SDLUtil.formatMacAddress(mac: dstMac))", for: .trace) await self.routeLayerPacket(dstMac: dstMac, type: type, data: data) } else { diff --git a/Tun/Punchnet/Policy/IdentitySnapshot.swift b/Tun/Punchnet/Policy/IdentitySnapshot.swift index 2df5f3f..e39735b 100644 --- a/Tun/Punchnet/Policy/IdentitySnapshot.swift +++ b/Tun/Punchnet/Policy/IdentitySnapshot.swift @@ -4,8 +4,9 @@ // // Created by 安礼成 on 2026/2/5. // +import Foundation -final class IdentitySnapshot { +final class IdentitySnapshot: Snapshot { typealias IdentityID = UInt32 private let identityMap: [IdentityID: IdentityRuleMap] diff --git a/Tun/Punchnet/Snapshot/Snapshot.swift b/Tun/Punchnet/Snapshot/Snapshot.swift new file mode 100644 index 0000000..8710fe4 --- /dev/null +++ b/Tun/Punchnet/Snapshot/Snapshot.swift @@ -0,0 +1,10 @@ +// +// Snapshot.swift +// punchnet +// +// Created by 安礼成 on 2026/5/20. +// + +protocol Snapshot: AnyObject { + +} diff --git a/Tun/Punchnet/Policy/SnapshotPublisher.swift b/Tun/Punchnet/Snapshot/SnapshotPublisher.swift similarity index 66% rename from Tun/Punchnet/Policy/SnapshotPublisher.swift rename to Tun/Punchnet/Snapshot/SnapshotPublisher.swift index e4b7559..2714761 100644 --- a/Tun/Punchnet/Policy/SnapshotPublisher.swift +++ b/Tun/Punchnet/Snapshot/SnapshotPublisher.swift @@ -6,21 +6,21 @@ // import Atomics -final class SnapshotPublisher { - private let atomic: ManagedAtomic> +final class SnapshotPublisher { + private let atomic: ManagedAtomic> - init(initial snapshot: IdentitySnapshot) { + init(initial snapshot: S) { self.atomic = ManagedAtomic(.passRetained(snapshot)) } - func publish(_ snapshot: IdentitySnapshot) { + func publish(_ snapshot: S) { let newRef = Unmanaged.passRetained(snapshot) let oldRef = atomic.exchange(newRef, ordering: .acquiring) oldRef.release() } @inline(__always) - func current() -> IdentitySnapshot { + func current() -> S { atomic.load(ordering: .relaxed).takeUnretainedValue() }