Skip to content

Commit e77d75b

Browse files
committed
Add support for MAC and hostname rule items
1 parent efa6088 commit e77d75b

4 files changed

Lines changed: 432 additions & 1 deletion

File tree

ApplicationLibrary/Views/Setting/CoreView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public struct CoreView: View {
175175
self.helperUnavailable = helperUnavailable
176176
#endif
177177
self.dataSize = dataSize
178-
self.dataSizeLoaded = true
178+
dataSizeLoaded = true
179179
}
180180
}
181181

HelperService/RootHelperService.swift

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
import Foundation
2+
import Libbox
23
import Library
4+
import Network
35
import os
46

57
private let logger = Logger(category: "RootHelper")
68

9+
private class NeighborGoListener: NSObject, LibboxNeighborUpdateListenerProtocol {
10+
private weak var service: RootHelperService?
11+
12+
init(service: RootHelperService) {
13+
self.service = service
14+
}
15+
16+
func updateNeighborTable(_ entries: (any LibboxNeighborEntryIteratorProtocol)?) {
17+
guard let entries, let service else { return }
18+
service.pushNeighborTable(entries: entries)
19+
}
20+
}
21+
722
class RootHelperService: NSObject {
823
private var listener: NSXPCListener?
24+
private var neighborSubscription: LibboxNeighborSubscription?
25+
private var neighborCallbackConnection: NSXPCConnection?
26+
private var neighborLeaseWatcher: DispatchSourceFileSystemObject?
27+
private var pathMonitor: NWPathMonitor?
28+
private var pendingNATFlush: DispatchWorkItem?
29+
private var tunInterfaceName: String?
930

1031
func start() {
1132
listener = NSXPCListener(machServiceName: AppConfiguration.rootHelperMachService)
@@ -86,4 +107,249 @@ extension RootHelperService: RootHelperProtocol {
86107
func getVersion(reply: @escaping (String) -> Void) {
87108
reply(Bundle.main.version)
88109
}
110+
111+
func startNeighborMonitor(callbackEndpoint: NSXPCListenerEndpoint, reply: @escaping (NSError?) -> Void) {
112+
logger.info("startNeighborMonitor")
113+
closeNeighborMonitorInternal()
114+
115+
let callbackConnection = NSXPCConnection(listenerEndpoint: callbackEndpoint)
116+
let listenerInterface = NSXPCInterface(with: NeighborTableListenerProtocol.self)
117+
RootHelperXPC.configureListenerInterface(listenerInterface)
118+
callbackConnection.remoteObjectInterface = listenerInterface
119+
callbackConnection.resume()
120+
neighborCallbackConnection = callbackConnection
121+
122+
let goListener = NeighborGoListener(service: self)
123+
var error: NSError?
124+
let subscription = LibboxSubscribeNeighborTable(goListener, &error)
125+
if let error {
126+
logger.error("startNeighborMonitor: \(error.localizedDescription)")
127+
callbackConnection.invalidate()
128+
neighborCallbackConnection = nil
129+
reply(error)
130+
return
131+
}
132+
neighborSubscription = subscription
133+
startLeaseFileWatcher()
134+
startNATCleaner()
135+
reply(nil)
136+
}
137+
138+
func registerMyInterface(name: String, reply: @escaping (NSError?) -> Void) {
139+
logger.info("registerMyInterface: \(name)")
140+
tunInterfaceName = name
141+
flushInternetSharingNAT()
142+
reply(nil)
143+
}
144+
145+
func closeNeighborMonitor(reply: @escaping (NSError?) -> Void) {
146+
logger.info("closeNeighborMonitor")
147+
closeNeighborMonitorInternal()
148+
reply(nil)
149+
}
150+
151+
private func closeNeighborMonitorInternal() {
152+
neighborSubscription?.close()
153+
neighborSubscription = nil
154+
neighborLeaseWatcher?.cancel()
155+
neighborLeaseWatcher = nil
156+
pendingNATFlush?.cancel()
157+
pendingNATFlush = nil
158+
pathMonitor?.cancel()
159+
pathMonitor = nil
160+
tunInterfaceName = nil
161+
neighborCallbackConnection?.invalidate()
162+
neighborCallbackConnection = nil
163+
}
164+
165+
func pushNeighborTable(entries: LibboxNeighborEntryIteratorProtocol) {
166+
guard let callbackConnection = neighborCallbackConnection else {
167+
logger.warning("pushNeighborTable: no callback connection")
168+
return
169+
}
170+
guard let proxy = callbackConnection.remoteObjectProxyWithErrorHandler({ error in
171+
logger.error("pushNeighborTable XPC error: \(error.localizedDescription)")
172+
}) as? NeighborTableListenerProtocol else {
173+
logger.warning("pushNeighborTable: failed to get proxy")
174+
return
175+
}
176+
177+
let leaseIterator = LibboxReadBootpdLeases()
178+
var leaseEntries: [NeighborEntryResult] = []
179+
var leaseHostnamesByMAC: [String: String] = [:]
180+
var leaseHostnamesByIP: [String: String] = [:]
181+
if let leaseIterator {
182+
while leaseIterator.hasNext() {
183+
guard let entry = leaseIterator.next() else { continue }
184+
leaseEntries.append(NeighborEntryResult(
185+
address: entry.address,
186+
macAddress: entry.macAddress,
187+
hostname: entry.hostname
188+
))
189+
if !entry.hostname.isEmpty {
190+
leaseHostnamesByMAC[entry.macAddress] = entry.hostname
191+
leaseHostnamesByIP[entry.address] = entry.hostname
192+
}
193+
}
194+
}
195+
logger.debug("pushNeighborTable: leases=\(leaseEntries.count), hostnames=\(leaseHostnamesByMAC.count)")
196+
197+
var results: [NeighborEntryResult] = []
198+
var seenAddresses: Set<String> = []
199+
while entries.hasNext() {
200+
guard let entry = entries.next() else { continue }
201+
seenAddresses.insert(entry.address)
202+
var hostname = entry.hostname
203+
if hostname.isEmpty {
204+
hostname = leaseHostnamesByIP[entry.address] ?? leaseHostnamesByMAC[entry.macAddress] ?? ""
205+
}
206+
results.append(NeighborEntryResult(
207+
address: entry.address,
208+
macAddress: entry.macAddress,
209+
hostname: hostname
210+
))
211+
}
212+
for leaseEntry in leaseEntries {
213+
if !seenAddresses.contains(leaseEntry.address) {
214+
results.append(leaseEntry)
215+
}
216+
}
217+
logger.debug("pushNeighborTable: \(results.count) entries")
218+
proxy.updateNeighborTable(entries: results as NSArray)
219+
}
220+
221+
private func startNATCleaner() {
222+
flushInternetSharingNAT()
223+
let monitor = NWPathMonitor()
224+
let queue = DispatchQueue(label: "nat-cleaner")
225+
monitor.pathUpdateHandler = { [weak self] path in
226+
guard let self else { return }
227+
logger.debug("NATCleaner: path update, status=\(String(describing: path.status)), interfaces=\(path.availableInterfaces.map(\.name))")
228+
self.pendingNATFlush?.cancel()
229+
let workItem = DispatchWorkItem { [weak self] in
230+
self?.flushInternetSharingNAT()
231+
}
232+
self.pendingNATFlush = workItem
233+
queue.asyncAfter(deadline: .now() + 2, execute: workItem)
234+
}
235+
monitor.start(queue: queue)
236+
pathMonitor = monitor
237+
}
238+
239+
private func flushInternetSharingNAT() {
240+
guard let tunName = tunInterfaceName, !tunName.isEmpty else {
241+
logger.debug("flushInternetSharingNAT: no tun interface name set")
242+
return
243+
}
244+
let anchors = [
245+
"com.apple.internet-sharing/shared_v4",
246+
"com.apple.internet-sharing/shared_v6",
247+
]
248+
let filter = " on \(tunName) "
249+
for anchor in anchors {
250+
removeNATRulesForInterface(anchor: anchor, filter: filter)
251+
}
252+
}
253+
254+
private func removeNATRulesForInterface(anchor: String, filter: String) {
255+
let readProcess = Process()
256+
readProcess.executableURL = URL(fileURLWithPath: "/sbin/pfctl")
257+
readProcess.arguments = ["-a", anchor, "-s", "nat"]
258+
let readPipe = Pipe()
259+
readProcess.standardOutput = readPipe
260+
readProcess.standardError = FileHandle.nullDevice
261+
do {
262+
try readProcess.run()
263+
} catch {
264+
logger.error("removeNATRules: failed to read \(anchor): \(error.localizedDescription)")
265+
return
266+
}
267+
let output = String(data: readPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
268+
readProcess.waitUntilExit()
269+
if readProcess.terminationStatus != 0 {
270+
logger.warning("removeNATRules: pfctl -s nat exited with \(readProcess.terminationStatus) for \(anchor)")
271+
return
272+
}
273+
if output.isEmpty {
274+
logger.debug("removeNATRules: \(anchor) has no NAT rules")
275+
return
276+
}
277+
guard output.contains(filter) else {
278+
logger.debug("removeNATRules: \(anchor) has no rules matching \(filter)")
279+
return
280+
}
281+
let lines = output.components(separatedBy: "\n")
282+
let removed = lines.filter { $0.contains(filter) }
283+
let remaining = lines.filter { !$0.contains(filter) }.joined(separator: "\n")
284+
logger.info("removeNATRules: \(anchor): removing \(removed.count) rules matching \(filter), keeping \(lines.count - removed.count) rules")
285+
for rule in removed {
286+
logger.debug("removeNATRules: removing: \(rule)")
287+
}
288+
let writeProcess = Process()
289+
writeProcess.executableURL = URL(fileURLWithPath: "/sbin/pfctl")
290+
writeProcess.arguments = ["-a", anchor, "-N", "-f", "-"]
291+
let writePipe = Pipe()
292+
writePipe.fileHandleForWriting.write(remaining.data(using: .utf8) ?? Data())
293+
writePipe.fileHandleForWriting.closeFile()
294+
writeProcess.standardInput = writePipe
295+
writeProcess.standardOutput = FileHandle.nullDevice
296+
let writeErrorPipe = Pipe()
297+
writeProcess.standardError = writeErrorPipe
298+
do {
299+
try writeProcess.run()
300+
} catch {
301+
logger.error("removeNATRules: failed to write \(anchor): \(error.localizedDescription)")
302+
return
303+
}
304+
let stderrData = writeErrorPipe.fileHandleForReading.readDataToEndOfFile()
305+
writeProcess.waitUntilExit()
306+
let stderrOutput = String(data: stderrData, encoding: .utf8) ?? ""
307+
if writeProcess.terminationStatus != 0 {
308+
logger.error("removeNATRules: pfctl -f exited with \(writeProcess.terminationStatus) for \(anchor), stderr: \(stderrOutput)")
309+
} else {
310+
logger.debug("removeNATRules: successfully updated \(anchor)")
311+
}
312+
}
313+
314+
private func startLeaseFileWatcher() {
315+
let leasePath = "/var/db/dhcpd_leases"
316+
let fd = open(leasePath, O_EVTONLY)
317+
guard fd >= 0 else {
318+
logger.warning("startLeaseFileWatcher: failed to open \(leasePath), errno=\(errno)")
319+
return
320+
}
321+
let source = DispatchSource.makeFileSystemObjectSource(
322+
fileDescriptor: fd,
323+
eventMask: [.write, .rename],
324+
queue: DispatchQueue.global()
325+
)
326+
source.setEventHandler { [weak self] in
327+
guard let self, neighborSubscription != nil else { return }
328+
guard let callbackConnection = neighborCallbackConnection else { return }
329+
guard let proxy = callbackConnection.remoteObjectProxyWithErrorHandler({ error in
330+
logger.error("leaseWatcher push error: \(error.localizedDescription)")
331+
}) as? NeighborTableListenerProtocol else {
332+
return
333+
}
334+
335+
let leaseIterator = LibboxReadBootpdLeases()
336+
var results: [NeighborEntryResult] = []
337+
if let leaseIterator {
338+
while leaseIterator.hasNext() {
339+
guard let entry = leaseIterator.next() else { continue }
340+
results.append(NeighborEntryResult(
341+
address: entry.address,
342+
macAddress: entry.macAddress,
343+
hostname: entry.hostname
344+
))
345+
}
346+
}
347+
proxy.updateNeighborTable(entries: results as NSArray)
348+
}
349+
source.setCancelHandler {
350+
close(fd)
351+
}
352+
source.resume()
353+
neighborLeaseWatcher = source
354+
}
89355
}

0 commit comments

Comments
 (0)