import Foundation import Darwin import UltraMeshCore import UMNSystem struct NativeInterfaceStatus { var name: String? var address: MeshAddress var mtu = 1280 var helperState: String var routeCount = 0 } final class NativeInterfaceClient { private let local: MeshAddress private let socketPath: String private let callbackQueue: DispatchQueue private var helperFD: Int32 = -1 private var deviceFD: Int32 = -1 private var readSource: DispatchSourceRead? private var helperSource: DispatchSourceRead? private let writeLock = NSLock() private(set) var status: NativeInterfaceStatus var onPacket: ((Data) -> Void)? var onDisconnect: (() -> Void)? init(local: MeshAddress, socketPath: String = "/var/run/ultramesh-helper.sock", queue: DispatchQueue) { self.local = local; self.socketPath = socketPath; callbackQueue = queue status = .init(address: local, helperState: "disconnected") } func connect() throws { guard helperFD < 0 else { return } let socket = try UnixIPC.connect(path: socketPath) do { try writeAll(Data("UMN2 \(local)\n".utf8), fd: socket) var receivedFD: Int32 = -1; var buffer = [UInt8](repeating: 0, count: 256) let count = umn_recv_fd(socket, &receivedFD, &buffer, buffer.count) guard count > 0, receivedFD >= 0 else { throw UMNError.invalidFrame } var adopted = false defer { if !adopted { Darwin.close(receivedFD) } } var readyData = Data(buffer.prefix(count)) while !readyData.contains(10), readyData.count < buffer.count { var byte: UInt8 = 0 let amount = Darwin.read(socket, &byte, 1) guard amount == 1 else { throw UMNError.invalidFrame } readyData.append(byte) } guard readyData.last == 10, let ready = String(data: readyData, encoding: .utf8) else { throw UMNError.invalidFrame } let fields = ready.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: " ") guard fields.count == 3, fields[0] == "READY", fields[1].hasPrefix("utun"), fields[2] == "1280" else { throw UMNError.invalidFrame } helperFD = socket; deviceFD = receivedFD adopted = true status.name = String(fields[1]); status.helperState = "connected" let source = DispatchSource.makeReadSource(fileDescriptor: receivedFD, queue: callbackQueue) source.setEventHandler { [weak self] in self?.readPacket() } source.setCancelHandler { Darwin.close(receivedFD) } source.resume(); readSource = source let helperMonitor = DispatchSource.makeReadSource(fileDescriptor: socket, queue: callbackQueue) helperMonitor.setEventHandler { [weak self] in self?.helperBecameReadable() } helperMonitor.resume(); helperSource = helperMonitor } catch { UnixIPC.close(socket); throw error } } func setRoutes(_ addresses: Set) throws { guard helperFD >= 0 else { throw UMNError.message("helper disconnected") } let line = "ROUTES" + addresses.sorted().map { " \($0)" }.joined() + "\n" do { try writeAll(Data(line.utf8), fd: helperFD); status.routeCount = addresses.count } catch { disconnect(); throw error } } func inject(_ packet: Data) -> Bool { guard deviceFD >= 0 else { return false } var family = UInt32(AF_INET6).bigEndian let framed = withUnsafeBytes(of: &family) { Data($0) } + packet writeLock.lock(); defer { writeLock.unlock() } return (try? writeAll(framed, fd: deviceFD)) != nil } func disconnect() { let wasConnected = helperFD >= 0 readSource?.cancel(); readSource = nil; deviceFD = -1 helperSource?.cancel(); helperSource = nil if helperFD >= 0 { UnixIPC.close(helperFD); helperFD = -1 } status.name = nil; status.helperState = "disconnected"; status.routeCount = 0 if wasConnected { onDisconnect?() } } private func readPacket() { guard deviceFD >= 0 else { return } var buffer = [UInt8](repeating: 0, count: IPv6PacketParser.maximumPacketSize + 4) let count = Darwin.read(deviceFD, &buffer, buffer.count) guard count > 4 else { if count <= 0 { disconnect() }; return } let family = buffer.prefix(4).reduce(UInt32(0)) { ($0 << 8) | UInt32($1) } guard family == UInt32(AF_INET6) else { return } onPacket?(Data(buffer[4..= 0 else { return } var byte: UInt8 = 0 _ = recv(helperFD, &byte, 1, MSG_PEEK) // The protocol has no unsolicited helper messages, so data or EOF means // the session is no longer trustworthy. disconnect() } private func writeAll(_ 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 count = Darwin.write(fd, base.advanced(by: offset), data.count - offset) if count < 0 { if errno == EINTR { continue }; throw POSIXError(.init(rawValue: errno) ?? .EIO) } offset += count } } } }