diff --git a/apple/Sources/Connlib/Adapter.swift b/apple/Sources/Connlib/Adapter.swift index 4b8a36c..5f52632 100644 --- a/apple/Sources/Connlib/Adapter.swift +++ b/apple/Sources/Connlib/Adapter.swift @@ -31,14 +31,9 @@ private enum State { public class Adapter { private let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") - // Maintain a handle to the currently instantiated tunnel adapter 🤮 - public static var currentAdapter: Adapter? - - // Maintain a reference to the initialized callback handler - public static var callbackHandler: CallbackHandler? - - // Latest applied NETunnelProviderNetworkSettings - public var lastNetworkSettings: NEPacketTunnelNetworkSettings? + // Initialize callback handler after the adapter is initialized, + // just when the callback handler needs to be used + private lazy var callbackHandler = CallbackHandler(adapter: self) /// Packet tunnel provider. private weak var packetTunnelProvider: NEPacketTunnelProvider? @@ -51,19 +46,16 @@ public class Adapter { /// Adapter state. private var state: State = .stopped + private var interfaceAddresses: InterfaceAddresses? + private var resources: [Resource] = [] + + private var isTunnelStarted = false public init(with packetTunnelProvider: NEPacketTunnelProvider) { self.packetTunnelProvider = packetTunnelProvider - - // There must be a better way than making this a static class var... - Self.currentAdapter = self - Self.callbackHandler = CallbackHandler(adapter: self) } deinit { - // Remove static var reference - Self.currentAdapter = nil - // Cancel network monitor networkMonitor?.cancel() @@ -77,7 +69,7 @@ public class Adapter { /// Start the tunnel tunnel. /// - Parameters: /// - completionHandler: completion handler. - public func start(completionHandler: @escaping (AdapterError?) -> Void) throws { + public func start(portalURL: String, token: String, completionHandler: @escaping (AdapterError?) -> Void) throws { workQueue.async { guard case .stopped = self.state else { completionHandler(.invalidState) @@ -90,17 +82,20 @@ public class Adapter { } networkMonitor.start(queue: self.workQueue) - do { - try self.setNetworkSettings(self.generateNetworkSettings(ipv4Routes: [], ipv6Routes: [])) + self.callbackHandler.delegate = self + do { self.state = .started( WrappedSession.connect( - "http://localhost:4568", - "test-token", - Self.callbackHandler! + portalURL, + token, + self.callbackHandler ) ) self.networkMonitor = networkMonitor + self.isTunnelStarted = true + let settings = self.generateNetworkSettings(interfaceAddresses: self.interfaceAddresses, resources: self.resources) + try self.setNetworkSettings(settings) completionHandler(nil) } catch let error as AdapterError { networkMonitor.cancel() @@ -133,49 +128,54 @@ public class Adapter { } } - public func generateNetworkSettings( - addresses4: [String] = ["100.100.111.2"], addresses6: [String] = ["fd00:0222:2011:1111::2"], - ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] - ) - -> NEPacketTunnelNetworkSettings - { - // The destination IP that connlib will assign our DNS proxy to. - let dnsSentinel = "1.1.1.1" + func generateNetworkSettings(interfaceAddresses: InterfaceAddresses?, resources: [Resource]) -> NEPacketTunnelNetworkSettings { - // We can probably do better than this; see https://www.rfc-editor.org/info/rfc4821 - // But stick with something simple for now. 1280 is the minimum that will work for IPv6. - let mtu = 1280 - - // TODO: replace these with IPs returned from the connect call to portal - let subnetmask = "255.192.0.0" - let networkPrefixLength = NSNumber(value: 64) - - /* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for - * a tunnel to have no endpoint, or for there to be many endpoints, in - * which case, displaying a single one in settings doesn't really - * make sense. So, we fill it in with this placeholder, which is not - * a valid IP address that will actually route over the Internet. - */ - let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + // Interface addresses + let ipv4InterfaceAddresses = [interfaceAddresses?.ipv4].compactMap { $0 } + let ipv4SubnetMasks = ipv4InterfaceAddresses.map { _ in "255.255.255.255" } + + let ipv6InterfaceAddresses = [interfaceAddresses?.ipv6].compactMap { $0 } + let ipv6SubnetMasks = ipv6InterfaceAddresses.map { _ in NSNumber(integerLiteral: 128) } + + // Routes + var ipv4Routes: [NEIPv4Route] = [] + var ipv6Routes: [NEIPv6Route] = [] + + for resource in resources { + switch resource.resourceLocation { + case .dns(_, let ipv4, let ipv6): + if !ipv4.isEmpty { + ipv4Routes.append(NEIPv4Route(destinationAddress: ipv4, subnetMask: "255.255.255.255")) + } + if !ipv6.isEmpty { + ipv6Routes.append(NEIPv6Route(destinationAddress: ipv6, networkPrefixLength: 128)) + } + case .cidr(let addressRange): + ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.maskedAddress())", subnetMask: "\(addressRange.subnetMask())")) + } + } + + // DNS + let dnsSentinel = "1.1.1.1" // The destination IP that connlib will assign our DNS proxy to let dnsSettings = NEDNSSettings(servers: [dnsSentinel]) + dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS - // All DNS queries must first go through the tunnel's DNS - dnsSettings.matchDomains = [""] - networkSettings.dnsSettings = dnsSettings - networkSettings.mtu = NSNumber(value: mtu) + // Put it together - let ipv4Settings = NEIPv4Settings( - addresses: addresses4, - subnetMasks: [subnetmask]) + let ipv4Settings = NEIPv4Settings(addresses: ipv4InterfaceAddresses, subnetMasks: ipv4SubnetMasks) + let ipv6Settings = NEIPv6Settings(addresses: ipv6InterfaceAddresses, networkPrefixLengths: ipv6SubnetMasks) ipv4Settings.includedRoutes = ipv4Routes - networkSettings.ipv4Settings = ipv4Settings - - let ipv6Settings = NEIPv6Settings( - addresses: addresses6, - networkPrefixLengths: [networkPrefixLength]) ipv6Settings.includedRoutes = ipv6Routes + + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + networkSettings.dnsSettings = dnsSettings + networkSettings.ipv4Settings = ipv4Settings networkSettings.ipv6Settings = ipv6Settings + // We can probably do better than this; see https://www.rfc-editor.org/info/rfc4821 + // But stick with something simple for now. 1280 is the minimum that will work for IPv6. + networkSettings.mtu = NSNumber(value: 1280) + return networkSettings } @@ -201,56 +201,18 @@ public class Adapter { throw AdapterError.setNetworkSettings(systemError) } } - - // Save the latest applied network settings if there was no error. - if systemError != nil { - self.lastNetworkSettings = networkSettings - } } - /// Update runtime configuration. - /// - Parameters: - /// - ipv4Routes: IPv4 routes to send through the tunnel. - /// - ipv6Routes: IPv6 routes to send through the tunnel. - /// - completionHandler: completion handler. - public func update( - ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route], - completionHandler: @escaping (AdapterError?) -> Void - ) { + private func updateTunnelNetworkSettings() { + guard isTunnelStarted else { + return + } + let settings = generateNetworkSettings(interfaceAddresses: self.interfaceAddresses, resources: self.resources) workQueue.async { - if case .stopped = self.state { - completionHandler(.invalidState) - return - } - - // Tell the system that the tunnel is going to reconnect using new WireGuard - // configuration. - // This will broadcast the `NEVPNStatusDidChange` notification to the GUI process. - self.packetTunnelProvider?.reasserting = true - defer { - self.packetTunnelProvider?.reasserting = false - } - do { - try self.setNetworkSettings( - self.generateNetworkSettings(ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes)) - - switch self.state { - case .started(let wrappedSession): - self.state = .started(wrappedSession) - - case .temporaryShutdown: - self.state = .temporaryShutdown - - case .stopped: - fatalError() - } - - completionHandler(nil) - } catch let error as AdapterError { - completionHandler(error) + try self.setNetworkSettings(settings) } catch { - fatalError() + self.logger.log(level: .debug, "setNetworkSettings failed: \(error)") } } } @@ -281,7 +243,7 @@ public class Adapter { try self.setNetworkSettings(self.lastNetworkSettings!) self.state = .started( - try WrappedSession.connect("http://localhost:4568", "test-token", Self.callbackHandler!) + try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler) ) } catch { self.logger.log(level: .debug, "Failed to restart backend: \(error.localizedDescription)") @@ -296,3 +258,15 @@ public class Adapter { #endif } } + +extension Adapter: CallbackHandlerDelegate { + public func didUpdateResources(_ resources: [Resource]) { + self.resources = resources + updateTunnelNetworkSettings() + } + + public func didUpdateInterfaceAddresses(_ interfaceAddresses: InterfaceAddresses) { + self.interfaceAddresses = interfaceAddresses + updateTunnelNetworkSettings() + } +} diff --git a/apple/Sources/Connlib/CallbackHandler.swift b/apple/Sources/Connlib/CallbackHandler.swift index a1967eb..5d1f469 100644 --- a/apple/Sources/Connlib/CallbackHandler.swift +++ b/apple/Sources/Connlib/CallbackHandler.swift @@ -9,12 +9,12 @@ import NetworkExtension import os.log public protocol CallbackHandlerDelegate: AnyObject { - func didUpdateResources(_ resourceList: ResourceList) + func didUpdateResources(_ resources: [Resource]) + func didUpdateInterfaceAddresses(_ interfaceAddresses: InterfaceAddresses) } public class CallbackHandler { - // TODO: Add a table view property here to update? - var adapter: Adapter? + var adapter: Adapter public weak var delegate: CallbackHandlerDelegate? init(adapter: Adapter) { @@ -23,71 +23,47 @@ public class CallbackHandler { func onUpdateResources(resourceList: ResourceList) -> Bool { - // If there's any entity that assigned itself as this callbackHandler's delegate, it will be called everytime this `onUpdateResources` method is, allowing that entity to react to resource updates and do whatever they want. - - delegate?.didUpdateResources(resourceList) - - let addresses4 = - self.adapter?.lastNetworkSettings?.ipv4Settings?.addresses ?? ["100.100.111.2"] - let addresses6 = - self.adapter?.lastNetworkSettings?.ipv6Settings?.addresses ?? [ - "fd00:0222:2021:1111::2" - ] + let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") - // TODO: Use actual passed in resources to achieve split tunnel - let ipv4Routes = [NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0")] - let ipv6Routes = [ - NEIPv6Route(destinationAddress: "fd00:0222:2021:1111::0", networkPrefixLength: 64) - ] + do { + let resources = try resourceList.toResources() + logger.debug("Resources updated: \(resources)") + delegate?.didUpdateResources(resources) + } catch { + logger.error("Error parsing resource list: \(String(describing: error)) (JSON: \(resourceList.resources.toString()))") + return false + } - return setTunnelSettingsKeepingSomeExisting( - addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes - ) + return true } func onSetTunnelAddresses(tunnelAddresses: TunnelAddresses) -> Bool { - let addresses4 = [tunnelAddresses.address4.toString()] - let addresses6 = [tunnelAddresses.address6.toString()] - let ipv4Routes = - Adapter.currentAdapter?.lastNetworkSettings?.ipv4Settings?.includedRoutes ?? [] - let ipv6Routes = - Adapter.currentAdapter?.lastNetworkSettings?.ipv6Settings?.includedRoutes ?? [] - - return setTunnelSettingsKeepingSomeExisting( - addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes - ) - } - - private func setTunnelSettingsKeepingSomeExisting( - addresses4: [String], addresses6: [String], ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] - ) -> Bool { let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") - if self.adapter != nil { - do { - /* If the tunnel interface addresses are being updated, it's impossible for the tunnel to - stay up due to the way WireGuard works. Still, we try not to change the tunnel's routes - here Just In Case™. - */ - try self.adapter!.setNetworkSettings( - self.adapter!.generateNetworkSettings( - addresses4: addresses4, - addresses6: addresses6, - ipv4Routes: ipv4Routes, - ipv6Routes: ipv6Routes - ) - ) + let interfaceAddresses = tunnelAddresses.toInterfaceAddresses() + logger.debug("Interface addresses updated: (\(interfaceAddresses.ipv4), \(interfaceAddresses.ipv6))") + delegate?.didUpdateInterfaceAddresses(interfaceAddresses) - return true - } catch let error { - logger.log(level: .debug, "Error setting adapter settings: \(String(describing: error))") + return true + } +} - return false - } - } else { - logger.log(level: .debug, "Adapter not initialized!") +extension ResourceList { + enum ParseError: Error { + case notUTF8 + } - return false + func toResources() throws -> [Resource] { + let jsonString = resources.toString() + guard let jsonData = jsonString.data(using: .utf8) else { + throw ParseError.notUTF8 } + return try JSONDecoder().decode([Resource].self, from: jsonData) + } +} + +extension TunnelAddresses { + func toInterfaceAddresses() -> InterfaceAddresses { + InterfaceAddresses(ipv4: address4.toString(), ipv6: address6.toString()) } } diff --git a/apple/Sources/Connlib/Models/IPAddressRange.swift b/apple/Sources/Connlib/Models/IPAddressRange.swift new file mode 100644 index 0000000..60430af --- /dev/null +++ b/apple/Sources/Connlib/Models/IPAddressRange.swift @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2023 WireGuard LLC. All Rights Reserved. + +import Foundation +import Network + +public struct IPAddressRange { + public let address: IPAddress + public let networkPrefixLength: UInt8 + + init(address: IPAddress, networkPrefixLength: UInt8) { + self.address = address + self.networkPrefixLength = networkPrefixLength + } +} + +extension IPAddressRange: Equatable { + public static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool { + return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength + } +} + +extension IPAddressRange: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(address.rawValue) + hasher.combine(networkPrefixLength) + } +} + +extension IPAddressRange { + public var stringRepresentation: String { + return "\(address)/\(networkPrefixLength)" + } + + public init?(from string: String) { + guard let parsed = IPAddressRange.parseAddressString(string) else { return nil } + address = parsed.0 + networkPrefixLength = parsed.1 + } + + private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? { + let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex + let addressString = String(string[string.startIndex ..< endOfIPAddress]) + let address: IPAddress + if let addr = IPv4Address(addressString) { + address = addr + } else if let addr = IPv6Address(addressString) { + address = addr + } else { + return nil + } + + let maxNetworkPrefixLength: UInt8 = address is IPv4Address ? 32 : 128 + var networkPrefixLength: UInt8 + if endOfIPAddress < string.endIndex { // "/" was located + let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress) + guard indexOfNetworkPrefixLength < string.endIndex else { return nil } + let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex] + guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil } + networkPrefixLength = min(npl, maxNetworkPrefixLength) + } else { + networkPrefixLength = maxNetworkPrefixLength + } + + return (address, networkPrefixLength) + } + + public func subnetMask() -> IPAddress { + if address is IPv4Address { + let mask = networkPrefixLength > 0 ? ~UInt32(0) << (32 - networkPrefixLength) : UInt32(0) + let bytes = Data([ + UInt8(truncatingIfNeeded: mask >> 24), + UInt8(truncatingIfNeeded: mask >> 16), + UInt8(truncatingIfNeeded: mask >> 8), + UInt8(truncatingIfNeeded: mask >> 0) + ]) + return IPv4Address(bytes)! + } + if address is IPv6Address { + var bytes = Data(repeating: 0, count: 16) + for i in 0..> 24) + bytes[i + 1] = UInt8(truncatingIfNeeded: mask >> 16) + bytes[i + 2] = UInt8(truncatingIfNeeded: mask >> 8) + bytes[i + 3] = UInt8(truncatingIfNeeded: mask >> 0) + } + return IPv6Address(bytes)! + } + fatalError() + } + + public func maskedAddress() -> IPAddress { + let subnet = subnetMask().rawValue + var masked = Data(address.rawValue) + if subnet.count != masked.count { + fatalError() + } + for i in 0..