inital commit

This commit is contained in:
Owen Qwen
2026-08-31 15:55:09 -05:00
commit e71b3ada71
22 changed files with 1783 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
import Foundation
import CryptoKit
public final class NodeIdentity: @unchecked Sendable {
public static let currentVersion = 1
public let signingKey: Curve25519.Signing.PrivateKey
public let agreementKey: Curve25519.KeyAgreement.PrivateKey
public let record: NodeRecord
public init(signingKey: Curve25519.Signing.PrivateKey = .init(),
agreementKey: Curve25519.KeyAgreement.PrivateKey = .init()) throws {
self.signingKey = signingKey; self.agreementKey = agreementKey
let signing = signingKey.publicKey.rawRepresentation
self.record = NodeRecord(address: try MeshAddress.derive(from: signing),
signingPublicKey: signing,
agreementPublicKey: agreementKey.publicKey.rawRepresentation)
}
public static func loadOrCreate(at url: URL) throws -> NodeIdentity {
struct Stored: Codable { let version: Int; let signing: Data; let agreement: Data }
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: url) {
let value = try decoder.decode(Stored.self, from: data)
guard value.version == currentVersion else { throw UMNError.invalidIdentity }
return try NodeIdentity(signingKey: .init(rawRepresentation: value.signing),
agreementKey: .init(rawRepresentation: value.agreement))
}
let identity = try NodeIdentity()
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
let data = try encoder.encode(Stored(version: currentVersion,
signing: identity.signingKey.rawRepresentation,
agreement: identity.agreementKey.rawRepresentation))
try data.write(to: url, options: .atomic)
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
return identity
}
public func sign(_ data: Data) throws -> Data { try signingKey.signature(for: data) }
public func makeLinkState(sequence: UInt64, neighbors: [MeshAddress]) throws -> LinkState {
let unsigned = LinkState(origin: record, sequence: sequence, neighbors: neighbors, signature: Data())
return LinkState(origin: record, sequence: sequence, neighbors: neighbors,
signature: try sign(unsigned.signingBytes()))
}
public func seal(_ inner: InnerFrame, to destination: NodeRecord) throws -> SealedPayload {
guard destination.validate() else { throw UMNError.invalidIdentity }
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
let innerData = try encoder.encode(inner)
struct Signed: Codable { let inner: Data; let signature: Data }
let signed = try encoder.encode(Signed(inner: innerData, signature: sign(innerData)))
let ephemeral = Curve25519.KeyAgreement.PrivateKey()
let remote = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: destination.agreementPublicKey)
let secret = try ephemeral.sharedSecretFromKeyAgreement(with: remote)
let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8),
sharedInfo: destination.address.bytes, outputByteCount: 32)
let box = try ChaChaPoly.seal(signed, using: key)
return SealedPayload(ephemeralPublicKey: ephemeral.publicKey.rawRepresentation,
combinedCiphertext: box.combined)
}
public func open(_ payload: SealedPayload, expectedSource: MeshAddress) throws -> InnerFrame {
struct Signed: Codable { let inner: Data; let signature: Data }
let ephemeral = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: payload.ephemeralPublicKey)
let secret = try agreementKey.sharedSecretFromKeyAgreement(with: ephemeral)
let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8),
sharedInfo: record.address.bytes, outputByteCount: 32)
let box = try ChaChaPoly.SealedBox(combined: payload.combinedCiphertext)
let data = try ChaChaPoly.open(box, using: key)
let decoder = PropertyListDecoder()
let signed = try decoder.decode(Signed.self, from: data)
let inner = try decoder.decode(InnerFrame.self, from: signed.inner)
guard inner.sourceRecord.address == expectedSource, inner.sourceRecord.validate() else { throw UMNError.invalidIdentity }
let keyVerify = try Curve25519.Signing.PublicKey(rawRepresentation: inner.sourceRecord.signingPublicKey)
guard keyVerify.isValidSignature(signed.signature, for: signed.inner) else { throw UMNError.invalidIdentity }
return inner
}
}
+21
View File
@@ -0,0 +1,21 @@
import Foundation
public struct FirewallRule: Codable, Hashable, Sendable {
public let port: UInt16
public let source: MeshAddress?
public init(port: UInt16, source: MeshAddress?) { self.port = port; self.source = source }
}
public final class MeshFirewall: @unchecked Sendable {
private var rules: Set<FirewallRule>
private let lock = NSLock()
public init(rules: Set<FirewallRule> = []) { self.rules = rules }
public func allows(port: UInt16, source: MeshAddress) -> Bool {
lock.lock(); defer { lock.unlock() }
return rules.contains(FirewallRule(port: port, source: nil)) || rules.contains(FirewallRule(port: port, source: source))
}
public func allow(port: UInt16, source: MeshAddress?) { lock.lock(); rules.insert(.init(port: port, source: source)); lock.unlock() }
public func revoke(port: UInt16, source: MeshAddress?) { lock.lock(); rules.remove(.init(port: port, source: source)); lock.unlock() }
public func allRules() -> [FirewallRule] { lock.lock(); defer { lock.unlock() }; return rules.sorted { $0.port < $1.port } }
}
+51
View File
@@ -0,0 +1,51 @@
import Foundation
public enum FrameCodec {
public static let maximumFrameSize = 1_200_000
public static func encode<T: Encodable>(_ value: T) throws -> Data {
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
let body = try encoder.encode(value)
guard body.count <= maximumFrameSize else { throw UMNError.invalidFrame }
var size = UInt32(body.count).bigEndian
return withUnsafeBytes(of: &size) { Data($0) } + body
}
public static func decode<T: Decodable>(_ type: T.Type, body: Data) throws -> T {
guard body.count <= maximumFrameSize else { throw UMNError.invalidFrame }
return try PropertyListDecoder().decode(type, from: body)
}
public static func bodyLength(from prefix: Data) throws -> Int {
guard prefix.count == 4 else { throw UMNError.invalidFrame }
let value = prefix.reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
guard value > 0, value <= maximumFrameSize else { throw UMNError.invalidFrame }
return Int(value)
}
}
public enum IPCOperation: String, Codable, Sendable {
case status, address, peers, routes, ping
case aliasSet, aliasRemove, aliasList
case firewallAllow, firewallRevoke, firewallList
case textSend, bindText, bindStream, openStream, streamData, streamClose
}
public struct IPCMessage: Codable, Sendable {
public var version: UInt16
public var operation: IPCOperation
public var requestID: UUID
public var target: String?
public var source: String?
public var name: String?
public var port: UInt16?
public var streamID: UInt64?
public var data: Data?
public var ok: Bool?
public var message: String?
public var values: [String]?
public init(operation: IPCOperation, requestID: UUID = UUID()) {
self.version = 1; self.operation = operation; self.requestID = requestID
}
}
+188
View File
@@ -0,0 +1,188 @@
import Foundation
import CryptoKit
public enum UMNError: Error, LocalizedError {
case invalidAddress, invalidFrame, invalidIdentity, cryptoFailure
case noRoute, denied, unbound, transferLimit, timeout
case message(String)
public var errorDescription: String? {
switch self {
case .invalidAddress: "invalid mesh address"
case .invalidFrame: "invalid protocol frame"
case .invalidIdentity: "identity verification failed"
case .cryptoFailure: "cryptographic operation failed"
case .noRoute: "no route to destination"
case .denied: "destination firewall denied the connection"
case .unbound: "destination port has no bound service"
case .transferLimit: "the 1 MiB stream limit was exceeded"
case .timeout: "operation timed out"
case .message(let value): value
}
}
}
public struct MeshAddress: Hashable, Codable, Comparable, CustomStringConvertible, Sendable {
public let bytes: Data
public init(bytes: Data) throws {
guard bytes.count == 16, bytes.first == 0xfd else { throw UMNError.invalidAddress }
self.bytes = bytes
}
public init(_ text: String) throws {
var raw = in6_addr()
guard inet_pton(AF_INET6, text, &raw) == 1 else { throw UMNError.invalidAddress }
let data = withUnsafeBytes(of: &raw) { Data($0) }
try self.init(bytes: data)
}
public static func derive(from signingPublicKey: Data) throws -> MeshAddress {
let digest = Data(SHA256.hash(data: signingPublicKey))
return try MeshAddress(bytes: Data([0xfd]) + digest.prefix(15))
}
public var description: String {
var raw = in6_addr()
_ = withUnsafeMutableBytes(of: &raw) { bytes.copyBytes(to: $0) }
var output = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN))
guard inet_ntop(AF_INET6, &raw, &output, socklen_t(INET6_ADDRSTRLEN)) != nil else {
return bytes.map { String(format: "%02x", $0) }.joined()
}
return String(cString: output)
}
public static func < (lhs: MeshAddress, rhs: MeshAddress) -> Bool {
lhs.bytes.lexicographicallyPrecedes(rhs.bytes)
}
}
public struct NodeRecord: Codable, Hashable, Sendable {
public let address: MeshAddress
public let signingPublicKey: Data
public let agreementPublicKey: Data
public init(address: MeshAddress, signingPublicKey: Data, agreementPublicKey: Data) {
self.address = address
self.signingPublicKey = signingPublicKey
self.agreementPublicKey = agreementPublicKey
}
public func validate() -> Bool {
guard signingPublicKey.count == 32, agreementPublicKey.count == 32,
let derived = try? MeshAddress.derive(from: signingPublicKey) else { return false }
return derived == address
}
}
public struct LinkState: Codable, Hashable, Sendable {
public let origin: NodeRecord
public let sequence: UInt64
public let neighbors: [MeshAddress]
public let signature: Data
public init(origin: NodeRecord, sequence: UInt64, neighbors: [MeshAddress], signature: Data) {
self.origin = origin
self.sequence = sequence
self.neighbors = neighbors.sorted()
self.signature = signature
}
public func signingBytes() throws -> Data {
struct Unsigned: Codable { let origin: NodeRecord; let sequence: UInt64; let neighbors: [MeshAddress] }
let encoder = PropertyListEncoder(); encoder.outputFormat = .binary
return try encoder.encode(Unsigned(origin: origin, sequence: sequence, neighbors: neighbors.sorted()))
}
public func validate() -> Bool {
guard origin.validate(), let key = try? Curve25519.Signing.PublicKey(rawRepresentation: origin.signingPublicKey),
let bytes = try? signingBytes() else { return false }
return key.isValidSignature(signature, for: bytes)
}
}
public enum PacketKind: String, Codable, Sendable {
case pingRequest, pingReply, text, streamOpen, streamData, streamAck, streamClose, streamReset, error
}
public struct InnerFrame: Codable, Sendable {
public let kind: PacketKind
public let streamID: UInt64
public let port: UInt16?
public let sequence: UInt64
public let acknowledgment: UInt64
public let payload: Data
public let sourceRecord: NodeRecord
public init(kind: PacketKind, streamID: UInt64 = 0, port: UInt16? = nil,
sequence: UInt64 = 0, acknowledgment: UInt64 = 0,
payload: Data = Data(), sourceRecord: NodeRecord) {
self.kind = kind; self.streamID = streamID; self.port = port
self.sequence = sequence; self.acknowledgment = acknowledgment
self.payload = payload; self.sourceRecord = sourceRecord
}
}
public struct SealedPayload: Codable, Sendable {
public let ephemeralPublicKey: Data
public let combinedCiphertext: Data
public init(ephemeralPublicKey: Data, combinedCiphertext: Data) {
self.ephemeralPublicKey = ephemeralPublicKey
self.combinedCiphertext = combinedCiphertext
}
}
public struct RoutedPacket: Codable, Sendable {
public let id: UUID
public let source: MeshAddress
public let destination: MeshAddress
public var hopLimit: UInt8
public let sealed: SealedPayload
public init(id: UUID = UUID(), source: MeshAddress, destination: MeshAddress,
hopLimit: UInt8 = 16, sealed: SealedPayload) {
self.id = id; self.source = source; self.destination = destination
self.hopLimit = hopLimit; self.sealed = sealed
}
}
public enum WireMessage: Codable, Sendable {
case hello(NodeRecord)
case linkState(LinkState)
case packet(RoutedPacket)
case keepalive
private enum CodingKeys: String, CodingKey { case type, payload }
private enum Kind: String, Codable { case hello, linkState, packet, keepalive }
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
switch try c.decode(Kind.self, forKey: .type) {
case .hello: self = .hello(try c.decode(NodeRecord.self, forKey: .payload))
case .linkState: self = .linkState(try c.decode(LinkState.self, forKey: .payload))
case .packet: self = .packet(try c.decode(RoutedPacket.self, forKey: .payload))
case .keepalive: self = .keepalive
}
}
public func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .hello(let value): try c.encode(Kind.hello, forKey: .type); try c.encode(value, forKey: .payload)
case .linkState(let value): try c.encode(Kind.linkState, forKey: .type); try c.encode(value, forKey: .payload)
case .packet(let value): try c.encode(Kind.packet, forKey: .type); try c.encode(value, forKey: .payload)
case .keepalive: try c.encode(Kind.keepalive, forKey: .type)
}
}
}
public struct WireEnvelope: Codable, Sendable {
public static let currentVersion: UInt16 = 1
public let version: UInt16
public let message: WireMessage
public init(message: WireMessage, version: UInt16 = currentVersion) {
self.version = version; self.message = message
}
}
+78
View File
@@ -0,0 +1,78 @@
import Foundation
public struct Route: Equatable, Sendable {
public let destination: MeshAddress
public let nextHop: MeshAddress
public let hopCount: Int
}
public final class LinkStateRouter: @unchecked Sendable {
private struct Entry { let state: LinkState; var receivedAt: Date }
private var database: [MeshAddress: Entry] = [:]
private let local: MeshAddress
private let lock = NSLock()
public init(local: MeshAddress) { self.local = local }
@discardableResult
public func ingest(_ state: LinkState, now: Date = Date()) -> Bool {
guard state.validate(), state.neighbors.count <= 32 else { return false }
lock.lock(); defer { lock.unlock() }
if let existing = database[state.origin.address], existing.state.sequence >= state.sequence { return false }
if database[state.origin.address] == nil, database.count >= 32 { return false }
database[state.origin.address] = Entry(state: state, receivedAt: now)
return true
}
public func expire(olderThan seconds: TimeInterval = 30, now: Date = Date()) {
lock.lock(); defer { lock.unlock() }
database = database.filter { now.timeIntervalSince($0.value.receivedAt) <= seconds || $0.key == local }
}
public func record(for address: MeshAddress) -> NodeRecord? {
lock.lock(); defer { lock.unlock() }
return database[address]?.state.origin
}
public func allRecords() -> [NodeRecord] {
lock.lock(); defer { lock.unlock() }
return database.values.map(\.state.origin)
}
public func allStates() -> [LinkState] {
lock.lock(); defer { lock.unlock() }
return database.values.map(\.state)
}
public func routes() -> [MeshAddress: Route] {
lock.lock(); let snapshot = database; lock.unlock()
var graph: [MeshAddress: Set<MeshAddress>] = [:]
for entry in snapshot.values {
let origin = entry.state.origin.address
for neighbor in entry.state.neighbors {
graph[origin, default: []].insert(neighbor)
graph[neighbor, default: []].insert(origin)
}
}
var distance: [MeshAddress: Int] = [local: 0]
var firstHop: [MeshAddress: MeshAddress] = [:]
var queue = [local]
while !queue.isEmpty {
let current = queue.removeFirst()
guard let base = distance[current], base < 16 else { continue }
for neighbor in (graph[current] ?? []).sorted() {
let candidateFirst = current == local ? neighbor : firstHop[current]!
let candidateDistance = base + 1
if distance[neighbor] == nil || candidateDistance < distance[neighbor]! ||
(candidateDistance == distance[neighbor]! && candidateFirst < firstHop[neighbor]!) {
distance[neighbor] = candidateDistance; firstHop[neighbor] = candidateFirst
queue.append(neighbor)
}
}
}
return Dictionary(uniqueKeysWithValues: distance.compactMap { address, hops in
guard address != local, let hop = firstHop[address] else { return nil }
return (address, Route(destination: address, nextHop: hop, hopCount: hops))
})
}
}
+114
View File
@@ -0,0 +1,114 @@
import Foundation
import Darwin
public enum UnixIPC {
public static let defaultSocketPath = ProcessInfo.processInfo.environment["UMN_SOCKET"] ??
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/UltraMesh/umnd.sock").path
public static func connect(path: String = defaultSocketPath) throws -> Int32 {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) }
do {
var address = try unixAddress(path: path)
let result = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.connect(fd, $0, unixAddressLength(path: path))
}
}
guard result == 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) }
return fd
} catch {
Darwin.close(fd); throw error
}
}
public static func listen(path: String = defaultSocketPath) throws -> Int32 {
try FileManager.default.createDirectory(at: URL(fileURLWithPath: path).deletingLastPathComponent(),
withIntermediateDirectories: true)
unlink(path)
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) }
var address = try unixAddress(path: path)
let bound = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.bind(fd, $0, unixAddressLength(path: path))
}
}
guard bound == 0, Darwin.listen(fd, 64) == 0 else {
let code = errno; Darwin.close(fd); throw POSIXError(.init(rawValue: code) ?? .EIO)
}
chmod(path, 0o600)
return fd
}
public static func accept(_ server: Int32) throws -> Int32 {
let fd = Darwin.accept(server, nil, nil)
guard fd >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) }
return fd
}
public static func send(_ message: IPCMessage, to fd: Int32) throws {
try writeAll(FrameCodec.encode(message), to: fd)
}
public static func receive(from fd: Int32) throws -> IPCMessage? {
guard let prefix = try readExact(4, from: fd) else { return nil }
let count = try FrameCodec.bodyLength(from: prefix)
guard let body = try readExact(count, from: fd) else { throw UMNError.invalidFrame }
return try FrameCodec.decode(IPCMessage.self, body: body)
}
public static func close(_ fd: Int32) { Darwin.shutdown(fd, SHUT_RDWR); Darwin.close(fd) }
private static func readExact(_ count: Int, from fd: Int32) throws -> Data? {
var output = Data(); output.reserveCapacity(count)
var buffer = [UInt8](repeating: 0, count: min(16_384, count))
while output.count < count {
let wanted = min(buffer.count, count - output.count)
let result = Darwin.read(fd, &buffer, wanted)
if result == 0 { return output.isEmpty ? nil : output }
if result < 0 {
if errno == EINTR { continue }
throw POSIXError(.init(rawValue: errno) ?? .EIO)
}
output.append(buffer, count: result)
}
return output
}
private static func writeAll(_ data: Data, to fd: Int32) throws {
try data.withUnsafeBytes { raw in
guard let base = raw.baseAddress else { return }
var offset = 0
while offset < data.count {
let result = Darwin.write(fd, base.advanced(by: offset), data.count - offset)
if result < 0 {
if errno == EINTR { continue }
throw POSIXError(.init(rawValue: errno) ?? .EIO)
}
offset += result
}
}
}
private static func unixAddress(path: String) throws -> sockaddr_un {
guard path.utf8.count < MemoryLayout.size(ofValue: sockaddr_un().sun_path) else {
throw UMNError.message("IPC socket path is too long")
}
var address = sockaddr_un(); address.sun_family = sa_family_t(AF_UNIX)
let capacity = MemoryLayout.size(ofValue: address.sun_path)
withUnsafeMutablePointer(to: &address.sun_path) { pointer in
pointer.withMemoryRebound(to: CChar.self, capacity: capacity) { destination in
path.withCString { source in
_ = strncpy(destination, source, capacity - 1)
}
}
}
return address
}
private static func unixAddressLength(path: String) -> socklen_t {
socklen_t(MemoryLayout<sa_family_t>.size + path.utf8.count + 1)
}
}
+71
View File
@@ -0,0 +1,71 @@
import Foundation
import UltraMeshCore
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)")
}
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 stored1 = try NodeIdentity.loadOrCreate(at: identityURL)
let stored2 = try NodeIdentity.loadOrCreate(at: identityURL)
try check(stored1.record == stored2.record, "identity persists")
let attributes = try FileManager.default.attributesOfItem(atPath: identityURL.path)
try check((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o600, "identity permissions")
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 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")
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 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 == 1, case .keepalive = decoded.message { try check(true, "wire 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)
}
+118
View File
@@ -0,0 +1,118 @@
import Foundation
import Darwin
import UltraMeshCore
final class StreamBridge {
let streamID: UInt64
let socket: Int32
private let daemon: DaemonConnection
private let writeLock = NSLock()
init(streamID: UInt64, socket: Int32, daemon: DaemonConnection) {
self.streamID = streamID; self.socket = socket; self.daemon = daemon
}
deinit { Darwin.shutdown(socket, SHUT_RDWR); Darwin.close(socket) }
func startReading() {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self else { return }
do {
while let data = try SocketIO.readSome(self.socket) {
var request = IPCMessage(operation: .streamData)
request.streamID = self.streamID; request.data = data
try self.daemon.send(request)
}
} catch {}
var close = IPCMessage(operation: .streamClose); close.streamID = self.streamID
try? self.daemon.send(close)
}
}
func write(_ data: Data) {
writeLock.lock(); defer { writeLock.unlock() }; try? SocketIO.write(data, fd: socket)
}
}
func runExpose(meshPort: UInt16, host: String, localPort: UInt16) throws -> Never {
let daemon = try DaemonConnection()
var bind = IPCMessage(operation: .bindStream); bind.port = meshPort
guard printResponse(try daemon.request(bind)) else { exit(1) }
print("forwarding mesh port \(meshPort) to \(host):\(localPort); press Ctrl-C to stop")
var bridges: [UInt64: StreamBridge] = [:]
while let event = try daemon.receive() {
guard let id = event.streamID else { continue }
switch event.operation {
case .openStream:
do {
let socket = try SocketIO.connect(host: host, port: localPort)
let bridge = StreamBridge(streamID: id, socket: socket, daemon: daemon)
bridges[id] = bridge; bridge.startReading()
} catch {
var close = IPCMessage(operation: .streamClose); close.streamID = id
try? daemon.send(close)
}
case .streamData: if let data = event.data { bridges[id]?.write(data) }
case .streamClose: bridges.removeValue(forKey: id)
default: break
}
}
throw UMNError.message("daemon disconnected")
}
func runSOCKS(port: UInt16) throws -> Never {
let server = try SocketIO.listenLoopback(port: port)
print("SOCKS5 proxy listening on 127.0.0.1:\(port); press Ctrl-C to stop")
while true {
let client = Darwin.accept(server, nil, nil)
if client < 0 { if errno == EINTR { continue }; throw POSIXError(.init(rawValue: errno) ?? .EIO) }
DispatchQueue.global(qos: .userInitiated).async { handleSOCKSClient(client) }
}
}
private func handleSOCKSClient(_ client: Int32) {
defer { Darwin.shutdown(client, SHUT_RDWR); Darwin.close(client) }
do {
guard let greeting = try SocketIO.readExact(2, fd: client), greeting[0] == 5 else { return }
_ = try SocketIO.readExact(Int(greeting[1]), fd: client)
try SocketIO.write(Data([5, 0]), fd: client)
guard let header = try SocketIO.readExact(4, fd: client), header[0] == 5, header[1] == 1 else { return }
let target: String
switch header[3] {
case 3:
guard let length = try SocketIO.readExact(1, fd: client)?.first,
let name = try SocketIO.readExact(Int(length), fd: client),
let value = String(data: name, encoding: .utf8) else { return }
target = value
case 4:
guard let bytes = try SocketIO.readExact(16, fd: client) else { return }
var raw = in6_addr(); _ = withUnsafeMutableBytes(of: &raw) { bytes.copyBytes(to: $0) }
var output = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN))
guard inet_ntop(AF_INET6, &raw, &output, socklen_t(INET6_ADDRSTRLEN)) != nil else { return }
target = String(cString: output)
default:
try SocketIO.write(Data([5, 8, 0, 1, 0, 0, 0, 0, 0, 0]), fd: client); return
}
guard let portData = try SocketIO.readExact(2, fd: client) else { return }
let port = UInt16(portData[0]) << 8 | UInt16(portData[1])
let daemon = try DaemonConnection()
var open = IPCMessage(operation: .openStream); open.target = target; open.port = port
let response = try daemon.request(open)
guard response.ok == true, let id = response.streamID else {
try SocketIO.write(Data([5, 4, 0, 1, 0, 0, 0, 0, 0, 0]), fd: client); return
}
try SocketIO.write(Data([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]), fd: client)
DispatchQueue.global(qos: .userInitiated).async {
do {
while let data = try SocketIO.readSome(client) {
var packet = IPCMessage(operation: .streamData); packet.streamID = id; packet.data = data
try daemon.send(packet)
}
} catch {}
var close = IPCMessage(operation: .streamClose); close.streamID = id; try? daemon.send(close)
}
while let event = try daemon.receive() {
if event.operation == .streamData, event.streamID == id, let data = event.data { try SocketIO.write(data, fd: client) }
if event.operation == .streamClose, event.streamID == id { return }
}
} catch { return }
}
+100
View File
@@ -0,0 +1,100 @@
import Foundation
import Darwin
import UltraMeshCore
final class DaemonConnection {
let fd: Int32
private let lock = NSLock()
init() throws { fd = try UnixIPC.connect() }
deinit { UnixIPC.close(fd) }
func send(_ message: IPCMessage) throws {
lock.lock(); defer { lock.unlock() }; try UnixIPC.send(message, to: fd)
}
func receive() throws -> IPCMessage? { try UnixIPC.receive(from: fd) }
func request(_ message: IPCMessage) throws -> IPCMessage {
try send(message)
guard let response = try receive() else { throw UMNError.message("daemon disconnected") }
return response
}
}
enum SocketIO {
static func listenLoopback(port: UInt16) throws -> Int32 {
let fd = socket(AF_INET, SOCK_STREAM, 0)
guard fd >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) }
var yes: Int32 = 1
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout.size(ofValue: yes)))
var address = sockaddr_in(); address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
address.sin_family = sa_family_t(AF_INET); address.sin_port = port.bigEndian
address.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
let result = withUnsafePointer(to: &address) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.bind(fd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
guard result == 0, Darwin.listen(fd, 64) == 0 else {
let code = errno; close(fd); throw POSIXError(.init(rawValue: code) ?? .EIO)
}
return fd
}
static func connect(host: String, port: UInt16) throws -> Int32 {
var hints = addrinfo(ai_flags: 0, ai_family: AF_UNSPEC, ai_socktype: SOCK_STREAM,
ai_protocol: IPPROTO_TCP, ai_addrlen: 0, ai_canonname: nil, ai_addr: nil, ai_next: nil)
var info: UnsafeMutablePointer<addrinfo>?
guard getaddrinfo(host, String(port), &hints, &info) == 0, let first = info else {
throw UMNError.message("cannot resolve local target")
}
defer { freeaddrinfo(info) }
var cursor: UnsafeMutablePointer<addrinfo>? = first
while let item = cursor {
let fd = socket(item.pointee.ai_family, item.pointee.ai_socktype, item.pointee.ai_protocol)
if fd >= 0, Darwin.connect(fd, item.pointee.ai_addr, item.pointee.ai_addrlen) == 0 { return fd }
if fd >= 0 { close(fd) }; cursor = item.pointee.ai_next
}
throw UMNError.message("cannot connect to local target")
}
static func readExact(_ count: Int, fd: Int32) throws -> Data? {
var data = Data(); var buffer = [UInt8](repeating: 0, count: count)
while data.count < count {
let n = Darwin.read(fd, &buffer, count - data.count)
if n == 0 { return data.isEmpty ? nil : data }
if n < 0 { if errno == EINTR { continue }; throw POSIXError(.init(rawValue: errno) ?? .EIO) }
data.append(buffer, count: n)
}
return data
}
static func readSome(_ fd: Int32, maximum: Int = 32_768) throws -> Data? {
var buffer = [UInt8](repeating: 0, count: maximum)
let n = Darwin.read(fd, &buffer, maximum)
if n == 0 { return nil }
if n < 0 {
if errno == EINTR { return try readSome(fd, maximum: maximum) }
throw POSIXError(.init(rawValue: errno) ?? .EIO)
}
return Data(buffer.prefix(n))
}
static func write(_ data: Data, fd: Int32) throws {
try data.withUnsafeBytes { bytes in
guard let base = bytes.baseAddress else { return }; var offset = 0
while offset < data.count {
let n = Darwin.write(fd, base.advanced(by: offset), data.count - offset)
if n < 0 { if errno == EINTR { continue }; throw POSIXError(.init(rawValue: errno) ?? .EIO) }
offset += n
}
}
}
}
@discardableResult
func printResponse(_ response: IPCMessage) -> Bool {
if let message = response.message { print(message) }
response.values?.forEach { print($0) }
if response.ok != true, response.message == nil { fputs("operation failed\n", stderr) }
return response.ok == true
}
+97
View File
@@ -0,0 +1,97 @@
import Foundation
import UltraMeshCore
let args = Array(CommandLine.arguments.dropFirst())
func usage() -> Never {
print("""
Ultra Mesh Network
umn status | address | peers | routes | ping <target>
umn alias set <name> <address> | remove <name> | list
umn firewall allow|revoke <port> --from <address|any> | list
umn text send <target> <port> <message> | listen <port>
umn expose <mesh-port> --to <host:port>
umn proxy [--listen 127.0.0.1:<port>]
""")
exit(2)
}
func parsePort(_ value: String) -> UInt16 {
guard let port = UInt16(value), port > 0 else { usage() }
return port
}
do {
guard let command = args.first else { usage() }
if command == "expose" {
guard args.count == 4, args[2] == "--to", let split = args[3].lastIndex(of: ":") else { usage() }
let host = String(args[3][..<split])
let localPort = parsePort(String(args[3][args[3].index(after: split)...]))
try runExpose(meshPort: parsePort(args[1]), host: host, localPort: localPort)
}
if command == "proxy" {
var listenPort: UInt16 = 1080
if args.count == 3, args[1] == "--listen", let split = args[2].lastIndex(of: ":") {
listenPort = parsePort(String(args[2][args[2].index(after: split)...]))
} else if args.count != 1 { usage() }
try runSOCKS(port: listenPort)
}
let daemon = try DaemonConnection()
var request: IPCMessage
switch command {
case "status": request = IPCMessage(operation: .status)
case "address": request = IPCMessage(operation: .address)
case "peers": request = IPCMessage(operation: .peers)
case "routes": request = IPCMessage(operation: .routes)
case "ping":
guard args.count == 2 else { usage() }
request = IPCMessage(operation: .ping); request.target = args[1]
case "alias":
guard args.count >= 2 else { usage() }
switch args[1] {
case "list": request = IPCMessage(operation: .aliasList)
case "set":
guard args.count == 4 else { usage() }
request = IPCMessage(operation: .aliasSet); request.name = args[2]; request.target = args[3]
case "remove":
guard args.count == 3 else { usage() }
request = IPCMessage(operation: .aliasRemove); request.name = args[2]
default: usage()
}
case "firewall":
guard args.count >= 2 else { usage() }
if args[1] == "list" { request = IPCMessage(operation: .firewallList) }
else {
guard args.count == 5, args[3] == "--from", ["allow", "revoke"].contains(args[1]) else { usage() }
request = IPCMessage(operation: args[1] == "allow" ? .firewallAllow : .firewallRevoke)
request.port = parsePort(args[2]); request.source = args[4]
}
case "text":
guard args.count >= 3 else { usage() }
if args[1] == "listen" {
request = IPCMessage(operation: .bindText); request.port = parsePort(args[2])
guard printResponse(try daemon.request(request)) else { exit(1) }
while let event = try daemon.receive() {
if event.operation == .textSend, let data = event.data,
let text = String(data: data, encoding: .utf8) {
print("[\(event.source ?? "unknown")] \(text)")
}
}
throw UMNError.message("daemon disconnected")
} else if args[1] == "send" {
guard args.count >= 5 else { usage() }
request = IPCMessage(operation: .textSend); request.target = args[2]
request.port = parsePort(args[3]); request.data = Data(args.dropFirst(4).joined(separator: " ").utf8)
} else { usage() }
default: usage()
}
let result = try daemon.request(request)
exit(printResponse(result) ? 0 : 1)
} catch {
fputs("umn: \(error.localizedDescription)\n", stderr)
if (error as NSError).domain == NSPOSIXErrorDomain {
fputs("Is umnd running? Install it or start with `swift run umnd`.\n", stderr)
}
exit(1)
}
+19
View File
@@ -0,0 +1,19 @@
import Foundation
import UltraMeshCore
struct DaemonConfig: Codable {
var aliases: [String: MeshAddress] = [:]
var firewall: Set<FirewallRule> = []
var lsaSequence: UInt64?
static func load(from url: URL) -> DaemonConfig {
guard let data = try? Data(contentsOf: url), let value = try? JSONDecoder().decode(Self.self, from: data) else { return .init() }
return value
}
func save(to url: URL) throws {
let encoder = JSONEncoder(); encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(self).write(to: url, options: .atomic)
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}
}
+60
View File
@@ -0,0 +1,60 @@
import Foundation
import UltraMeshCore
final class IPCClient {
let fd: Int32
private let writeLock = NSLock()
private var closed = false
var onClose: ((IPCClient) -> Void)?
init(fd: Int32) { self.fd = fd }
func send(_ message: IPCMessage) {
writeLock.lock()
guard !closed else { writeLock.unlock(); return }
do { try UnixIPC.send(message, to: fd); writeLock.unlock() }
catch {
closed = true; writeLock.unlock()
UnixIPC.close(fd); onClose?(self)
}
}
func close() {
writeLock.lock()
guard !closed else { writeLock.unlock(); return }
closed = true; writeLock.unlock()
UnixIPC.close(fd); onClose?(self)
}
}
final class IPCServer {
private let path: String
private var server: Int32 = -1
private var running = true
var onMessage: ((IPCClient, IPCMessage) -> Void)?
init(path: String = UnixIPC.defaultSocketPath) { self.path = path }
func start() throws {
server = try UnixIPC.listen(path: path)
DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.acceptLoop() }
}
func stop() {
running = false
if server >= 0 { UnixIPC.close(server) }
unlink(path)
}
private func acceptLoop() {
while running {
guard let fd = try? UnixIPC.accept(server) else { if running { continue }; return }
let client = IPCClient(fd: fd)
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
while let message = try? UnixIPC.receive(from: fd) {
self?.onMessage?(client, message)
}
client.close()
}
}
}
}
+452
View File
@@ -0,0 +1,452 @@
import Foundation
import Network
import UltraMeshCore
private final class StreamState {
struct Pending { var frame: InnerFrame; var lastSent: Date; var attempts: Int }
let id: UInt64
let remote: NodeRecord
weak var client: IPCClient?
var nextSequence: UInt64 = 1
var expectedSequence: UInt64 = 1
var pending: [UInt64: Pending] = [:]
var queued: [Data] = []
var closing = false
var transferred = 0
var lastProgress = Date()
init(id: UInt64, remote: NodeRecord, client: IPCClient) {
self.id = id; self.remote = remote; self.client = client
}
}
final class MeshDaemon {
private let queue = DispatchQueue(label: "com.ultramesh.daemon")
private let identity: NodeIdentity
private let router: LinkStateRouter
private let firewall: MeshFirewall
private let configURL: URL
private var config: DaemonConfig
private let ipc: IPCServer
private var listener: NWListener?
private var browser: NWBrowser?
private var pendingEndpoints = Set<String>()
private var peers: [MeshAddress: PeerSession] = [:]
private var allSessions: [ObjectIdentifier: PeerSession] = [:]
private var lsaSequence: UInt64 = 0
private var seenPackets = Set<UUID>()
private var seenOrder: [UUID] = []
private var textBindings: [UInt16: IPCClient] = [:]
private var streamBindings: [UInt16: IPCClient] = [:]
private var streams: [UInt64: StreamState] = [:]
private var pendingPings: [UUID: (IPCClient, Date)] = [:]
private var pingWindows: [MeshAddress: (Date, Int)] = [:]
private var timer: DispatchSourceTimer?
init(baseURL: URL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/UltraMesh"), socketPath: String? = nil) throws {
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true)
identity = try NodeIdentity.loadOrCreate(at: baseURL.appendingPathComponent("identity.plist"))
router = LinkStateRouter(local: identity.record.address)
configURL = baseURL.appendingPathComponent("config.json")
config = DaemonConfig.load(from: configURL)
firewall = MeshFirewall(rules: config.firewall)
ipc = IPCServer(path: socketPath ?? baseURL.appendingPathComponent("umnd.sock").path)
let wallClockBase = UInt64(Date().timeIntervalSince1970 * 1000)
lsaSequence = max((config.lsaSequence ?? 0) &+ 1_000_000, wallClockBase)
config.lsaSequence = lsaSequence
try config.save(to: configURL)
}
var address: MeshAddress { identity.record.address }
func start() throws {
try startNetwork()
ipc.onMessage = { [weak self] client, message in self?.queue.async { self?.handleIPC(client, message) } }
try ipc.start()
publishLocalState()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 1, repeating: 1)
timer.setEventHandler { [weak self] in self?.tick() }
timer.resume(); self.timer = timer
log("node \(address) started")
}
func stop() {
queue.sync {
timer?.cancel(); browser?.cancel(); listener?.cancel()
allSessions.values.forEach { $0.stop() }; ipc.stop()
}
}
private func startNetwork() throws {
let parameters = NWParameters.tcp
parameters.includePeerToPeer = true
let listener = try NWListener(using: parameters)
listener.service = .init(name: "umn-\(address.description.suffix(8))", type: "_umn._tcp")
listener.newConnectionHandler = { [weak self] connection in self?.queue.async { self?.addSession(connection, outbound: false) } }
listener.stateUpdateHandler = { [weak self] state in
if case .failed(let error) = state { self?.log("listener failed: \(error)") }
}
listener.start(queue: queue); self.listener = listener
let browseParameters = NWParameters.tcp
browseParameters.includePeerToPeer = true
let browser = NWBrowser(for: .bonjour(type: "_umn._tcp", domain: nil), using: browseParameters)
browser.browseResultsChangedHandler = { [weak self] results, _ in
guard let self else { return }
self.queue.async {
for result in results {
let key = String(describing: result.endpoint)
guard !self.pendingEndpoints.contains(key) else { continue }
self.pendingEndpoints.insert(key)
let connection = NWConnection(to: result.endpoint, using: browseParameters)
self.addSession(connection, outbound: true)
}
}
}
browser.stateUpdateHandler = { [weak self] state in
if case .failed(let error) = state { self?.log("browser failed: \(error)") }
}
browser.start(queue: queue); self.browser = browser
}
private func addSession(_ connection: NWConnection, outbound: Bool) {
let session = PeerSession(connection: connection, outbound: outbound, queue: queue)
allSessions[ObjectIdentifier(session)] = session
session.onMessage = { [weak self] peer, message in self?.queue.async { self?.handle(peer, message) } }
session.onStop = { [weak self] peer in self?.queue.async { self?.remove(peer) } }
session.start(local: identity.record)
}
private func remove(_ session: PeerSession) {
allSessions.removeValue(forKey: ObjectIdentifier(session))
pendingEndpoints.remove(String(describing: session.connection.endpoint))
if let remote = session.remote, peers[remote.address] === session {
peers.removeValue(forKey: remote.address); log("peer \(remote.address) disconnected"); publishLocalState()
let endpoint = session.connection.endpoint
queue.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self, self.peers[remote.address] == nil else { return }
let parameters = NWParameters.tcp; parameters.includePeerToPeer = true
self.addSession(NWConnection(to: endpoint, using: parameters), outbound: true)
}
}
}
private func handle(_ session: PeerSession, _ message: WireMessage) {
switch message {
case .hello(let record):
guard record.validate(), record.address != address else { session.stop(); return }
guard peers[record.address] != nil || peers.count < 31 else { session.stop(); return }
session.remote = record
let shouldBeOutbound = address < record.address
if session.outbound != shouldBeOutbound, let existing = peers[record.address], existing.outbound == shouldBeOutbound {
session.stop(); return
}
if let existing = peers[record.address], existing !== session {
if existing.outbound == shouldBeOutbound { session.stop(); return }
existing.stop()
}
peers[record.address] = session
log("peer \(record.address) connected over \(session.outbound ? "outbound" : "inbound") path")
for state in currentLinkStates() { session.send(.linkState(state)) }
publishLocalState()
case .linkState(let state):
guard session.remote != nil, router.ingest(state) else { return }
broadcast(.linkState(state), excluding: session)
case .packet(var packet):
guard session.remote != nil, markSeen(packet.id) else { return }
if packet.destination == address { receiveLocal(packet) }
else if packet.hopLimit > 1 {
packet.hopLimit -= 1; forward(packet)
}
case .keepalive: break
}
}
private func publishLocalState() {
lsaSequence &+= 1
guard let state = try? identity.makeLinkState(sequence: lsaSequence, neighbors: Array(peers.keys)) else { return }
_ = router.ingest(state); broadcast(.linkState(state), excluding: nil)
}
private func currentLinkStates() -> [LinkState] {
router.allStates()
}
private func broadcast(_ message: WireMessage, excluding: PeerSession?) {
for peer in peers.values where peer !== excluding { peer.send(message) }
}
private func forward(_ packet: RoutedPacket) {
guard let route = router.routes()[packet.destination], let peer = peers[route.nextHop] else { return }
peer.send(.packet(packet))
}
@discardableResult private func send(_ inner: InnerFrame, to remote: NodeRecord) -> Bool {
guard remote.address != address,
let sealed = try? identity.seal(inner, to: remote) else { return false }
let packet = RoutedPacket(source: address, destination: remote.address, sealed: sealed)
guard router.routes()[remote.address] != nil || peers[remote.address] != nil else { return false }
forward(packet); return true
}
private func receiveLocal(_ packet: RoutedPacket) {
guard let inner = try? identity.open(packet.sealed, expectedSource: packet.source) else { return }
switch inner.kind {
case .pingRequest:
guard allowPing(from: packet.source) else { return }
let reply = InnerFrame(kind: .pingReply, streamID: inner.streamID, payload: inner.payload, sourceRecord: identity.record)
_ = send(reply, to: inner.sourceRecord)
case .pingReply:
guard let idString = String(data: inner.payload, encoding: .utf8), let id = UUID(uuidString: idString),
let pending = pendingPings.removeValue(forKey: id) else { return }
var response = IPCMessage(operation: .ping, requestID: id); response.ok = true
response.message = String(format: "reply from %@: %.1f ms", packet.source.description,
Date().timeIntervalSince(pending.1) * 1000)
pending.0.send(response)
case .text:
guard let port = inner.port, firewall.allows(port: port, source: packet.source),
let client = textBindings[port] else { return }
var event = IPCMessage(operation: .textSend); event.ok = true
event.source = packet.source.description; event.port = port; event.data = inner.payload
client.send(event)
case .streamOpen: receiveStreamOpen(inner, from: packet.source)
case .streamData: receiveStreamData(inner)
case .streamAck: receiveStreamAck(inner)
case .streamClose, .streamReset: receiveStreamClose(inner)
case .error: receiveStreamClose(inner)
}
}
private func receiveStreamOpen(_ inner: InnerFrame, from source: MeshAddress) {
if let existing = streams[inner.streamID] {
sendAck(for: existing, sequence: 0); return
}
guard streams.count < 32,
streams.values.filter({ $0.remote.address == source }).count < 8,
let port = inner.port,
firewall.allows(port: port, source: source), let client = streamBindings[port] else {
let reset = InnerFrame(kind: .streamReset, streamID: inner.streamID,
payload: Data("denied or unbound".utf8), sourceRecord: identity.record)
_ = send(reset, to: inner.sourceRecord); return
}
let state = StreamState(id: inner.streamID, remote: inner.sourceRecord, client: client)
streams[inner.streamID] = state
var event = IPCMessage(operation: .openStream); event.ok = true; event.streamID = inner.streamID
event.source = source.description; event.port = port; client.send(event)
sendAck(for: state, sequence: 0)
}
private func receiveStreamData(_ inner: InnerFrame) {
guard let state = streams[inner.streamID] else { return }
if inner.sequence == state.expectedSequence {
guard state.transferred + inner.payload.count <= 1_048_576 else { reset(state, reason: "1 MiB limit exceeded"); return }
state.transferred += inner.payload.count; state.expectedSequence &+= 1; state.lastProgress = Date()
var event = IPCMessage(operation: .streamData); event.streamID = state.id; event.data = inner.payload; event.ok = true
state.client?.send(event)
}
sendAck(for: state, sequence: state.expectedSequence - 1)
}
private func receiveStreamAck(_ inner: InnerFrame) {
guard let state = streams[inner.streamID] else { return }
state.pending = state.pending.filter { $0.key > inner.acknowledgment }
state.lastProgress = Date()
pump(state)
finishIfDrained(state)
}
private func receiveStreamClose(_ inner: InnerFrame) {
guard let state = streams.removeValue(forKey: inner.streamID) else { return }
var event = IPCMessage(operation: .streamClose); event.streamID = state.id; event.data = inner.payload
event.ok = inner.kind == .streamClose; state.client?.send(event)
}
private func sendAck(for state: StreamState, sequence: UInt64) {
let ack = InnerFrame(kind: .streamAck, streamID: state.id, acknowledgment: sequence, sourceRecord: identity.record)
_ = send(ack, to: state.remote)
}
private func reset(_ state: StreamState, reason: String) {
let frame = InnerFrame(kind: .streamReset, streamID: state.id, payload: Data(reason.utf8), sourceRecord: identity.record)
_ = send(frame, to: state.remote); streams.removeValue(forKey: state.id)
var event = IPCMessage(operation: .streamClose); event.streamID = state.id; event.ok = false; event.message = reason
state.client?.send(event)
}
private func sendReliable(_ frame: InnerFrame, state: StreamState) -> Bool {
guard send(frame, to: state.remote) else { return false }
state.pending[frame.sequence] = .init(frame: frame, lastSent: Date(), attempts: 0)
return true
}
private func pump(_ state: StreamState) {
var inFlightBytes = state.pending.values.reduce(0) { $0 + $1.frame.payload.count }
while let data = state.queued.first, inFlightBytes + data.count <= 65_536 {
state.queued.removeFirst()
let frame = InnerFrame(kind: .streamData, streamID: state.id, sequence: state.nextSequence,
payload: data, sourceRecord: identity.record)
state.nextSequence &+= 1
guard sendReliable(frame, state: state) else { state.queued.insert(data, at: 0); return }
inFlightBytes += data.count
}
}
private func finishIfDrained(_ state: StreamState) {
guard state.closing, state.queued.isEmpty, state.pending.isEmpty else { return }
let frame = InnerFrame(kind: .streamClose, streamID: state.id, sequence: state.nextSequence, sourceRecord: identity.record)
_ = send(frame, to: state.remote); streams.removeValue(forKey: state.id)
}
private func handleIPC(_ client: IPCClient, _ message: IPCMessage) {
func reply(_ ok: Bool, _ text: String? = nil, values: [String]? = nil, streamID: UInt64? = nil) {
var response = IPCMessage(operation: message.operation, requestID: message.requestID)
response.ok = ok; response.message = text; response.values = values; response.streamID = streamID
client.send(response)
}
switch message.operation {
case .status:
reply(true, "umnd running as \(address); \(peers.count) direct peer(s), \(router.routes().count) route(s)")
case .address: reply(true, address.description)
case .peers: reply(true, values: peers.keys.sorted().map(\.description))
case .routes:
let values = router.routes().values.sorted { $0.destination < $1.destination }
.map { "\($0.destination) via \($0.nextHop) (\($0.hopCount) hop\($0.hopCount == 1 ? "" : "s"))" }
reply(true, values: values)
case .aliasList:
reply(true, values: config.aliases.sorted { $0.key < $1.key }.map { "\($0.key).mesh \($0.value)" })
case .aliasSet:
guard let name = message.name?.lowercased(), validAlias(name), let target = message.target,
let value = try? MeshAddress(target) else { reply(false, "invalid alias or address"); return }
config.aliases[name] = value; saveConfig(); reply(true, "\(name).mesh -> \(value)")
case .aliasRemove:
guard let name = message.name?.lowercased() else { reply(false, "missing alias"); return }
config.aliases.removeValue(forKey: name); saveConfig(); reply(true)
case .firewallList:
let values = firewall.allRules().map { "allow \($0.port) from \($0.source?.description ?? "any")" }
reply(true, values: values)
case .firewallAllow, .firewallRevoke:
guard let port = message.port, let sourceText = message.source else { reply(false, "missing port or source"); return }
let source: MeshAddress?
if sourceText == "any" { source = nil } else if let parsed = resolve(sourceText) { source = parsed } else { reply(false, "invalid source"); return }
if message.operation == .firewallAllow { firewall.allow(port: port, source: source) }
else { firewall.revoke(port: port, source: source) }
config.firewall = Set(firewall.allRules()); saveConfig(); reply(true)
case .ping:
guard let target = message.target, let remote = record(for: target) else { reply(false, "no route to destination"); return }
let id = message.requestID
let inner = InnerFrame(kind: .pingRequest, streamID: UInt64.random(in: 1...UInt64.max),
payload: Data(id.uuidString.utf8), sourceRecord: identity.record)
guard send(inner, to: remote) else { reply(false, "no route to destination"); return }
pendingPings[id] = (client, Date())
case .textSend:
guard let target = message.target, let remote = record(for: target), let port = message.port,
let data = message.data, data.count <= 4096, String(data: data, encoding: .utf8) != nil else {
reply(false, "invalid destination, port, or UTF-8 message (maximum 4 KiB)"); return
}
let inner = InnerFrame(kind: .text, port: port, payload: data, sourceRecord: identity.record)
let sent = send(inner, to: remote)
reply(sent, sent ? "sent" : "no route to destination")
case .bindText:
guard let port = message.port, textBindings[port] == nil else { reply(false, "port is already bound"); return }
textBindings[port] = client; bindCleanup(client: client); reply(true, "listening on mesh port \(port)")
case .bindStream:
guard let port = message.port, streamBindings[port] == nil else { reply(false, "port is already bound"); return }
streamBindings[port] = client; bindCleanup(client: client); reply(true, "bound mesh port \(port)")
case .openStream:
guard streams.count < 32, let target = message.target, let remote = record(for: target), let port = message.port else {
reply(false, "no route to destination"); return
}
var id = UInt64.random(in: 1...UInt64.max); while streams[id] != nil { id = UInt64.random(in: 1...UInt64.max) }
let state = StreamState(id: id, remote: remote, client: client); streams[id] = state
let open = InnerFrame(kind: .streamOpen, streamID: id, port: port, sequence: 0, sourceRecord: identity.record)
guard sendReliable(open, state: state) else { streams.removeValue(forKey: id); reply(false, "no route"); return }
bindCleanup(client: client); reply(true, "opening", streamID: id)
case .streamData:
guard let id = message.streamID, let state = streams[id], state.client === client, let data = message.data,
state.transferred + data.count <= 1_048_576 else { reply(false, "unknown stream or 1 MiB limit exceeded"); return }
state.transferred += data.count
state.queued.append(data); pump(state)
case .streamClose:
guard let id = message.streamID, let state = streams[id], state.client === client else { return }
state.closing = true; finishIfDrained(state)
}
}
private func bindCleanup(client: IPCClient) {
client.onClose = { [weak self, weak client] _ in
guard let self, let client else { return }
self.queue.async {
self.textBindings = self.textBindings.filter { $0.value !== client }
self.streamBindings = self.streamBindings.filter { $0.value !== client }
let ids = self.streams.filter { $0.value.client === client }.map(\.key)
for id in ids { if let state = self.streams[id] { self.reset(state, reason: "local service disconnected") } }
}
}
}
private func record(for text: String) -> NodeRecord? {
guard let target = resolve(text) else { return nil }
if let peer = peers[target]?.remote { return peer }
return router.record(for: target)
}
private func resolve(_ text: String) -> MeshAddress? {
if let value = try? MeshAddress(text) { return value }
let name = text.lowercased().hasSuffix(".mesh") ? String(text.lowercased().dropLast(5)) : text.lowercased()
return config.aliases[name]
}
private func validAlias(_ value: String) -> Bool {
!value.isEmpty && value.count <= 63 && value.allSatisfy { $0.isLetter || $0.isNumber || $0 == "-" }
}
private func saveConfig() { try? config.save(to: configURL) }
private func markSeen(_ id: UUID) -> Bool {
guard !seenPackets.contains(id) else { return false }
seenPackets.insert(id); seenOrder.append(id)
if seenOrder.count > 4096 { seenPackets.remove(seenOrder.removeFirst()) }
return true
}
private func allowPing(from source: MeshAddress) -> Bool {
let now = Date()
if let current = pingWindows[source], now.timeIntervalSince(current.0) < 1 {
guard current.1 < 10 else { return false }
pingWindows[source] = (current.0, current.1 + 1)
} else {
guard pingWindows[source] != nil || pingWindows.count < 128 else { return false }
pingWindows[source] = (now, 1)
}
return true
}
private func tick() {
let now = Date()
if Int(now.timeIntervalSince1970) % 5 == 0 { broadcast(.keepalive, excluding: nil) }
if Int(now.timeIntervalSince1970) % 10 == 0 { publishLocalState() }
router.expire(now: now)
pingWindows = pingWindows.filter { now.timeIntervalSince($0.value.0) < 60 }
let expiredPings = pendingPings.filter { now.timeIntervalSince($0.value.1) > 10 }
for (id, pending) in expiredPings {
var response = IPCMessage(operation: .ping, requestID: id); response.ok = false; response.message = "ping timed out"
pending.0.send(response); pendingPings.removeValue(forKey: id)
}
for state in Array(streams.values) {
if now.timeIntervalSince(state.lastProgress) > 60 { reset(state, reason: "stream timed out"); continue }
for (sequence, pending) in Array(state.pending) {
let delay = min(5.0, 0.5 * pow(2.0, Double(min(pending.attempts, 4))))
if now.timeIntervalSince(pending.lastSent) >= delay {
_ = send(pending.frame, to: state.remote)
state.pending[sequence] = .init(frame: pending.frame, lastSent: now, attempts: pending.attempts + 1)
}
}
}
}
private func log(_ message: String) {
let line = "[\(ISO8601DateFormatter().string(from: Date()))] \(message)\n"
FileHandle.standardError.write(Data(line.utf8))
}
}
+67
View File
@@ -0,0 +1,67 @@
import Foundation
import Network
import UltraMeshCore
final class PeerSession {
let connection: NWConnection
let outbound: Bool
var remote: NodeRecord?
var onMessage: ((PeerSession, WireMessage) -> Void)?
var onStop: ((PeerSession) -> Void)?
private let queue: DispatchQueue
private var stopped = false
init(connection: NWConnection, outbound: Bool, queue: DispatchQueue) {
self.connection = connection; self.outbound = outbound; self.queue = queue
}
func start(local: NodeRecord) {
connection.stateUpdateHandler = { [weak self] state in
guard let self else { return }
switch state {
case .ready:
self.send(.hello(local)); self.receiveMessage()
case .failed, .cancelled: self.stop()
default: break
}
}
connection.start(queue: queue)
}
func send(_ message: WireMessage) {
guard !stopped, let data = try? FrameCodec.encode(WireEnvelope(message: message)) else { return }
connection.send(content: data, completion: .contentProcessed { [weak self] error in
if error != nil { self?.stop() }
})
}
func stop() {
guard !stopped else { return }; stopped = true
connection.cancel(); onStop?(self)
}
private func receiveMessage() {
receiveExactly(4, accumulated: Data()) { [weak self] prefix in
guard let self, let prefix,
let size = try? FrameCodec.bodyLength(from: prefix) else { self?.stop(); return }
self.receiveExactly(size, accumulated: Data()) { [weak self] body in
guard let self, let body,
let envelope = try? FrameCodec.decode(WireEnvelope.self, body: body),
envelope.version == WireEnvelope.currentVersion else { self?.stop(); return }
self.onMessage?(self, envelope.message)
self.receiveMessage()
}
}
}
private func receiveExactly(_ count: Int, accumulated: Data, completion: @escaping (Data?) -> Void) {
if accumulated.count == count { completion(accumulated); return }
connection.receive(minimumIncompleteLength: 1, maximumLength: count - accumulated.count) { [weak self] data, _, complete, error in
guard self != nil, error == nil, let data, !data.isEmpty else { completion(nil); return }
let joined = accumulated + data
if joined.count == count { completion(joined) }
else if complete { completion(nil) }
else { self?.receiveExactly(count, accumulated: joined, completion: completion) }
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import Foundation
import UltraMeshCore
do {
let arguments = Array(CommandLine.arguments.dropFirst())
var stateURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/UltraMesh")
var socketPath: String?
var index = 0
while index < arguments.count {
guard index + 1 < arguments.count else { throw UMNError.message("missing value for \(arguments[index])") }
if arguments[index] == "--state-dir" { stateURL = URL(fileURLWithPath: arguments[index + 1]) }
else if arguments[index] == "--socket" { socketPath = arguments[index + 1] }
else { throw UMNError.message("unknown option \(arguments[index])") }
index += 2
}
let daemon = try MeshDaemon(baseURL: stateURL, socketPath: socketPath)
try daemon.start()
signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN)
let source = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
let term = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
source.setEventHandler { daemon.stop(); exit(0) }
term.setEventHandler { daemon.stop(); exit(0) }
source.resume(); term.resume()
dispatchMain()
} catch {
FileHandle.standardError.write(Data("umnd: \(error.localizedDescription)\n".utf8))
exit(1)
}