Files
2026-08-31 16:44:54 -05:00

177 lines
8.8 KiB
Swift

import Foundation
public enum IPv6Transport: UInt8, Sendable {
case tcp = 6
case udp = 17
case icmpv6 = 58
}
public struct IPv6PacketInfo: Sendable {
public let source: MeshAddress
public let destination: MeshAddress
public let transport: IPv6Transport
public let sourcePort: UInt16?
public let destinationPort: UInt16?
public let tcpFlags: UInt8?
public let icmpType: UInt8?
public let transportOffset: Int
}
public enum IPv6PacketParser {
public static let maximumPacketSize = 1280
public static func parse(_ packet: Data) throws -> IPv6PacketInfo {
guard packet.count >= 40, packet.count <= maximumPacketSize, packet[0] >> 4 == 6 else { throw UMNError.invalidFrame }
let payloadLength = Int(read16(packet, 4))
guard payloadLength == packet.count - 40 else { throw UMNError.invalidFrame }
let source = try MeshAddress(bytes: packet.subdata(in: 8..<24))
let destinationBytes = packet.subdata(in: 24..<40)
guard destinationBytes.first != 0xff else { throw UMNError.invalidFrame }
let destination = try MeshAddress(bytes: destinationBytes)
var next = packet[6]
var offset = 40
var extensions = 0
while [UInt8(0), 43, 60].contains(next) {
guard extensions < 8, offset + 2 <= packet.count else { throw UMNError.invalidFrame }
let following = packet[offset]
let length: Int
length = (Int(packet[offset + 1]) + 1) * 8
guard length >= 8, offset + length <= packet.count else { throw UMNError.invalidFrame }
if next == 43, packet[offset + 3] != 0 { throw UMNError.invalidFrame }
if next == 0 || next == 60 { try validateOptions(packet, range: (offset + 2)..<(offset + length)) }
next = following; offset += length; extensions += 1
}
guard next != 44, let transport = IPv6Transport(rawValue: next) else { throw UMNError.invalidFrame }
switch transport {
case .tcp:
guard offset + 20 <= packet.count else { throw UMNError.invalidFrame }
let headerLength = Int(packet[offset + 12] >> 4) * 4
guard headerLength >= 20, offset + headerLength <= packet.count else { throw UMNError.invalidFrame }
return .init(source: source, destination: destination, transport: transport,
sourcePort: read16(packet, offset), destinationPort: read16(packet, offset + 2),
tcpFlags: packet[offset + 13], icmpType: nil, transportOffset: offset)
case .udp:
guard offset + 8 <= packet.count else { throw UMNError.invalidFrame }
let length = Int(read16(packet, offset + 4))
guard length >= 8, offset + length == packet.count else { throw UMNError.invalidFrame }
return .init(source: source, destination: destination, transport: transport,
sourcePort: read16(packet, offset), destinationPort: read16(packet, offset + 2),
tcpFlags: nil, icmpType: nil, transportOffset: offset)
case .icmpv6:
guard offset + 8 <= packet.count else { throw UMNError.invalidFrame }
return .init(source: source, destination: destination, transport: transport,
sourcePort: nil, destinationPort: nil, tcpFlags: nil,
icmpType: packet[offset], transportOffset: offset)
}
}
private static func read16(_ data: Data, _ offset: Int) -> UInt16 {
UInt16(data[offset]) << 8 | UInt16(data[offset + 1])
}
private static func validateOptions(_ data: Data, range: Range<Int>) throws {
var index = range.lowerBound
while index < range.upperBound {
if data[index] == 0 { index += 1; continue } // Pad1
guard index + 2 <= range.upperBound else { throw UMNError.invalidFrame }
let length = Int(data[index + 1])
guard index + 2 + length <= range.upperBound else { throw UMNError.invalidFrame }
index += 2 + length
}
}
}
public final class NativePacketFirewall: @unchecked Sendable {
private struct Flow: Hashable {
let kind: FirewallProtocol
let local: MeshAddress
let remote: MeshAddress
let localPort: UInt16
let remotePort: UInt16
}
private struct Entry { var lastSeen: Date; var established: Bool }
private let rules: MeshFirewall
private let lock = NSLock()
private var flows: [Flow: Entry] = [:]
private var echoWindows: [MeshAddress: (Date, Int)] = [:]
public let maximumFlows: Int
public init(rules: MeshFirewall, maximumFlows: Int = 4096) {
self.rules = rules; self.maximumFlows = maximumFlows
}
/// Records an OS-originated packet. Outbound connections are always permitted.
public func allowOutbound(_ info: IPv6PacketInfo, now: Date = Date()) -> Bool {
guard let kind = protocolKind(info), let localPort = info.sourcePort, let remotePort = info.destinationPort else {
return info.transport == .icmpv6 && [1, 2, 3, 4, 128, 129].contains(info.icmpType ?? 255)
}
lock.lock(); defer { lock.unlock() }
expireLocked(now)
let key = Flow(kind: kind, local: info.source, remote: info.destination,
localPort: localPort, remotePort: remotePort)
insert(key, entry: .init(lastSeen: now, established: kind == .udp || ((info.tcpFlags ?? 0) & 0x02) != 0))
return true
}
public func allowInbound(_ info: IPv6PacketInfo, packet: Data? = nil, now: Date = Date()) -> Bool {
if info.transport == .icmpv6 { return allowICMP(info, packet: packet, now: now) }
guard let kind = protocolKind(info), let remotePort = info.sourcePort, let localPort = info.destinationPort else { return false }
lock.lock(); defer { lock.unlock() }
expireLocked(now)
let key = Flow(kind: kind, local: info.destination, remote: info.source,
localPort: localPort, remotePort: remotePort)
if var entry = flows[key] {
entry.lastSeen = now
if kind == .tcp, ((info.tcpFlags ?? 0) & 0x12) == 0x12 { entry.established = true }
flows[key] = entry
if kind == .tcp, ((info.tcpFlags ?? 0) & 0x05) != 0 { flows.removeValue(forKey: key) }
return true
}
guard rules.allows(protocol: kind, port: localPort, source: info.source) else { return false }
// A new TCP flow must begin with SYN and not ACK. UDP is connectionless.
if kind == .tcp, ((info.tcpFlags ?? 0) & 0x12) != 0x02 { return false }
insert(key, entry: .init(lastSeen: now, established: kind == .udp))
return true
}
private func allowICMP(_ info: IPv6PacketInfo, packet: Data?, now: Date) -> Bool {
guard let type = info.icmpType else { return false }
if (1...4).contains(type), let packet, packet.count >= info.transportOffset + 8 + 48 {
var quoted = Data(packet[(info.transportOffset + 8)...])
let actualPayload = quoted.count - 40
quoted[4] = UInt8(actualPayload >> 8); quoted[5] = UInt8(actualPayload & 255)
guard let original = try? IPv6PacketParser.parse(quoted),
let kind = protocolKind(original), let localPort = original.sourcePort,
let remotePort = original.destinationPort else { return false }
lock.lock(); defer { lock.unlock() }; expireLocked(now)
return flows[Flow(kind: kind, local: original.source, remote: original.destination,
localPort: localPort, remotePort: remotePort)] != nil
}
guard type == 128 || type == 129 else { return false }
lock.lock(); defer { lock.unlock() }
let current = echoWindows[info.source]
if let current, now.timeIntervalSince(current.0) < 1 {
guard current.1 < 10 else { return false }
echoWindows[info.source] = (current.0, current.1 + 1)
} else {
guard echoWindows[info.source] != nil || echoWindows.count < 128 else { return false }
echoWindows[info.source] = (now, 1)
}
return true
}
private func protocolKind(_ info: IPv6PacketInfo) -> FirewallProtocol? {
info.transport == .tcp ? .tcp : (info.transport == .udp ? .udp : nil)
}
private func insert(_ flow: Flow, entry: Entry) {
if flows[flow] == nil, flows.count >= maximumFlows,
let oldest = flows.min(by: { $0.value.lastSeen < $1.value.lastSeen })?.key { flows.removeValue(forKey: oldest) }
flows[flow] = entry
}
private func expireLocked(_ now: Date) {
flows = flows.filter { now.timeIntervalSince($0.value.lastSeen) <= ($0.key.kind == .tcp ? 300 : 60) }
echoWindows = echoWindows.filter { now.timeIntervalSince($0.value.0) < 60 }
}
}