// // LeafAdapter.swift // Runner // // Created by Jerry Bool on 2022/12/7. // import Foundation import NetworkExtension //import LeafFFI import leaf public enum LeafAdapterError: Error { /// Failure to locate tunnel file descriptor. case cannotLocateTunnelFileDescriptor /// Failure to perform an operation in such state. case invalidState /// Failure to set network settings. case setNetworkSettings(Error) /// Failure to set tunnel configuration case setTunnelConfiguration(Int32) /// Failure to start Leaf FFI. case startLeafFFI } /// Enum representing internal state of the `LeafAdapter` private enum State { /// The tunnel is stopped case stopped /// The tunnel is up and running case started /// The tunnel is temporarily shutdown due to device going offline case temporaryShutdown } private extension Network.NWPath.Status { /// Returns `true` if the path is potentially satisfiable. var isSatisfiable: Bool { switch self { case .requiresConnection, .satisfied: return true case .unsatisfied: return false @unknown default: return true } } } public class LeafAdapater { private static var sharedLeafAdapater: LeafAdapater = { return LeafAdapater() }() public class func shared() -> LeafAdapater { return sharedLeafAdapater } /// Leaf instance id public static let leafId: UInt16 = 666 /// Network routes monitor. private var networkMonitor: NWPathMonitor? /// Packet tunnel provider. private static weak var packetTunnelProvider: NEPacketTunnelProvider? /// Private queue used to synchronize access to `LeafAdapter` members. private let workQueue = DispatchQueue(label: "LeafAdapterWorkQueue") /// Adapter state. private var state: State = .stopped var tunnelFd: Int32? { var buf = [CChar](repeating: 0, count: Int(IFNAMSIZ)) for fd: Int32 in 0 ... 1024 { var len = socklen_t(buf.count) if getsockopt(fd, 2 /* IGMP */, 2, &buf, &len) == 0 && String(cString: buf).hasPrefix("utun") { return fd } } return LeafAdapater.packetTunnelProvider?.packetFlow.value(forKey: "socket.fileDescriptor") as? Int32 } /// Set PacketTunnelProvider instance /// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored /// as a weak public static func setPacketTunnelProvider(with packetTunnelProvider: NEPacketTunnelProvider) { LeafAdapater.packetTunnelProvider = packetTunnelProvider } /// Designated initializer. public init() { } deinit { // Cancel network monitor networkMonitor?.cancel() // Shutdown the tunnel if case .started = self.state { leaf_shutdown(LeafAdapater.leafId) } } public func setRuntimeConfiguration(conf: String?, completionHandler: @escaping (LeafAdapterError?) -> Void) { guard let conf = conf else { completionHandler(.startLeafFFI) return } let file = FileManager.default.leafConfFile try! conf.write(to: file!, atomically: true, encoding: .utf8) setenv("LOG_NO_COLOR", "true", 1) let result = leaf_test_config(file?.path) guard result == 0 else { completionHandler(.setTunnelConfiguration(result)) return } completionHandler(nil) } /// Returns a runtime configuration. /// - Parameter completionHandler: completion handler. public func getRuntimeConfiguration(completionHandler: @escaping (String?) -> Void) { workQueue.async { let fm = FileManager.default if let conf = fm.leafConfFile?.contents { completionHandler(conf) } else { completionHandler(nil) } } } /// Start the tunnel tunnel. /// - Parameters: /// - tunnelConfiguration: tunnel configuration. /// - completionHandler: completion handler. public func start(completionHandler: @escaping (LeafAdapterError?) -> Void) { workQueue.async { guard case .stopped = self.state else { completionHandler(.invalidState) return } let networkMonitor = NWPathMonitor() networkMonitor.pathUpdateHandler = { [weak self] path in self?.didReceivePathUpdate(path: path) } networkMonitor.start(queue: self.workQueue) let tunFd = self.tunnelFd != nil ? String(self.tunnelFd!) : nil // Reset log file. FileManager.default.leafLogFile?.truncate() let fm = FileManager.default let file = fm.leafConfFile var conf = file?.contents ?? "" conf = conf .replacingOccurrences(of: "{{leafLogFile}}", with: fm.leafLogFile?.path ?? "") .replacingOccurrences(of: "{{tunFd}}", with: tunFd ?? "") try! conf.write(to: file!, atomically: true, encoding: .utf8) setenv("LOG_NO_COLOR", "true", 1) leaf_run(LeafAdapater.leafId, file?.path) self.state = .started self.networkMonitor = networkMonitor completionHandler(nil) } } /// Stop the tunnel. /// - Parameter completionHandler: completion handler. public func stop(completionHandler: @escaping (LeafAdapterError?) -> Void) { workQueue.async { switch self.state { case .started: leaf_shutdown(LeafAdapater.leafId) case .temporaryShutdown: break case .stopped: completionHandler(.invalidState) return } self.networkMonitor?.cancel() self.networkMonitor = nil self.state = .stopped completionHandler(nil) } } /// Update runtime configuration. /// - Parameters: /// - tunnelConfiguration: tunnel configuration. /// - completionHandler: completion handler. public func update(conf: String?, completionHandler: @escaping (LeafAdapterError?) -> Void) { workQueue.async { if case .stopped = self.state { completionHandler(.invalidState) return } // Tell the system that the tunnel is going to reconnect using new configuration. // This will broadcast the `NEVPNStatusDidChange` notification to the GUI process. LeafAdapater.packetTunnelProvider?.reasserting = true defer { LeafAdapater.packetTunnelProvider?.reasserting = false } switch self.state { case .started: self.setRuntimeConfiguration(conf: conf, completionHandler: completionHandler) leaf_reload(LeafAdapater.leafId) self.state = .started case .temporaryShutdown: self.state = .temporaryShutdown case .stopped: fatalError() } completionHandler(nil) } } /// Helper method used by network path monitor. /// - Parameter path: new network path private func didReceivePathUpdate(path: Network.NWPath) { switch self.state { case .started: if path.status.isSatisfiable { } else { self.state = .temporaryShutdown leaf_shutdown(LeafAdapater.leafId) } case .temporaryShutdown: guard path.status.isSatisfiable else { return } self.state = .started case .stopped: // no-op break } } }