import Foundation import Darwin import UltraMeshCore import UMNSystem private struct HelperOptions { var uid: uid_t var socket = "/var/run/ultramesh-helper.sock" init() throws { let args = Array(CommandLine.arguments.dropFirst()) guard let uidIndex = args.firstIndex(of: "--uid"), uidIndex + 1 < args.count, let parsed = UInt32(args[uidIndex + 1]) else { throw UMNError.message("umn-helper requires --uid") } uid = parsed if let index = args.firstIndex(of: "--socket"), index + 1 < args.count { socket = args[index + 1] } } } private func runCommand(_ executable: String, _ arguments: [String]) throws { let process = Process(); process.executableURL = URL(fileURLWithPath: executable); process.arguments = arguments process.standardOutput = FileHandle.nullDevice; process.standardError = FileHandle.standardError try process.run(); process.waitUntilExit() guard process.terminationStatus == 0 else { throw UMNError.message("\(executable) failed with status \(process.terminationStatus)") } } private func readLine(_ fd: Int32, limit: Int = 32_768) throws -> String? { var bytes: [UInt8] = [] while bytes.count < limit { var byte: UInt8 = 0; let count = Darwin.read(fd, &byte, 1) if count == 0 { return bytes.isEmpty ? nil : String(decoding: bytes, as: UTF8.self) } if count < 0 { if errno == EINTR { continue }; throw POSIXError(.init(rawValue: errno) ?? .EIO) } if byte == 10 { return String(decoding: bytes, as: UTF8.self) } bytes.append(byte) } throw UMNError.invalidFrame } private func handle(_ client: Int32, allowedUID: uid_t) { var device: Int32 = -1; var interface = ""; var routes = Set() defer { for route in routes { try? runCommand("/sbin/route", ["-n", "delete", "-inet6", route.description]) } if !interface.isEmpty { try? runCommand("/sbin/ifconfig", [interface, "down"]) } if device >= 0 { Darwin.close(device) } UnixIPC.close(client) } do { var peerUID: uid_t = 0; var peerGID: gid_t = 0 guard umn_get_peer_eid(client, &peerUID, &peerGID) == 0, peerUID == allowedUID else { throw UMNError.message("helper authentication failed") } guard let hello = try readLine(client), hello.hasPrefix("UMN2 "), let local = try? MeshAddress(String(hello.dropFirst(5))) else { throw UMNError.invalidFrame } var name = [CChar](repeating: 0, count: Int(IFNAMSIZ)) device = umn_utun_open(&name, name.count) guard device >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } interface = String(cString: name) try runCommand("/sbin/ifconfig", [interface, "inet6", local.description, "prefixlen", "128", "mtu", "1280", "up"]) let ready = Data("READY \(interface) 1280\n".utf8) let sent = ready.withUnsafeBytes { umn_send_fd(client, device, $0.baseAddress, ready.count) } guard sent == 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } while let line = try readLine(client) { guard line.hasPrefix("ROUTES") else { throw UMNError.invalidFrame } let fields = line.split(separator: " ").dropFirst() var desired = Set() for field in fields { guard let address = try? MeshAddress(String(field)) else { throw UMNError.invalidFrame } desired.insert(address) } try HelperRouteSet.validate(desired, local: local) let diff = HelperRouteSet.diff(current: routes, desired: desired) for address in diff.removals { try runCommand("/sbin/route", ["-n", "delete", "-inet6", address.description]); routes.remove(address) } for address in diff.additions { try runCommand("/sbin/route", ["-n", "add", "-inet6", address.description, "-interface", interface]); routes.insert(address) } } } catch { FileHandle.standardError.write(Data("umn-helper: \(error.localizedDescription)\n".utf8)) } } do { guard geteuid() == 0 else { throw UMNError.message("umn-helper must run as root") } let options = try HelperOptions() let server = try UnixIPC.listen(path: options.socket) guard chown(options.socket, options.uid, gid_t.max) == 0, chmod(options.socket, 0o600) == 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } signal(SIGPIPE, SIG_IGN) while true { let client = try UnixIPC.accept(server); handle(client, allowedUID: options.uid) } } catch { FileHandle.standardError.write(Data("umn-helper: \(error.localizedDescription)\n".utf8)); exit(1) }