Files
umn/Sources/umn-selftest/main.swift
T
2026-08-31 17:21:07 -05:00

284 lines
20 KiB
Swift

import Foundation
import Darwin
import UltraMeshCore
import UMNSystem
struct TestFailure: Error, LocalizedError {
let errorDescription: String?
init(_ message: String) { errorDescription = message }
}
var passed = 0
func check(_ condition: @autoclosure () throws -> Bool, _ name: String) throws {
guard try condition() else { throw TestFailure(name) }
passed += 1; print("ok \(passed) - \(name)")
}
func rejects(_ name: String, _ operation: () throws -> Void) throws {
do { try operation() } catch {
passed += 1; print("ok \(passed) - \(name)")
return
}
throw TestFailure(name)
}
func ipv6Packet(source: MeshAddress, destination: MeshAddress, next: UInt8, payload: Data) -> Data {
var packet = Data(repeating: 0, count: 40)
packet[0] = 0x60; packet[4] = UInt8(payload.count >> 8); packet[5] = UInt8(payload.count & 255)
packet[6] = next; packet[7] = 64
packet.replaceSubrange(8..<24, with: source.bytes); packet.replaceSubrange(24..<40, with: destination.bytes)
packet.append(payload); return packet
}
func tcpHeader(source: UInt16, destination: UInt16, flags: UInt8) -> Data {
var data = Data(repeating: 0, count: 20)
data[0] = UInt8(source >> 8); data[1] = UInt8(source & 255)
data[2] = UInt8(destination >> 8); data[3] = UInt8(destination & 255)
data[12] = 0x50; data[13] = flags; return data
}
func udpHeader(source: UInt16, destination: UInt16, payload: Data = Data()) -> Data {
let length = 8 + payload.count; var data = Data(repeating: 0, count: 8)
data[0] = UInt8(source >> 8); data[1] = UInt8(source & 255)
data[2] = UInt8(destination >> 8); data[3] = UInt8(destination & 255)
data[4] = UInt8(length >> 8); data[5] = UInt8(length & 255); data.append(payload); return data
}
func dnsQuery(_ name: String, type: UInt16) -> Data {
var data = Data([0x12, 0x34, 0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0])
for label in name.split(separator: ".") { data.append(UInt8(label.utf8.count)); data.append(contentsOf: label.utf8) }
data.append(0); data.append(UInt8(type >> 8)); data.append(UInt8(type & 255)); data.append(contentsOf: [0, 1]); return data
}
do {
let alice = try NodeIdentity(); let bob = try NodeIdentity(); let carol = try NodeIdentity()
try check(try MeshAddress(alice.record.address.description) == alice.record.address, "mesh address round trip")
try check(alice.record.address.bytes.first == 0xfd, "mesh address uses fd prefix")
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let identityURL = directory.appendingPathComponent("identity.plist")
let addressURL = directory.appendingPathComponent("address")
let stored1 = try NodeIdentity.loadOrCreate(in: directory)
let stored2 = try NodeIdentity.loadOrCreate(in: directory)
try check(stored1.record == stored2.record, "identity and address persist across daemon-style reloads")
let attributes = try FileManager.default.attributesOfItem(atPath: identityURL.path)
try check((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o600, "identity permissions")
let addressAttributes = try FileManager.default.attributesOfItem(atPath: addressURL.path)
try check((addressAttributes[.posixPermissions] as? NSNumber)?.intValue == 0o600, "address permissions")
let storedAddressText = try String(contentsOf: addressURL, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
try check(try MeshAddress(storedAddressText) == stored1.record.address, "stored address is valid IPv6 matching identity")
let legacyDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: legacyDirectory) }
let legacyIdentity = try NodeIdentity.loadOrCreate(at: legacyDirectory.appendingPathComponent("identity.plist"))
try check(!FileManager.default.fileExists(atPath: legacyDirectory.appendingPathComponent("address").path),
"legacy path API creates only identity")
let upgradedIdentity = try NodeIdentity.loadOrCreate(in: legacyDirectory)
try check(upgradedIdentity.record == legacyIdentity.record &&
FileManager.default.fileExists(atPath: legacyDirectory.appendingPathComponent("address").path),
"existing installation gains address sidecar without identity change")
let corruptDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: corruptDirectory) }
try FileManager.default.createDirectory(at: corruptDirectory, withIntermediateDirectories: true)
let corruptURL = corruptDirectory.appendingPathComponent("identity.plist")
let corruptBytes = Data("not an identity".utf8)
try corruptBytes.write(to: corruptURL)
try rejects("corrupt identity fails without replacement") { _ = try NodeIdentity.loadOrCreate(in: corruptDirectory) }
try check(try Data(contentsOf: corruptURL) == corruptBytes, "corrupt identity remains unchanged")
let incompatibleDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: incompatibleDirectory) }
try FileManager.default.createDirectory(at: incompatibleDirectory, withIntermediateDirectories: true)
let incompatibleURL = incompatibleDirectory.appendingPathComponent("identity.plist")
let incompatibleBytes = try PropertyListSerialization.data(
fromPropertyList: ["version": 999, "signing": Data(repeating: 1, count: 32),
"agreement": Data(repeating: 2, count: 32)], format: .binary, options: 0)
try incompatibleBytes.write(to: incompatibleURL)
try rejects("incompatible identity version fails without replacement") {
_ = try NodeIdentity.loadOrCreate(in: incompatibleDirectory)
}
try check(try Data(contentsOf: incompatibleURL) == incompatibleBytes, "incompatible identity remains unchanged")
let orphanDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: orphanDirectory) }
try FileManager.default.createDirectory(at: orphanDirectory, withIntermediateDirectories: true)
let orphanAddressURL = orphanDirectory.appendingPathComponent("address")
let orphanBytes = Data("\(alice.record.address)\n".utf8)
try orphanBytes.write(to: orphanAddressURL)
try rejects("address without identity fails without creating identity") {
_ = try NodeIdentity.loadOrCreate(in: orphanDirectory)
}
try check(!FileManager.default.fileExists(atPath: orphanDirectory.appendingPathComponent("identity.plist").path) &&
(try Data(contentsOf: orphanAddressURL)) == orphanBytes, "orphan address state remains unchanged")
let mismatchDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: mismatchDirectory) }
let mismatchIdentity = try NodeIdentity.loadOrCreate(in: mismatchDirectory)
let mismatchAddressURL = mismatchDirectory.appendingPathComponent("address")
let mismatchedBytes = Data("\(bob.record.address)\n".utf8)
try mismatchedBytes.write(to: mismatchAddressURL, options: .atomic)
try rejects("address and identity mismatch fails") { _ = try NodeIdentity.loadOrCreate(in: mismatchDirectory) }
try check(try Data(contentsOf: mismatchAddressURL) == mismatchedBytes &&
mismatchIdentity.record.address != bob.record.address, "mismatched address remains unchanged")
let unreadableDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: unreadableDirectory) }
_ = try NodeIdentity.loadOrCreate(in: unreadableDirectory)
let unreadableURL = unreadableDirectory.appendingPathComponent("identity.plist")
let unreadableBytes = try Data(contentsOf: unreadableURL)
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: unreadableURL.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: unreadableURL.path) }
try rejects("unreadable identity fails without replacement") {
_ = try NodeIdentity.loadOrCreate(in: unreadableDirectory)
}
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: unreadableURL.path)
try check(try Data(contentsOf: unreadableURL) == unreadableBytes, "unreadable identity remains unchanged")
let original = InnerFrame(kind: .text, port: 7000, payload: Data("hello".utf8), sourceRecord: alice.record)
let sealed = try alice.seal(original, to: bob.record)
let opened = try bob.open(sealed, expectedSource: alice.record.address)
try check(opened.payload == Data("hello".utf8), "end-to-end encryption round trip")
var corrupted = sealed.combinedCiphertext; corrupted[corrupted.startIndex] ^= 1
let tampered = SealedPayload(ephemeralPublicKey: sealed.ephemeralPublicKey, combinedCiphertext: corrupted)
var rejected = false
do { _ = try bob.open(tampered, expectedSource: alice.record.address) } catch { rejected = true }
try check(rejected, "tampered ciphertext rejected")
let replayID = UUID()
let nativeFrame = InnerFrame(kind: .ipv6Packet, payload: Data([1, 2]), sourceRecord: alice.record, replayID: replayID)
let nativeOpened = try bob.open(alice.seal(nativeFrame, to: bob.record), expectedSource: alice.record.address)
try check(nativeOpened.replayID == replayID, "signed native packet replay UUID round trip")
let router = LinkStateRouter(local: alice.record.address)
try check(router.ingest(try alice.makeLinkState(sequence: 1, neighbors: [bob.record.address])), "local topology accepted")
try check(router.ingest(try bob.makeLinkState(sequence: 1, neighbors: [alice.record.address, carol.record.address])), "relay topology accepted")
try check(router.ingest(try carol.makeLinkState(sequence: 1, neighbors: [bob.record.address])), "remote topology accepted")
let route = router.routes()[carol.record.address]
try check(route?.nextHop == bob.record.address && route?.hopCount == 2, "three-node route uses relay")
try check(!router.ingest(try carol.makeLinkState(sequence: 1, neighbors: [])), "stale topology rejected")
let forged = LinkState(origin: carol.record, sequence: 2, neighbors: [], signature: Data(repeating: 0, count: 64))
try check(!router.ingest(forged), "forged topology rejected")
try check((try NodeIdentity.loadOrCreate(in: directory)).record.address == stored1.record.address,
"route and peer topology changes do not alter persistent local address")
let firewall = MeshFirewall()
try check(!firewall.allows(port: 80, source: alice.record.address), "firewall defaults to deny")
firewall.allow(port: 80, source: alice.record.address)
try check(firewall.allows(port: 80, source: alice.record.address), "scoped firewall rule allows source")
try check(!firewall.allows(port: 80, source: bob.record.address), "scoped firewall rule denies other source")
firewall.allow(port: 443, source: nil)
try check(firewall.allows(port: 443, source: bob.record.address), "any-source firewall rule")
let migrated = try JSONDecoder().decode(FirewallRule.self, from: Data("{\"port\":7000}".utf8))
try check(migrated.protocolKind == .overlay, "protocol-less firewall rule migrates to overlay")
firewall.allow(protocol: .tcp, port: 8080, source: nil)
try check(firewall.allows(protocol: .tcp, port: 8080, source: carol.record.address) &&
!firewall.allows(protocol: .udp, port: 8080, source: carol.record.address), "firewall protocols remain distinct")
let syn = ipv6Packet(source: alice.record.address, destination: bob.record.address, next: 6,
payload: tcpHeader(source: 50_000, destination: 8080, flags: 0x02))
let synInfo = try IPv6PacketParser.parse(syn)
try check(synInfo.transport == .tcp && synInfo.destinationPort == 8080, "TCP packet parsed")
let udp = ipv6Packet(source: alice.record.address, destination: bob.record.address, next: 17,
payload: udpHeader(source: 50_001, destination: 5353, payload: Data([1, 2, 3])))
try check(try IPv6PacketParser.parse(udp).transport == .udp, "UDP packet parsed")
let echo = ipv6Packet(source: alice.record.address, destination: bob.record.address, next: 58,
payload: Data([128, 0, 0, 0, 0, 1, 0, 1]))
try check(try IPv6PacketParser.parse(echo).icmpType == 128, "ICMPv6 echo parsed")
let extensionHeader = Data([6, 0, 0, 0, 0, 0, 0, 0]) + tcpHeader(source: 1, destination: 2, flags: 0x02)
let extended = ipv6Packet(source: alice.record.address, destination: bob.record.address, next: 0, payload: extensionHeader)
try check(try IPv6PacketParser.parse(extended).transportOffset == 48, "IPv6 extension header parsed safely")
var badOption = extensionHeader; badOption[2] = 5; badOption[3] = 10
var badOptionRejected = false
do { _ = try IPv6PacketParser.parse(ipv6Packet(source: alice.record.address, destination: bob.record.address,
next: 0, payload: badOption)) } catch { badOptionRejected = true }
try check(badOptionRejected, "malformed IPv6 option rejected")
var fragmentRejected = false
do { _ = try IPv6PacketParser.parse(ipv6Packet(source: alice.record.address, destination: bob.record.address,
next: 44, payload: Data(repeating: 0, count: 8))) } catch { fragmentRejected = true }
try check(fragmentRejected, "IPv6 fragments rejected")
var malformedRejected = false; var malformedUDP = udp; malformedUDP[44] = 0; malformedUDP[45] = 8
do { _ = try IPv6PacketParser.parse(malformedUDP) } catch { malformedRejected = true }
try check(malformedRejected, "malformed UDP length rejected")
try check(synInfo.source != carol.record.address, "packet source exposes spoof mismatch")
let packetFrame = InnerFrame(kind: .ipv6Packet, payload: syn, sourceRecord: alice.record, replayID: UUID())
let routedNative = RoutedPacket(source: alice.record.address, destination: carol.record.address,
sealed: try alice.seal(packetFrame, to: carol.record), trafficClass: .nativeIPv6)
try check(router.routes()[carol.record.address]?.nextHop == bob.record.address, "native packet selects three-node next hop")
var relayCannotDecrypt = false
do { _ = try bob.open(routedNative.sealed, expectedSource: alice.record.address) } catch { relayCannotDecrypt = true }
let destinationFrame = try carol.open(routedNative.sealed, expectedSource: alice.record.address)
try check(relayCannotDecrypt && destinationFrame.payload == syn, "native packet relays end-to-end encrypted")
var replayCache = Set<UUID>(); let packetReplayID = destinationFrame.replayID!
try check(replayCache.insert(packetReplayID).inserted && !replayCache.insert(packetReplayID).inserted,
"native replay UUID rejects duplicate")
var nativeBytes = 0
for _ in 0..<900 {
let large = ipv6Packet(source: alice.record.address, destination: carol.record.address, next: 17,
payload: udpHeader(source: 40_000, destination: 8080,
payload: Data(repeating: 0x5a, count: 1_160)))
_ = try IPv6PacketParser.parse(large); nativeBytes += large.count
}
try check(nativeBytes > 1_048_576, "native packet path exceeds legacy 1 MiB stream cap")
try check(router.ingest(try alice.makeLinkState(sequence: 2, neighbors: [carol.record.address])) &&
router.ingest(try carol.makeLinkState(sequence: 2, neighbors: [alice.record.address])), "route-change topology accepted")
try check(router.routes()[carol.record.address]?.nextHop == carol.record.address,
"retransmitted native packet uses current route")
let nativeRules = MeshFirewall(); let stateful = NativePacketFirewall(rules: nativeRules, maximumFlows: 4)
try check(stateful.allowOutbound(synInfo), "outbound TCP allowed and tracked")
let synAck = ipv6Packet(source: bob.record.address, destination: alice.record.address, next: 6,
payload: tcpHeader(source: 8080, destination: 50_000, flags: 0x12))
try check(stateful.allowInbound(try IPv6PacketParser.parse(synAck)), "TCP return packet admitted")
let unsolicited = ipv6Packet(source: bob.record.address, destination: alice.record.address, next: 6,
payload: tcpHeader(source: 9000, destination: 9001, flags: 0x02))
try check(!stateful.allowInbound(try IPv6PacketParser.parse(unsolicited)), "new inbound TCP denied by default")
nativeRules.allow(protocol: .tcp, port: 9001, source: bob.record.address)
try check(stateful.allowInbound(try IPv6PacketParser.parse(unsolicited)), "source-scoped inbound TCP admitted")
let aaaaResponse = try MeshDNSCodec.response(to: dnsQuery("alice.mesh", type: 28), aliases: ["alice": alice.record.address])
try check(aaaaResponse[7] == 1 && aaaaResponse.suffix(16) == alice.record.address.bytes, "DNS AAAA response")
let noData = try MeshDNSCodec.response(to: dnsQuery("alice.mesh", type: 1), aliases: ["alice": alice.record.address])
try check(noData[3] & 0x0f == 0 && noData[7] == 0, "DNS A query returns NODATA")
let notFound = try MeshDNSCodec.response(to: dnsQuery("missing.mesh", type: 28), aliases: [:])
try check(notFound[3] & 0x0f == 3, "DNS unknown alias returns NXDOMAIN")
var compressionRejected = false
var compressed = dnsQuery("x.mesh", type: 28); compressed.replaceSubrange(12..<20, with: Data([0xc0, 0x0c, 0, 28, 0, 1]))
do { _ = try MeshDNSCodec.response(to: compressed, aliases: [:]) } catch { compressionRejected = true }
try check(compressionRejected, "DNS compression loop rejected")
let desired: Set<MeshAddress> = [bob.record.address, carol.record.address]
try HelperRouteSet.validate(desired, local: alice.record.address)
let routeDiff = HelperRouteSet.diff(current: [bob.record.address], desired: desired)
try check(routeDiff.additions == [carol.record.address] && routeDiff.removals.isEmpty, "helper route-set diff is idempotent")
var localRouteRejected = false
do { try HelperRouteSet.validate([alice.record.address], local: alice.record.address) } catch { localRouteRejected = true }
try check(localRouteRejected, "helper rejects a route to the local identity")
var sockets = [Int32](repeating: -1, count: 2); var pipeFDs = [Int32](repeating: -1, count: 2)
guard socketpair(AF_UNIX, SOCK_STREAM, 0, &sockets) == 0, pipe(&pipeFDs) == 0 else { throw TestFailure("socket setup") }
defer { sockets.forEach { Darwin.close($0) }; pipeFDs.forEach { Darwin.close($0) } }
var peerUID: uid_t = 0, peerGID: gid_t = 0
try check(umn_get_peer_eid(sockets[0], &peerUID, &peerGID) == 0 && peerUID == getuid(), "helper peer UID authentication primitive")
let marker = Data("FD\n".utf8)
let fdSent = marker.withUnsafeBytes { umn_send_fd(sockets[0], pipeFDs[0], $0.baseAddress, marker.count) }
var receivedFD: Int32 = -1; var fdBuffer = [UInt8](repeating: 0, count: 8)
let fdCount = umn_recv_fd(sockets[1], &receivedFD, &fdBuffer, fdBuffer.count)
defer { if receivedFD >= 0 { Darwin.close(receivedFD) } }
try check(fdSent == 0 && fdCount == marker.count && receivedFD >= 0, "utun descriptor passing primitive")
let framed = try FrameCodec.encode(WireEnvelope(message: .keepalive))
try check(try FrameCodec.bodyLength(from: framed.prefix(4)) == framed.count - 4, "frame length prefix")
let decoded = try FrameCodec.decode(WireEnvelope.self, body: framed.dropFirst(4))
if decoded.version == 2, case .keepalive = decoded.message { try check(true, "wire v2 frame round trip") }
else { throw TestFailure("wire frame round trip") }
var zeroRejected = false
do { _ = try FrameCodec.bodyLength(from: Data([0, 0, 0, 0])) } catch { zeroRejected = true }
try check(zeroRejected, "zero-length frame rejected")
print("1..\(passed)")
print("all core self-tests passed")
} catch {
fputs("not ok - \(error.localizedDescription)\n", stderr)
exit(1)
}