a lot
This commit is contained in:
@@ -24,6 +24,7 @@ final class MeshDaemon {
|
||||
private let identity: NodeIdentity
|
||||
private let router: LinkStateRouter
|
||||
private let firewall: MeshFirewall
|
||||
private let nativeFirewall: NativePacketFirewall
|
||||
private let configURL: URL
|
||||
private var config: DaemonConfig
|
||||
private let ipc: IPCServer
|
||||
@@ -35,12 +36,19 @@ final class MeshDaemon {
|
||||
private var lsaSequence: UInt64 = 0
|
||||
private var seenPackets = Set<UUID>()
|
||||
private var seenOrder: [UUID] = []
|
||||
private var replayPackets = Set<UUID>()
|
||||
private var replayOrder: [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?
|
||||
private var nativeInterface: NativeInterfaceClient?
|
||||
private var synchronizedRoutes = Set<MeshAddress>()
|
||||
private var nextInterfaceAttempt = Date.distantPast
|
||||
private var dns: MeshDNSServer?
|
||||
private var dnsState = "stopped"
|
||||
|
||||
init(baseURL: URL = FileManager.default.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent("Library/Application Support/UltraMesh"), socketPath: String? = nil) throws {
|
||||
@@ -50,6 +58,7 @@ final class MeshDaemon {
|
||||
configURL = baseURL.appendingPathComponent("config.json")
|
||||
config = DaemonConfig.load(from: configURL)
|
||||
firewall = MeshFirewall(rules: config.firewall)
|
||||
nativeFirewall = NativePacketFirewall(rules: 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)
|
||||
@@ -63,6 +72,8 @@ final class MeshDaemon {
|
||||
try startNetwork()
|
||||
ipc.onMessage = { [weak self] client, message in self?.queue.async { self?.handleIPC(client, message) } }
|
||||
try ipc.start()
|
||||
startDNS()
|
||||
attemptInterface()
|
||||
publishLocalState()
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.schedule(deadline: .now() + 1, repeating: 1)
|
||||
@@ -74,6 +85,7 @@ final class MeshDaemon {
|
||||
func stop() {
|
||||
queue.sync {
|
||||
timer?.cancel(); browser?.cancel(); listener?.cancel()
|
||||
nativeInterface?.disconnect(); dns?.stop()
|
||||
allSessions.values.forEach { $0.stop() }; ipc.stop()
|
||||
}
|
||||
}
|
||||
@@ -185,7 +197,8 @@ final class MeshDaemon {
|
||||
@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)
|
||||
let packet = RoutedPacket(source: address, destination: remote.address, sealed: sealed,
|
||||
trafficClass: inner.kind == .ipv6Packet ? .nativeIPv6 : .overlay)
|
||||
guard router.routes()[remote.address] != nil || peers[remote.address] != nil else { return false }
|
||||
forward(packet); return true
|
||||
}
|
||||
@@ -214,6 +227,12 @@ final class MeshDaemon {
|
||||
case .streamData: receiveStreamData(inner)
|
||||
case .streamAck: receiveStreamAck(inner)
|
||||
case .streamClose, .streamReset: receiveStreamClose(inner)
|
||||
case .ipv6Packet:
|
||||
guard let replayID = inner.replayID, markReplay(replayID),
|
||||
let info = try? IPv6PacketParser.parse(inner.payload),
|
||||
info.source == packet.source, info.destination == address,
|
||||
nativeFirewall.allowInbound(info, packet: inner.payload) else { return }
|
||||
_ = nativeInterface?.inject(inner.payload)
|
||||
case .error: receiveStreamClose(inner)
|
||||
}
|
||||
}
|
||||
@@ -304,9 +323,23 @@ final class MeshDaemon {
|
||||
response.ok = ok; response.message = text; response.values = values; response.streamID = streamID
|
||||
client.send(response)
|
||||
}
|
||||
guard message.version == 2 else { reply(false, "incompatible CLI protocol version"); return }
|
||||
switch message.operation {
|
||||
case .status:
|
||||
reply(true, "umnd running as \(address); \(peers.count) direct peer(s), \(router.routes().count) route(s)")
|
||||
let state = nativeInterface?.status
|
||||
reply(true, "umnd running as \(address); \(peers.count) direct peer(s), \(router.routes().count) route(s); " +
|
||||
"interface \(state?.name ?? "unavailable") MTU 1280, helper \(state?.helperState ?? "disconnected"), " +
|
||||
"\(state?.routeCount ?? 0) route(s) synchronized, DNS \(dnsState)")
|
||||
case .interfaceStatus:
|
||||
let state = nativeInterface?.status
|
||||
let name = state?.name ?? "unavailable"
|
||||
var response = IPCMessage(operation: message.operation, requestID: message.requestID)
|
||||
response.ok = true; response.interfaceName = state?.name; response.interfaceAddress = address.description
|
||||
response.interfaceMTU = 1280; response.helperState = state?.helperState ?? "disconnected"
|
||||
response.synchronizedRouteCount = state?.routeCount ?? 0; response.dnsState = dnsState
|
||||
response.message = "interface: \(name)\naddress: \(address)\nMTU: 1280\nhelper: \(response.helperState!)\n" +
|
||||
"routes synchronized: \(response.synchronizedRouteCount!)\nDNS: \(dnsState)"
|
||||
client.send(response)
|
||||
case .address: reply(true, address.description)
|
||||
case .peers: reply(true, values: peers.keys.sorted().map(\.description))
|
||||
case .routes:
|
||||
@@ -323,14 +356,16 @@ final class MeshDaemon {
|
||||
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")" }
|
||||
let values = firewall.allRules().map { "allow \($0.protocolKind.rawValue) \($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 }
|
||||
guard let protocolKind = message.firewallProtocol, let port = message.port, let sourceText = message.source else {
|
||||
reply(false, "new firewall rules require overlay, tcp, or udp"); 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) }
|
||||
if message.operation == .firewallAllow { firewall.allow(protocol: protocolKind, port: port, source: source) }
|
||||
else { firewall.revoke(protocol: protocolKind, 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 }
|
||||
@@ -410,6 +445,51 @@ final class MeshDaemon {
|
||||
return true
|
||||
}
|
||||
|
||||
private func markReplay(_ id: UUID) -> Bool {
|
||||
guard !replayPackets.contains(id) else { return false }
|
||||
replayPackets.insert(id); replayOrder.append(id)
|
||||
if replayOrder.count > 4096 { replayPackets.remove(replayOrder.removeFirst()) }
|
||||
return true
|
||||
}
|
||||
|
||||
private func startDNS() {
|
||||
let server = MeshDNSServer { [weak self] in
|
||||
guard let self else { return [:] }
|
||||
return self.queue.sync { self.config.aliases }
|
||||
}
|
||||
do { try server.start(); dns = server; dnsState = "listening on 127.0.0.1:53535" }
|
||||
catch { dnsState = "unavailable (\(error.localizedDescription))"; log("DNS unavailable: \(error)") }
|
||||
}
|
||||
|
||||
private func attemptInterface() {
|
||||
guard nativeInterface == nil, Date() >= nextInterfaceAttempt else { return }
|
||||
nextInterfaceAttempt = Date().addingTimeInterval(5)
|
||||
let socket = ProcessInfo.processInfo.environment["UMN_HELPER_SOCKET"] ?? "/var/run/ultramesh-helper.sock"
|
||||
let client = NativeInterfaceClient(local: address, socketPath: socket, queue: queue)
|
||||
client.onPacket = { [weak self] packet in self?.receiveOutboundIPv6(packet) }
|
||||
client.onDisconnect = { [weak self] in self?.nativeInterface = nil; self?.synchronizedRoutes.removeAll() }
|
||||
do {
|
||||
try client.connect(); nativeInterface = client; synchronizeNativeRoutes(force: true)
|
||||
if nativeInterface != nil { log("native interface connected") }
|
||||
}
|
||||
catch { client.disconnect(); log("native interface unavailable: \(error.localizedDescription)") }
|
||||
}
|
||||
|
||||
private func synchronizeNativeRoutes(force: Bool = false) {
|
||||
guard let nativeInterface else { return }
|
||||
let desired = Set(router.routes().keys.filter { router.record(for: $0) != nil })
|
||||
guard force || desired != synchronizedRoutes else { return }
|
||||
do { try nativeInterface.setRoutes(desired); synchronizedRoutes = desired }
|
||||
catch { log("route synchronization failed: \(error.localizedDescription)"); nativeInterface.disconnect() }
|
||||
}
|
||||
|
||||
private func receiveOutboundIPv6(_ packet: Data) {
|
||||
guard let info = try? IPv6PacketParser.parse(packet), info.source == address,
|
||||
let remote = router.record(for: info.destination), nativeFirewall.allowOutbound(info) else { return }
|
||||
let frame = InnerFrame(kind: .ipv6Packet, payload: packet, sourceRecord: identity.record, replayID: UUID())
|
||||
_ = send(frame, to: remote)
|
||||
}
|
||||
|
||||
private func allowPing(from source: MeshAddress) -> Bool {
|
||||
let now = Date()
|
||||
if let current = pingWindows[source], now.timeIntervalSince(current.0) < 1 {
|
||||
@@ -427,6 +507,7 @@ final class MeshDaemon {
|
||||
if Int(now.timeIntervalSince1970) % 5 == 0 { broadcast(.keepalive, excluding: nil) }
|
||||
if Int(now.timeIntervalSince1970) % 10 == 0 { publishLocalState() }
|
||||
router.expire(now: now)
|
||||
attemptInterface(); synchronizeNativeRoutes()
|
||||
pingWindows = pingWindows.filter { now.timeIntervalSince($0.value.0) < 60 }
|
||||
let expiredPings = pendingPings.filter { now.timeIntervalSince($0.value.1) > 10 }
|
||||
for (id, pending) in expiredPings {
|
||||
|
||||
Reference in New Issue
Block a user