Add support for MAC and hostname rule items
This commit is contained in:
@@ -175,7 +175,7 @@ public struct CoreView: View {
|
||||
self.helperUnavailable = helperUnavailable
|
||||
#endif
|
||||
self.dataSize = dataSize
|
||||
self.dataSizeLoaded = true
|
||||
dataSizeLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import Network
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "RootHelper")
|
||||
|
||||
private class NeighborGoListener: NSObject, LibboxNeighborUpdateListenerProtocol {
|
||||
private weak var service: RootHelperService?
|
||||
|
||||
init(service: RootHelperService) {
|
||||
self.service = service
|
||||
}
|
||||
|
||||
func updateNeighborTable(_ entries: (any LibboxNeighborEntryIteratorProtocol)?) {
|
||||
guard let entries, let service else { return }
|
||||
service.pushNeighborTable(entries: entries)
|
||||
}
|
||||
}
|
||||
|
||||
class RootHelperService: NSObject {
|
||||
private var listener: NSXPCListener?
|
||||
private var neighborSubscription: LibboxNeighborSubscription?
|
||||
private var neighborCallbackConnection: NSXPCConnection?
|
||||
private var neighborLeaseWatcher: DispatchSourceFileSystemObject?
|
||||
private var pathMonitor: NWPathMonitor?
|
||||
private var pendingNATFlush: DispatchWorkItem?
|
||||
private var tunInterfaceName: String?
|
||||
|
||||
func start() {
|
||||
listener = NSXPCListener(machServiceName: AppConfiguration.rootHelperMachService)
|
||||
@@ -86,4 +107,249 @@ extension RootHelperService: RootHelperProtocol {
|
||||
func getVersion(reply: @escaping (String) -> Void) {
|
||||
reply(Bundle.main.version)
|
||||
}
|
||||
|
||||
func startNeighborMonitor(callbackEndpoint: NSXPCListenerEndpoint, reply: @escaping (NSError?) -> Void) {
|
||||
logger.info("startNeighborMonitor")
|
||||
closeNeighborMonitorInternal()
|
||||
|
||||
let callbackConnection = NSXPCConnection(listenerEndpoint: callbackEndpoint)
|
||||
let listenerInterface = NSXPCInterface(with: NeighborTableListenerProtocol.self)
|
||||
RootHelperXPC.configureListenerInterface(listenerInterface)
|
||||
callbackConnection.remoteObjectInterface = listenerInterface
|
||||
callbackConnection.resume()
|
||||
neighborCallbackConnection = callbackConnection
|
||||
|
||||
let goListener = NeighborGoListener(service: self)
|
||||
var error: NSError?
|
||||
let subscription = LibboxSubscribeNeighborTable(goListener, &error)
|
||||
if let error {
|
||||
logger.error("startNeighborMonitor: \(error.localizedDescription)")
|
||||
callbackConnection.invalidate()
|
||||
neighborCallbackConnection = nil
|
||||
reply(error)
|
||||
return
|
||||
}
|
||||
neighborSubscription = subscription
|
||||
startLeaseFileWatcher()
|
||||
startNATCleaner()
|
||||
reply(nil)
|
||||
}
|
||||
|
||||
func registerMyInterface(name: String, reply: @escaping (NSError?) -> Void) {
|
||||
logger.info("registerMyInterface: \(name)")
|
||||
tunInterfaceName = name
|
||||
flushInternetSharingNAT()
|
||||
reply(nil)
|
||||
}
|
||||
|
||||
func closeNeighborMonitor(reply: @escaping (NSError?) -> Void) {
|
||||
logger.info("closeNeighborMonitor")
|
||||
closeNeighborMonitorInternal()
|
||||
reply(nil)
|
||||
}
|
||||
|
||||
private func closeNeighborMonitorInternal() {
|
||||
neighborSubscription?.close()
|
||||
neighborSubscription = nil
|
||||
neighborLeaseWatcher?.cancel()
|
||||
neighborLeaseWatcher = nil
|
||||
pendingNATFlush?.cancel()
|
||||
pendingNATFlush = nil
|
||||
pathMonitor?.cancel()
|
||||
pathMonitor = nil
|
||||
tunInterfaceName = nil
|
||||
neighborCallbackConnection?.invalidate()
|
||||
neighborCallbackConnection = nil
|
||||
}
|
||||
|
||||
func pushNeighborTable(entries: LibboxNeighborEntryIteratorProtocol) {
|
||||
guard let callbackConnection = neighborCallbackConnection else {
|
||||
logger.warning("pushNeighborTable: no callback connection")
|
||||
return
|
||||
}
|
||||
guard let proxy = callbackConnection.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("pushNeighborTable XPC error: \(error.localizedDescription)")
|
||||
}) as? NeighborTableListenerProtocol else {
|
||||
logger.warning("pushNeighborTable: failed to get proxy")
|
||||
return
|
||||
}
|
||||
|
||||
let leaseIterator = LibboxReadBootpdLeases()
|
||||
var leaseEntries: [NeighborEntryResult] = []
|
||||
var leaseHostnamesByMAC: [String: String] = [:]
|
||||
var leaseHostnamesByIP: [String: String] = [:]
|
||||
if let leaseIterator {
|
||||
while leaseIterator.hasNext() {
|
||||
guard let entry = leaseIterator.next() else { continue }
|
||||
leaseEntries.append(NeighborEntryResult(
|
||||
address: entry.address,
|
||||
macAddress: entry.macAddress,
|
||||
hostname: entry.hostname
|
||||
))
|
||||
if !entry.hostname.isEmpty {
|
||||
leaseHostnamesByMAC[entry.macAddress] = entry.hostname
|
||||
leaseHostnamesByIP[entry.address] = entry.hostname
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("pushNeighborTable: leases=\(leaseEntries.count), hostnames=\(leaseHostnamesByMAC.count)")
|
||||
|
||||
var results: [NeighborEntryResult] = []
|
||||
var seenAddresses: Set<String> = []
|
||||
while entries.hasNext() {
|
||||
guard let entry = entries.next() else { continue }
|
||||
seenAddresses.insert(entry.address)
|
||||
var hostname = entry.hostname
|
||||
if hostname.isEmpty {
|
||||
hostname = leaseHostnamesByIP[entry.address] ?? leaseHostnamesByMAC[entry.macAddress] ?? ""
|
||||
}
|
||||
results.append(NeighborEntryResult(
|
||||
address: entry.address,
|
||||
macAddress: entry.macAddress,
|
||||
hostname: hostname
|
||||
))
|
||||
}
|
||||
for leaseEntry in leaseEntries {
|
||||
if !seenAddresses.contains(leaseEntry.address) {
|
||||
results.append(leaseEntry)
|
||||
}
|
||||
}
|
||||
logger.debug("pushNeighborTable: \(results.count) entries")
|
||||
proxy.updateNeighborTable(entries: results as NSArray)
|
||||
}
|
||||
|
||||
private func startNATCleaner() {
|
||||
flushInternetSharingNAT()
|
||||
let monitor = NWPathMonitor()
|
||||
let queue = DispatchQueue(label: "nat-cleaner")
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
guard let self else { return }
|
||||
logger.debug("NATCleaner: path update, status=\(String(describing: path.status)), interfaces=\(path.availableInterfaces.map(\.name))")
|
||||
self.pendingNATFlush?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.flushInternetSharingNAT()
|
||||
}
|
||||
self.pendingNATFlush = workItem
|
||||
queue.asyncAfter(deadline: .now() + 2, execute: workItem)
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
pathMonitor = monitor
|
||||
}
|
||||
|
||||
private func flushInternetSharingNAT() {
|
||||
guard let tunName = tunInterfaceName, !tunName.isEmpty else {
|
||||
logger.debug("flushInternetSharingNAT: no tun interface name set")
|
||||
return
|
||||
}
|
||||
let anchors = [
|
||||
"com.apple.internet-sharing/shared_v4",
|
||||
"com.apple.internet-sharing/shared_v6",
|
||||
]
|
||||
let filter = " on \(tunName) "
|
||||
for anchor in anchors {
|
||||
removeNATRulesForInterface(anchor: anchor, filter: filter)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNATRulesForInterface(anchor: String, filter: String) {
|
||||
let readProcess = Process()
|
||||
readProcess.executableURL = URL(fileURLWithPath: "/sbin/pfctl")
|
||||
readProcess.arguments = ["-a", anchor, "-s", "nat"]
|
||||
let readPipe = Pipe()
|
||||
readProcess.standardOutput = readPipe
|
||||
readProcess.standardError = FileHandle.nullDevice
|
||||
do {
|
||||
try readProcess.run()
|
||||
} catch {
|
||||
logger.error("removeNATRules: failed to read \(anchor): \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
let output = String(data: readPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||
readProcess.waitUntilExit()
|
||||
if readProcess.terminationStatus != 0 {
|
||||
logger.warning("removeNATRules: pfctl -s nat exited with \(readProcess.terminationStatus) for \(anchor)")
|
||||
return
|
||||
}
|
||||
if output.isEmpty {
|
||||
logger.debug("removeNATRules: \(anchor) has no NAT rules")
|
||||
return
|
||||
}
|
||||
guard output.contains(filter) else {
|
||||
logger.debug("removeNATRules: \(anchor) has no rules matching \(filter)")
|
||||
return
|
||||
}
|
||||
let lines = output.components(separatedBy: "\n")
|
||||
let removed = lines.filter { $0.contains(filter) }
|
||||
let remaining = lines.filter { !$0.contains(filter) }.joined(separator: "\n")
|
||||
logger.info("removeNATRules: \(anchor): removing \(removed.count) rules matching \(filter), keeping \(lines.count - removed.count) rules")
|
||||
for rule in removed {
|
||||
logger.debug("removeNATRules: removing: \(rule)")
|
||||
}
|
||||
let writeProcess = Process()
|
||||
writeProcess.executableURL = URL(fileURLWithPath: "/sbin/pfctl")
|
||||
writeProcess.arguments = ["-a", anchor, "-N", "-f", "-"]
|
||||
let writePipe = Pipe()
|
||||
writePipe.fileHandleForWriting.write(remaining.data(using: .utf8) ?? Data())
|
||||
writePipe.fileHandleForWriting.closeFile()
|
||||
writeProcess.standardInput = writePipe
|
||||
writeProcess.standardOutput = FileHandle.nullDevice
|
||||
let writeErrorPipe = Pipe()
|
||||
writeProcess.standardError = writeErrorPipe
|
||||
do {
|
||||
try writeProcess.run()
|
||||
} catch {
|
||||
logger.error("removeNATRules: failed to write \(anchor): \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
let stderrData = writeErrorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
writeProcess.waitUntilExit()
|
||||
let stderrOutput = String(data: stderrData, encoding: .utf8) ?? ""
|
||||
if writeProcess.terminationStatus != 0 {
|
||||
logger.error("removeNATRules: pfctl -f exited with \(writeProcess.terminationStatus) for \(anchor), stderr: \(stderrOutput)")
|
||||
} else {
|
||||
logger.debug("removeNATRules: successfully updated \(anchor)")
|
||||
}
|
||||
}
|
||||
|
||||
private func startLeaseFileWatcher() {
|
||||
let leasePath = "/var/db/dhcpd_leases"
|
||||
let fd = open(leasePath, O_EVTONLY)
|
||||
guard fd >= 0 else {
|
||||
logger.warning("startLeaseFileWatcher: failed to open \(leasePath), errno=\(errno)")
|
||||
return
|
||||
}
|
||||
let source = DispatchSource.makeFileSystemObjectSource(
|
||||
fileDescriptor: fd,
|
||||
eventMask: [.write, .rename],
|
||||
queue: DispatchQueue.global()
|
||||
)
|
||||
source.setEventHandler { [weak self] in
|
||||
guard let self, neighborSubscription != nil else { return }
|
||||
guard let callbackConnection = neighborCallbackConnection else { return }
|
||||
guard let proxy = callbackConnection.remoteObjectProxyWithErrorHandler({ error in
|
||||
logger.error("leaseWatcher push error: \(error.localizedDescription)")
|
||||
}) as? NeighborTableListenerProtocol else {
|
||||
return
|
||||
}
|
||||
|
||||
let leaseIterator = LibboxReadBootpdLeases()
|
||||
var results: [NeighborEntryResult] = []
|
||||
if let leaseIterator {
|
||||
while leaseIterator.hasNext() {
|
||||
guard let entry = leaseIterator.next() else { continue }
|
||||
results.append(NeighborEntryResult(
|
||||
address: entry.address,
|
||||
macAddress: entry.macAddress,
|
||||
hostname: entry.hostname
|
||||
))
|
||||
}
|
||||
}
|
||||
proxy.updateNeighborTable(entries: results as NSArray)
|
||||
}
|
||||
source.setCancelHandler {
|
||||
close(fd)
|
||||
}
|
||||
source.resume()
|
||||
neighborLeaseWatcher = source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +460,11 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
networkSettings = nil
|
||||
nwMonitor?.cancel()
|
||||
nwMonitor = nil
|
||||
#if os(macOS)
|
||||
neighborCallbackListener?.invalidate()
|
||||
neighborCallbackListener = nil
|
||||
neighborCallbackHandler = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
public func send(_ notification: LibboxNotification?) throws {
|
||||
@@ -492,6 +497,50 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var neighborCallbackListener: NSXPCListener?
|
||||
private var neighborCallbackHandler: NeighborCallbackHandler?
|
||||
#endif
|
||||
|
||||
public func startNeighborMonitor(_ listener: LibboxNeighborUpdateListenerProtocol?) throws {
|
||||
#if os(macOS)
|
||||
guard let listener else { return }
|
||||
if Variant.useSystemExtension {
|
||||
let handler = NeighborCallbackHandler(listener)
|
||||
let xpcListener = NSXPCListener.anonymous()
|
||||
xpcListener.delegate = handler
|
||||
xpcListener.resume()
|
||||
try RootHelperClient.shared.startNeighborMonitor(
|
||||
callbackEndpoint: xpcListener.endpoint
|
||||
)
|
||||
neighborCallbackListener = xpcListener
|
||||
neighborCallbackHandler = handler
|
||||
return
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func registerMyInterface(_ name: String?) {
|
||||
#if os(macOS)
|
||||
guard let name, !name.isEmpty else { return }
|
||||
if Variant.useSystemExtension {
|
||||
try? RootHelperClient.shared.registerMyInterface(name: name)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func closeNeighborMonitor(_: LibboxNeighborUpdateListenerProtocol?) throws {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try? RootHelperClient.shared.closeNeighborMonitor()
|
||||
neighborCallbackListener?.invalidate()
|
||||
neighborCallbackListener = nil
|
||||
neighborCallbackHandler = nil
|
||||
return
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func localDNSTransport() -> (any LibboxLocalDNSTransportProtocol)? {
|
||||
nil
|
||||
}
|
||||
@@ -500,3 +549,51 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private class NeighborCallbackHandler: NSObject, NSXPCListenerDelegate, NeighborTableListenerProtocol {
|
||||
private let listener: LibboxNeighborUpdateListenerProtocol
|
||||
|
||||
init(_ listener: LibboxNeighborUpdateListenerProtocol) {
|
||||
self.listener = listener
|
||||
}
|
||||
|
||||
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
let exportedInterface = NSXPCInterface(with: NeighborTableListenerProtocol.self)
|
||||
RootHelperXPC.configureListenerInterface(exportedInterface)
|
||||
newConnection.exportedInterface = exportedInterface
|
||||
newConnection.exportedObject = self
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
|
||||
func updateNeighborTable(entries: NSArray) {
|
||||
let iterator = NeighborEntryArrayIterator(entries)
|
||||
listener.updateNeighborTable(iterator)
|
||||
}
|
||||
}
|
||||
|
||||
private class NeighborEntryArrayIterator: NSObject, LibboxNeighborEntryIteratorProtocol {
|
||||
private var entries: [NeighborEntryResult]
|
||||
private var index = 0
|
||||
|
||||
init(_ array: NSArray) {
|
||||
entries = array.compactMap { $0 as? NeighborEntryResult }
|
||||
}
|
||||
|
||||
func hasNext() -> Bool {
|
||||
index < entries.count
|
||||
}
|
||||
|
||||
func next() -> LibboxNeighborEntry? {
|
||||
guard index < entries.count else { return nil }
|
||||
let result = entries[index]
|
||||
index += 1
|
||||
let entry = LibboxNeighborEntry()
|
||||
entry.address = result.address
|
||||
entry.macAddress = result.macAddress
|
||||
entry.hostname = result.hostname
|
||||
return entry
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,36 @@
|
||||
|
||||
private let logger = Logger(category: "RootHelperXPC")
|
||||
|
||||
@objc(NeighborEntryResult) public class NeighborEntryResult: NSObject, NSSecureCoding {
|
||||
public static let supportsSecureCoding = true
|
||||
|
||||
@objc public var address: String
|
||||
@objc public var macAddress: String
|
||||
@objc public var hostname: String
|
||||
|
||||
public init(address: String, macAddress: String, hostname: String) {
|
||||
self.address = address
|
||||
self.macAddress = macAddress
|
||||
self.hostname = hostname
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
address = coder.decodeObject(of: NSString.self, forKey: "address") as? String ?? ""
|
||||
macAddress = coder.decodeObject(of: NSString.self, forKey: "macAddress") as? String ?? ""
|
||||
hostname = coder.decodeObject(of: NSString.self, forKey: "hostname") as? String ?? ""
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(address as NSString, forKey: "address")
|
||||
coder.encode(macAddress as NSString, forKey: "macAddress")
|
||||
coder.encode(hostname as NSString, forKey: "hostname")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public protocol NeighborTableListenerProtocol {
|
||||
func updateNeighborTable(entries: NSArray)
|
||||
}
|
||||
|
||||
@objc public class ConnectionOwnerResult: NSObject, NSSecureCoding {
|
||||
public static let supportsSecureCoding = true
|
||||
|
||||
@@ -43,6 +73,9 @@
|
||||
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void)
|
||||
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void)
|
||||
func getVersion(reply: @escaping (String) -> Void)
|
||||
func startNeighborMonitor(callbackEndpoint: NSXPCListenerEndpoint, reply: @escaping (NSError?) -> Void)
|
||||
func closeNeighborMonitor(reply: @escaping (NSError?) -> Void)
|
||||
func registerMyInterface(name: String, reply: @escaping (NSError?) -> Void)
|
||||
}
|
||||
|
||||
public enum RootHelperXPC {
|
||||
@@ -54,6 +87,23 @@
|
||||
argumentIndex: 0,
|
||||
ofReply: true
|
||||
)
|
||||
let endpointClasses = NSSet(array: [NSXPCListenerEndpoint.self]) as! Set<AnyHashable>
|
||||
interface.setClasses(
|
||||
endpointClasses,
|
||||
for: #selector(RootHelperProtocol.startNeighborMonitor(callbackEndpoint:reply:)),
|
||||
argumentIndex: 0,
|
||||
ofReply: false
|
||||
)
|
||||
}
|
||||
|
||||
public static func configureListenerInterface(_ interface: NSXPCInterface) {
|
||||
let entryClasses = NSSet(array: [NSArray.self, NeighborEntryResult.self]) as! Set<AnyHashable>
|
||||
interface.setClasses(
|
||||
entryClasses,
|
||||
for: #selector(NeighborTableListenerProtocol.updateNeighborTable(entries:)),
|
||||
argumentIndex: 0,
|
||||
ofReply: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +269,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
public func startNeighborMonitor(callbackEndpoint: NSXPCListenerEndpoint) throws {
|
||||
try performXPCCallVoid("startNeighborMonitor") { proxy, reply in
|
||||
proxy.startNeighborMonitor(callbackEndpoint: callbackEndpoint, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
public func closeNeighborMonitor() throws {
|
||||
try performXPCCallVoid("closeNeighborMonitor") { proxy, reply in
|
||||
proxy.closeNeighborMonitor(reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
public func registerMyInterface(name: String) throws {
|
||||
try performXPCCallVoid("registerMyInterface") { proxy, reply in
|
||||
proxy.registerMyInterface(name: name, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
public func getVersion() throws -> String {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: String?
|
||||
|
||||
Reference in New Issue
Block a user