122 lines
5.3 KiB
Swift
122 lines
5.3 KiB
Swift
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<MeshAddress>) 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..<count]))
|
|
}
|
|
|
|
private func helperBecameReadable() {
|
|
guard helperFD >= 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
|
|
}
|
|
}
|
|
}
|
|
}
|