diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-12-20 16:30:26 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-01-03 11:39:21 +0100 |
| commit | 3a10abba95512f30df021260c68165bc58a770af (patch) | |
| tree | 882735e7db3cf0db8a92b453a204c2a861ff0eb8 | |
| parent | 04c26a00c0fc030f523d147fb1471001a52d8923 (diff) | |
| download | mullvadvpn-3a10abba95512f30df021260c68165bc58a770af.tar.xz mullvadvpn-3a10abba95512f30df021260c68165bc58a770af.zip | |
Move WireGuard related routine to WireguardDevice
| -rw-r--r-- | ios/MullvadVPN/IPEndpoint.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadEndpoint.swift | 2 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 406 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelSettingsGenerator.swift | 58 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardCommand.swift | 30 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardConfiguration.swift | 86 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardDevice.swift | 343 |
7 files changed, 643 insertions, 299 deletions
diff --git a/ios/MullvadVPN/IPEndpoint.swift b/ios/MullvadVPN/IPEndpoint.swift index 21ed353aa6..435267ae15 100644 --- a/ios/MullvadVPN/IPEndpoint.swift +++ b/ios/MullvadVPN/IPEndpoint.swift @@ -10,7 +10,7 @@ import Foundation import Network /// An abstract struct describing IP address based endpoint -struct IPEndpont<T> where T: IPAddress { +struct IPEndpont<T>: Hashable where T: IPAddress & Hashable { let ip: T let port: UInt16 } @@ -36,11 +36,24 @@ typealias IPv4Endpoint = IPEndpont<IPv4Address> typealias IPv6Endpoint = IPEndpont<IPv6Address> /// A enum describing any IP endpoint -enum AnyIPEndpoint { +enum AnyIPEndpoint: Hashable { case ipv4(IPv4Endpoint) case ipv6(IPv6Endpoint) } +extension AnyIPEndpoint: Equatable { + static func == (lhs: AnyIPEndpoint, rhs: AnyIPEndpoint) -> Bool { + switch (lhs, rhs) { + case (.ipv4(let a), .ipv4(let b)): + return a == b + case (.ipv6(let a), .ipv6(let b)): + return a == b + case (.ipv6, .ipv4), (.ipv4, .ipv6): + return false + } + } +} + /// Convenience methods for accessing endpoint fields extension AnyIPEndpoint { var ip: IPAddress { diff --git a/ios/MullvadVPN/MullvadEndpoint.swift b/ios/MullvadVPN/MullvadEndpoint.swift index 3aacca9806..f9bbc851d6 100644 --- a/ios/MullvadVPN/MullvadEndpoint.swift +++ b/ios/MullvadVPN/MullvadEndpoint.swift @@ -10,7 +10,7 @@ import Foundation import Network /// Contains server data needed to connect to a single mullvad endpoint -struct MullvadEndpoint { +struct MullvadEndpoint: Equatable { let ipv4Relay: IPv4Endpoint let ipv6Relay: IPv6Endpoint? let ipv4Gateway: IPv4Address diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 00a55a594d..7c2609df38 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -28,11 +28,11 @@ enum PacketTunnelProviderError: Error { /// Failure to set network settings case setNetworkSettings(Error) - /// Failure to discover the tunnel device (utun) - case tunnelDeviceNotFound - /// Failure to start the Wireguard backend - case startWireGuardBackend + case startWireguardDevice(WireguardDevice.Error) + + /// Failure to update the Wireguard configuration + case updateWireguardConfiguration(Error) /// IPC handler failure case ipcHandler(PacketTunnelIpcHandlerError) @@ -54,11 +54,11 @@ enum PacketTunnelProviderError: Error { case .setNetworkSettings(let systemError): return "Failed to set network settings: \(systemError.localizedDescription)" - case .tunnelDeviceNotFound: - return "Failed to find the tunnel device descriptor" + case .startWireguardDevice(let deviceError): + return "Failure to start Wireguard: \(deviceError.localizedDescription)" - case .startWireGuardBackend: - return "Failure to start the wireguard backend" + case .updateWireguardConfiguration(let error): + return "Failure to update Wireguard configuration: \(error.localizedDescription)" case .ipcHandler(let ipcError): return "Failure to handle the IPC request: \(ipcError.localizedDescription)" @@ -66,18 +66,39 @@ enum PacketTunnelProviderError: Error { } } -/// A wireguard events log -let wireguardLog = OSLog(subsystem: "net.mullvad.vpn.packet-tunnel", category: "Wireguard") +struct PacketTunnelConfiguration { + var tunnelConfig: TunnelConfiguration + var selectorResult: RelaySelectorResult +} + +extension PacketTunnelConfiguration { + var wireguardConfig: WireguardConfiguration { + let mullvadEndpoint = selectorResult.endpoint + var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)] + + if let ipv6Relay = mullvadEndpoint.ipv6Relay { + peers.append(.ipv6(ipv6Relay)) + } + + let wireguardPeers = peers.map { + WireguardPeer( + endpoint: $0, + publicKey: selectorResult.endpoint.publicKey) + } -/// A general tunnel provider log -let tunnelProviderLog = OSLog(subsystem: "net.mullvad.vpn.packet-tunnel", category: "Tunnel Provider") + return WireguardConfiguration( + privateKey: tunnelConfig.interface.privateKey, + peers: wireguardPeers + ) + } +} class PacketTunnelProvider: NEPacketTunnelProvider { - private var handle: Int32? - private var networkMonitor: NWPathMonitor? - private var tunnelInterfaceName: String? - private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator? + /// Active wireguard device + private var wireguardDevice: WireguardDevice? + + /// Active tunnel connection information private var connectionInfo: TunnelConnectionInfo? private let cancellableSet = CancellableSet() @@ -86,7 +107,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private let exclusivityQueue = DispatchQueue(label: "net.mullvad.vpn.packet-tunnel.exclusivity-queue") private let executionQueue = DispatchQueue(label: "net.mullvad.vpn.packet-tunnel.execution-queue") - private let networkMonitorQueue = DispatchQueue(label: "net.mullvad.vpn.packet-tunnel.network-monitor") override init() { super.init() @@ -94,26 +114,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.configureLogger() } - deinit { - networkMonitor?.cancel() - } - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { os_log(.default, log: tunnelProviderLog, "Start tunnel received.") startStopTunnelSubscriber = self.startTunnel() .sink(receiveCompletion: { (completion) in - switch completion { - case .finished: - os_log(.default, log: tunnelProviderLog, "Started the tunnel") + switch completion { + case .finished: + os_log(.default, log: tunnelProviderLog, "Started the tunnel") - completionHandler(nil) + completionHandler(nil) - case .failure(let error): - os_log(.error, log: tunnelProviderLog, "Failed to start the tunnel: %{public}s", error.localizedDescription) + case .failure(let error): + os_log(.error, log: tunnelProviderLog, "Failed to start the tunnel: %{public}s", error.localizedDescription) - completionHandler(error) - } + completionHandler(error) + } }) } @@ -170,12 +186,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } private func configureLogger() { - wgSetLogger { (level, messagePtr) in - guard let message = messagePtr.map({ String(cString: $0) }) else { return } - - let logType = WireguardLogLevel(rawValue: level)?.asOSLogType ?? .default - - os_log(logType, log: wireguardLog, "%{public}s", message) + WireguardDevice.setLogger { (level, message) in + os_log(level.osLogType, log: wireguardLog, "%{public}s", message) } } @@ -188,28 +200,38 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.startedTunnel = true - return self.setupTunnelNetworkSettings() - .flatMap { self.startWireguard().publisher } - .eraseToAnyPublisher() + return self.makePacketTunnelConfigAndApplyNetworkSettings() + .flatMap { + Self.startWireguard( + packetFlow: self.packetFlow, + configuration: $0.wireguardConfig + ) + .receive(on: self.executionQueue) + .handleEvents(receiveOutput: { (wireguardDevice) in + self.wireguardDevice = wireguardDevice + }).map { _ in () } + }.eraseToAnyPublisher() }.eraseToAnyPublisher() } private func stopTunnel() -> AnyPublisher<(), Never> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { () -> Just<()> in + MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { () -> AnyPublisher<(), Never> in os_log(.default, log: tunnelProviderLog, "Stopping the tunnel") self.startedTunnel = false - self.networkMonitor?.cancel() - self.networkMonitor = nil + if let device = self.wireguardDevice { + self.wireguardDevice = nil - if let handle = self.handle { - wgTurnOff(handle) + // ignore errors at this point + return device.stop() + .replaceError(with: ()) + .assertNoFailure() + .eraseToAnyPublisher() + } else { + return Just(()) + .eraseToAnyPublisher() } - - self.handle = nil - - return Just(()) }.eraseToAnyPublisher() } @@ -223,223 +245,143 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return Result.Publisher(()).eraseToAnyPublisher() } - os_log(.default, log: tunnelProviderLog, "Reload tunnel settings") - return self.setupTunnelNetworkSettings() - .handleEvents(receiveSubscription: { _ in - // Tell the system that the tunnel is about to reconnect with the new endpoint - self.reasserting = true - }, receiveCompletion: { (completion) in - switch completion { - case .finished: - guard let handle = self.handle else { return } + guard let wireguardDevice = self.wireguardDevice else { + os_log(.default, log: tunnelProviderLog, + "Ignore reloading tunnel settings. The WireguardDevice is not set yet.") - os_log(.default, log: tunnelProviderLog, "Replace Wireguard endpoints") + return Result.Publisher(()).eraseToAnyPublisher() + } + + os_log(.default, log: tunnelProviderLog, "Reload tunnel settings") - _ = self.packetTunnelSettingsGenerator? - .wireguardConfigurationForChangingRelays() - .withGoString { wgSetConfig(handle, $0) } + return self.makePacketTunnelConfigAndApplyNetworkSettings() + .flatMap { (packetTunnelConfig) in + wireguardDevice + .setConfig(configuration: packetTunnelConfig.wireguardConfig) + .mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) } + } + .receive(on: self.executionQueue) + .handleEvents(receiveSubscription: { _ in + // Tell the system that the tunnel is about to reconnect with the new endpoint + self.reasserting = true + }, receiveCompletion: { (completion) in + switch completion { + case .finished: + os_log(.default, log: tunnelProviderLog, "Set new tunnel settings") - case .failure(let error): - os_log(.default, log: tunnelProviderLog, - "Failed to set the new tunnel settings: %{public}s", - error.localizedDescription) - } + case .failure(let error): + os_log(.default, log: tunnelProviderLog, + "Failed to set the new tunnel settings: %{public}s", + error.localizedDescription) + } - // Tell the system that the tunnel has finished reconnecting - self.reasserting = false - }, receiveCancel: { - // Tell the system that the tunnel has finished reconnecting - // in the event of task cancellation - self.reasserting = false - }).eraseToAnyPublisher() + // Tell the system that the tunnel has finished reconnecting + self.reasserting = false + }, receiveCancel: { + // Tell the system that the tunnel has finished reconnecting + // in the event of task cancellation + self.reasserting = false + }).eraseToAnyPublisher() }.eraseToAnyPublisher() } - private func setupTunnelNetworkSettings() -> AnyPublisher<(), PacketTunnelProviderError> { - return readTunnelConfigurationFromKeychain().publisher - .flatMap { (tunnelConfiguration) in - return self.selectRelayEndpoint(tunnelConfiguration: tunnelConfiguration) - .receive(on: self.executionQueue) - .map({ (result) -> MullvadEndpoint in - os_log(.default, log: tunnelProviderLog, "Selected relay: %{public}s", result.relay.hostname) - - self.connectionInfo = TunnelConnectionInfo( - ipv4Relay: result.endpoint.ipv4Relay, - ipv6Relay: result.endpoint.ipv6Relay, - hostname: result.relay.hostname) + private func setTunnelConnectionInfo(selectorResult: RelaySelectorResult) { + self.connectionInfo = TunnelConnectionInfo( + ipv4Relay: selectorResult.endpoint.ipv4Relay, + ipv6Relay: selectorResult.endpoint.ipv6Relay, + hostname: selectorResult.relay.hostname) - return result.endpoint - }) - .flatMap({ (endpoint) -> AnyPublisher<(), PacketTunnelProviderError> in - let settingsGenerator = PacketTunnelSettingsGenerator( - mullvadEndpoint: endpoint, - tunnelConfiguration: tunnelConfiguration) - - let networkSettings = settingsGenerator.networkSettings() + os_log(.default, log: tunnelProviderLog, + "Selected relay: %{public}s", + selectorResult.relay.hostname) + } - self.packetTunnelSettingsGenerator = settingsGenerator + /// Make and return `PacketTunnelConfig` after applying network settings and setting the + /// tunnel connection info + private func makePacketTunnelConfigAndApplyNetworkSettings() + -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> { + self.makePacketTunnelConfig() + .receive(on: executionQueue) + .flatMap { (packetTunnelConfig) -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> in + self.setTunnelConnectionInfo(selectorResult: packetTunnelConfig.selectorResult) - os_log(.default, log: tunnelProviderLog, "Set tunnel network settings") + return self.applyNetworkSettings(packetTunnelConfig: packetTunnelConfig) + .map { packetTunnelConfig } + .eraseToAnyPublisher() + }.eraseToAnyPublisher() + } - return self.setTunnelNetworkSettings(networkSettings) - .mapError { (error) in - os_log(.error, log: tunnelProviderLog, "Cannot set network settings: %{public}s", error.localizedDescription) + /// Returns a `PacketTunnelConfig` that contains the tunnel configuration and selected relay + private func makePacketTunnelConfig() -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> { + let keychainRef = (protocolConfiguration as? NETunnelProviderProtocol)?.passwordReference - return PacketTunnelProviderError.setNetworkSettings(error) + return Just(keychainRef) + .setFailureType(to: PacketTunnelProviderError.self) + .replaceNil(with: PacketTunnelProviderError.missingKeychainConfigurationReference) + .flatMap { (keychainRef) in + Self.readTunnelConfiguration(keychainReference: keychainRef).publisher + .flatMap { (tunnelConfiguration) in + Self.selectRelayEndpoint(relayConstraints: tunnelConfiguration.relayConstraints) + .map { (selectorResult) in + PacketTunnelConfiguration( + tunnelConfig: tunnelConfiguration, + selectorResult: selectorResult) } - .receive(on: self.executionQueue) - .eraseToAnyPublisher() - }) + } }.eraseToAnyPublisher() } - private func readTunnelConfigurationFromKeychain() -> Result<TunnelConfiguration, PacketTunnelProviderError> { - guard let keychainReference = (protocolConfiguration as? NETunnelProviderProtocol)?.passwordReference else { - return .failure(.missingKeychainConfigurationReference) + /// Set system network settings using `PacketTunnelConfig` + private func applyNetworkSettings(packetTunnelConfig: PacketTunnelConfiguration) -> AnyPublisher<(), PacketTunnelProviderError> { + let settingsGenerator = PacketTunnelSettingsGenerator( + mullvadEndpoint: packetTunnelConfig.selectorResult.endpoint, + tunnelConfiguration: packetTunnelConfig.tunnelConfig + ) + + os_log(.default, log: tunnelProviderLog, "Set tunnel network settings") + + return self.setTunnelNetworkSettings(settingsGenerator.networkSettings()) + .mapError { (error) in + os_log(.error, log: tunnelProviderLog, "Cannot set network settings: %{public}s", error.localizedDescription) + + return PacketTunnelProviderError.setNetworkSettings(error) } + .receive(on: self.executionQueue) + .eraseToAnyPublisher() + } + /// Read tunnel configuration from Keychain + private class func readTunnelConfiguration(keychainReference: Data) -> Result<TunnelConfiguration, PacketTunnelProviderError> { return TunnelConfigurationManager.load(persistentKeychainRef: keychainReference) .mapError { PacketTunnelProviderError.cannotReadTunnelConfiguration($0) } } - private func selectRelayEndpoint(tunnelConfiguration: TunnelConfiguration) -> AnyPublisher<RelaySelectorResult, PacketTunnelProviderError> { + /// Load relay cache with potential networking to refresh the cache and pick the relay for the + /// given relay constraints. + private class func selectRelayEndpoint(relayConstraints: RelayConstraints) -> AnyPublisher<RelaySelectorResult, PacketTunnelProviderError> { return RelaySelector.loadedFromRelayCache() .mapError { PacketTunnelProviderError.readRelayCache($0) } .flatMap { (relaySelector) -> Result<RelaySelectorResult, PacketTunnelProviderError>.Publisher in return relaySelector - .evaluate(with: tunnelConfiguration.relayConstraints) + .evaluate(with: relayConstraints) .flatMap { .init($0) } ?? .init(.noRelaySatisfyingConstraint) }.eraseToAnyPublisher() } - private func startWireguard() -> Result<(), PacketTunnelProviderError> { - let tunnelSettingsGenerator = self.packetTunnelSettingsGenerator! - - let fileDescriptor = self.getTunnelInterfaceDescriptor() - if fileDescriptor < 0 { - os_log(.error, log: tunnelProviderLog, "Cannot find the file descriptor for socket.") - return .failure(.tunnelDeviceNotFound) - } - - self.tunnelInterfaceName = self.getInterfaceName(fileDescriptor) - - os_log(.default, log: tunnelProviderLog, "Tunnel interface is %{public}s", self.tunnelInterfaceName ?? "unknown") - - let handle = tunnelSettingsGenerator.entireWireguardConfiguration() - .withGoString { wgTurnOn($0, fileDescriptor) } - - if handle < 0 { - os_log(.error, log: tunnelProviderLog, "Failed to start the Wireguard backend, wgTurnOn returned %{public}d", handle) - - return .failure(.startWireGuardBackend) - } - - self.handle = handle - - startNetworkMonitor() - - return .success(()) - } - - private func startNetworkMonitor() { - self.networkMonitor?.cancel() - - let networkMonitor = NWPathMonitor() - networkMonitor.pathUpdateHandler = { [weak self] in self?.didReceiveNetworkPathUpdate(path: $0) } - networkMonitor.start(queue: networkMonitorQueue) - self.networkMonitor = networkMonitor - } - - private func didReceiveNetworkPathUpdate(path: Network.NWPath) { - executionQueue.async { - guard let handle = self.handle else { return } - - os_log(.default, log: tunnelProviderLog, - "Network change detected with %{public}s route and interface order: %{public}s", - "\(path.status)", - "\(path.availableInterfaces.map { $0.name }.joined(separator: ", "))" - ) - - _ = self.packetTunnelSettingsGenerator? - .wireguardConfigurationWithReresolvedEndpoints() - .withGoString { wgSetConfig(handle, $0) } - - wgBumpSockets(handle) - } - } -} - - -extension PacketTunnelProvider { - - fileprivate func getTunnelInterfaceDescriptor() -> Int32 { - return packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32 ?? -1 - } - - fileprivate func getInterfaceName(_ fileDescriptor: Int32) -> String? { - var buffer = [UInt8](repeating: 0, count: Int(IFNAMSIZ)) - - return buffer.withUnsafeMutableBufferPointer({ (mutableBufferPointer) -> String? in - guard let baseAddress = mutableBufferPointer.baseAddress else { return nil } - - var ifnameSize = socklen_t(IFNAMSIZ) - let result = getsockopt( - fileDescriptor, - 2 /* SYSPROTO_CONTROL */, - 2 /* UTUN_OPT_IFNAME */, - baseAddress, - &ifnameSize) - - if result == 0 { - return String(cString: baseAddress) - } else { - return nil - } - }) - } - -} - -extension Network.NWPath.Status: CustomDebugStringConvertible { - public var debugDescription: String { - var output = "NWPath.Status." - - switch self { - case .requiresConnection: - output += "requiresConnection" - case .satisfied: - output += "satisfied" - case .unsatisfied: - output += "unsatisfied" - @unknown default: - output += "unknown" - } - - return output - } -} - -extension String { - func withGoString<R>(_ block: (_ goString: gostring_t) throws -> R) rethrows -> R { - return try withCString { try block(gostring_t(p: $0, n: utf8.count)) } - } -} - -/// A enum describing the Wireguard log levels defined in api-ios.go from wireguard-apple repository -enum WireguardLogLevel: Int32 { - case debug = 0 - case info = 1 - case error = 2 + private class func startWireguard(packetFlow: NEPacketTunnelFlow, configuration: WireguardConfiguration) -> AnyPublisher<WireguardDevice, PacketTunnelProviderError> { + WireguardDevice.fromPacketFlow(packetFlow) + .publisher + .flatMap { (device) -> AnyPublisher<WireguardDevice, WireguardDevice.Error> in + os_log(.default, log: tunnelProviderLog, + "Tunnel interface is %{public}s", + device.getInterfaceName() ?? "unknown") - var asOSLogType: OSLogType { - switch self { - case .debug: - return .debug - case .info: - return .info - case .error: - return .error + return device.start(configuration: configuration) + .map { device } + .eraseToAnyPublisher() } + .mapError { PacketTunnelProviderError.startWireguardDevice($0) } + .eraseToAnyPublisher() } } diff --git a/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift index a431eba5de..5d69be937b 100644 --- a/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift +++ b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift @@ -27,64 +27,6 @@ struct PacketTunnelSettingsGenerator { return networkSettings } - func entireWireguardConfiguration() -> String { - var commands: [WireguardCommand] = [ - .privateKey(tunnelConfiguration.interface.privateKey), - .listenPort(0), - .replacePeers - ] - - commands.append(contentsOf: makePeersConfiguration()) - commands.append(makeAllowedIPsConfiguration()) - - return commands.toWireguardConfig() - } - - func wireguardConfigurationWithReresolvedEndpoints() -> String { - return makePeersConfiguration().toWireguardConfig() - } - - func wireguardConfigurationForChangingRelays() -> String { - var commands = [WireguardCommand.replacePeers] - - commands.append(contentsOf: makePeersConfiguration()) - commands.append(makeAllowedIPsConfiguration()) - - return commands.toWireguardConfig() - } - - private func makePeersConfiguration() -> [WireguardCommand] { - var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)] - - if let ipv6Relay = mullvadEndpoint.ipv6Relay { - peers.append(.ipv6(ipv6Relay)) - } - - return peers.compactMap { (peer) in - switch peer.withReresolvedIP() { - case .success(let resolvedPeer): - // TODO: this is not reliable. We should attempt to re-resolve the IPs in case of - // failure? - return .peer( - WireguardPeer( - endpoint: resolvedPeer, - publicKey: mullvadEndpoint.publicKey - ) - ) - - case .failure(let error): - os_log(.error, - "Failed to resolve the endpoint: %s. Cause: %{public}s", - "\(peer.ip)", error.localizedDescription) - return nil - } - } - } - - private func makeAllowedIPsConfiguration() -> WireguardCommand { - return .allowedIP(IPAddressRange(from: "0.0.0.0/0")!) - } - private func dnsSettings() -> NEDNSSettings { let serverAddresses = [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway] .map { String(reflecting: $0) } diff --git a/ios/PacketTunnel/WireguardCommand.swift b/ios/PacketTunnel/WireguardCommand.swift index 45993586b0..caa15a7eeb 100644 --- a/ios/PacketTunnel/WireguardCommand.swift +++ b/ios/PacketTunnel/WireguardCommand.swift @@ -9,17 +9,38 @@ import Foundation import Network -struct WireguardPeer { +struct WireguardPeer: Hashable { let endpoint: AnyIPEndpoint let publicKey: Data } +extension WireguardPeer { + + func withReresolvedEndpoint() -> Result<WireguardPeer, Error> { + self.endpoint.withReresolvedIP() + .map { WireguardPeer(endpoint: $0, publicKey: self.publicKey) } + } + +} + +extension WireguardPeer { + + /// Returns a 0.0.0.0/0 for IPv4 and ::0/0 for IPv6 + var anyAllowedIP: IPAddressRange { + switch endpoint { + case .ipv4: + return IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0) + case .ipv6: + return IPAddressRange(address: IPv6Address.any, networkPrefixLength: 0) + } + } +} + enum WireguardCommand { case privateKey(WireguardPrivateKey) case listenPort(UInt16) case replacePeers case peer(WireguardPeer) - case replaceAllowedIPs case allowedIP(IPAddressRange) } @@ -45,9 +66,6 @@ extension WireguardCommand { return ["public_key=\(keyString)", "endpoint=\(endpointString)"] .joined(separator: "\n") - case .replaceAllowedIPs: - return "replace_allowed_ips=true" - case .allowedIP(let ipAddressRange): return "allowed_ip=\(ipAddressRange)" } @@ -56,7 +74,7 @@ extension WireguardCommand { } extension Array where Element == WireguardCommand { - func toWireguardConfig() -> String { + func toRawWireguardConfigString() -> String { map { $0.toRawWireguardCommand() } .joined(separator: "\n") } diff --git a/ios/PacketTunnel/WireguardConfiguration.swift b/ios/PacketTunnel/WireguardConfiguration.swift new file mode 100644 index 0000000000..70410bfa17 --- /dev/null +++ b/ios/PacketTunnel/WireguardConfiguration.swift @@ -0,0 +1,86 @@ +// +// WireguardConfiguration.swift +// PacketTunnel +// +// Created by pronebird on 17/12/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Combine +import Foundation +import os + +/// A struct describing a basic WireGuard configuration +struct WireguardConfiguration { + var privateKey: WireguardPrivateKey + var peers: [WireguardPeer] +} + +extension WireguardConfiguration { + + /// Returns a baseline configuration for WireGuard + func baseline() -> [WireguardCommand] { + var commands: [WireguardCommand] = [ + .privateKey(privateKey), + .listenPort(0), + .replacePeers + ] + + peers.forEach { (peer) in + commands.append(.peer(peer)) + commands.append(.allowedIP(peer.anyAllowedIP)) + } + + return commands + } + + /// Returns a WireGuard configuration for transition to the given configuration + func transition(to newConfig: WireguardConfiguration) -> [WireguardCommand] { + var commands = [WireguardCommand]() + + if self.privateKey != newConfig.privateKey { + commands.append(.privateKey(newConfig.privateKey)) + } + + let oldPeers = Set(self.peers) + let newPeers = Set(newConfig.peers) + + if oldPeers != newPeers { + let oldPublicKeys = Set(oldPeers.map { $0.publicKey }) + let newPublicKeys = Set(newPeers.map { $0.publicKey }) + + // Avoid using `replace_peers` when updating the existing peers. + if oldPublicKeys != newPublicKeys { + commands.append(.replacePeers) + } + + newPeers.forEach { (peer) in + commands.append(.peer(peer)) + commands.append(.allowedIP(peer.anyAllowedIP)) + } + } + + return commands + } + + func withReresolvedPeers(maxRetryOnFailure: Int = 0) -> AnyPublisher<WireguardConfiguration, Error> { + self.peers + .publisher + .setFailureType(to: Error.self) + .flatMap { + $0.withReresolvedEndpoint() + .publisher + .retry(maxRetryOnFailure) + + } + .collect() + .map({ (peers) in + WireguardConfiguration( + privateKey: self.privateKey, + peers: peers + ) + }) + .eraseToAnyPublisher() + } + +} diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift new file mode 100644 index 0000000000..62c346b37d --- /dev/null +++ b/ios/PacketTunnel/WireguardDevice.swift @@ -0,0 +1,343 @@ +// +// WireguardDevice.swift +// PacketTunnel +// +// Created by pronebird on 16/12/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Combine +import Foundation +import NetworkExtension +import os + +/// A class describing the `wireguard-go` interactions +/// +/// - Thread safety: +/// This class is thread safe. +class WireguardDevice { + + typealias WireguardLogHandler = (WireguardLogLevel, String) -> Void + + /// An error type describing the errors returned by `WireguardDevice` + enum Error: Swift.Error { + /// A failure to obtain the tunnel device file descriptor + case cannotLocateSocketDescriptor + + /// A failure to start the Wireguard backend + case start(Int32) + + /// A failure that indicates that Wireguard has not been started yet + case notStarted + + /// A failure that indicates that Wireguard has already been started + case alreadyStarted + + /// A failure to resolve endpoints + case resolveEndpoints(Swift.Error) + + var localizedDescription: String { + switch self { + case .cannotLocateSocketDescriptor: + return "Unable to locate the file descriptor for socket." + case .start(let wgErrorCode): + return "Failed to start Wireguard. Return code: \(wgErrorCode)" + case .notStarted: + return "Wireguard has not been started yet" + case .alreadyStarted: + return "Wireguard has already been started" + case .resolveEndpoints(let resolutionError): + return "Failed to resolve endpoints: \(resolutionError.localizedDescription)" + } + } + } + + /// A global Wireguard log handler + /// It should only be accessed from the `loggingQueue` + private static var wireguardLogHandler: WireguardLogHandler? + + /// A private queue used for Wireguard logging + private static let loggingQueue = DispatchQueue( + label: "net.mullvad.vpn.packet-tunnel.wireguard-device.global-logging-queue", + qos: .background + ) + + /// A private queue used to synchronize access to `WireguardDevice` members + private let workQueue = DispatchQueue( + label: "net.mullvad.vpn.packet-tunnel.wireguard-device.work-queue" + ) + + /// A private queue used for network monitor + private let networkMonitorQueue = DispatchQueue( + label: "net.mullvad.vpn.packet-tunnel.network-monitor" + ) + + /// Network routes monitor + private var networkMonitor: NWPathMonitor? + + /// A subscriber used when resolving peer addresses + private var peerResolutionSubscriber: AnyCancellable? + + /// A tunnel device descriptor + private let tunFd: Int32 + + /// A wireguard internal handle returned by `wgTurnOn` that's used to associate the calls + /// with the specific Wireguard tunnel. + private var wireguardHandle: Int32? + + /// An instance of `WireguardConfiguration` + private var configuration: WireguardConfiguration? + + /// Returns a Wireguard version + class var version: String { + String(cString: wgVersion()) + } + + /// Set global Wireguard log handler. + /// The given handler is dispatched on a background serial queue. + /// + /// - Thread safety: + /// This function is thread safe + class func setLogger(with handler: @escaping WireguardLogHandler) { + WireguardDevice.loggingQueue.async { + WireguardDevice.wireguardLogHandler = handler + + wgSetLogger { (level, messagePtr) in + guard let message = messagePtr.map({ String(cString: $0) }) else { return } + let logType = WireguardLogLevel(rawValue: level) ?? .debug + + WireguardDevice.loggingQueue.async { + WireguardDevice.wireguardLogHandler?(logType, message) + } + } + } + } + + // MARK: - Initialization + + /// A designated initializer + class func fromPacketFlow(_ packetFlow: NEPacketTunnelFlow) -> Result<WireguardDevice, Error> { + if let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32 { + return .success(.init(tunFd: fd)) + } else { + return .failure(.cannotLocateSocketDescriptor) + } + } + + /// Private initializer + private init(tunFd: Int32) { + self.tunFd = tunFd + } + + deinit { + networkMonitor?.cancel() + } + + // MARK: - Public methods + + func start(configuration: WireguardConfiguration) -> AnyPublisher<(), Error> { + return Deferred { + Future { (fulfill) in + fulfill(self._start(configuration: configuration)) + } + }.subscribe(on: workQueue) + .eraseToAnyPublisher() + } + + func stop() -> AnyPublisher<(), Error> { + Deferred { + Future { (fulfill) in + fulfill(self._stop()) + } + }.subscribe(on: workQueue) + .eraseToAnyPublisher() + } + + func setConfig(configuration: WireguardConfiguration) -> AnyPublisher<(), Error> { + Deferred { + Future { (fulfill) in + fulfill(self._setConfig(configuration: configuration)) + } + }.subscribe(on: workQueue) + .eraseToAnyPublisher() + } + + func getInterfaceName() -> String? { + var buffer = [UInt8](repeating: 0, count: Int(IFNAMSIZ)) + + return buffer.withUnsafeMutableBufferPointer { (mutableBufferPointer) in + guard let baseAddress = mutableBufferPointer.baseAddress else { return nil } + + var ifnameSize = socklen_t(IFNAMSIZ) + let result = getsockopt( + self.tunFd, + 2 /* SYSPROTO_CONTROL */, + 2 /* UTUN_OPT_IFNAME */, + baseAddress, + &ifnameSize) + + if result == 0 { + return String(cString: baseAddress) + } else { + return nil + } + } + } + + // MARK: - Private methods + + private func _start(configuration: WireguardConfiguration) -> Result<(), Error> { + guard wireguardHandle == nil else { + return .failure(.alreadyStarted) + } + + let handle = configuration.baseline().toRawWireguardConfigString() + .withGoString { wgTurnOn($0, self.tunFd) } + + if handle < 0 { + return .failure(.start(handle)) + } else { + wireguardHandle = handle + self.configuration = configuration + + startNetworkMonitor() + + return .success(()) + } + } + + private func _stop() -> Result<(), Error> { + if let handle = wireguardHandle { + networkMonitor?.cancel() + networkMonitor = nil + + wgTurnOff(handle) + wireguardHandle = nil + + return .success(()) + } else { + return .failure(.notStarted) + } + } + + private func _setConfig(configuration: WireguardConfiguration) -> Result<(), Error> { + if let handle = wireguardHandle, let activeConfiguration = self.configuration { + let wireguardCommands = activeConfiguration.transition(to: configuration) + + Self.setWireguardConfig(handle: handle, commands: wireguardCommands) + + self.configuration = configuration + + return .success(()) + } else { + return .failure(.notStarted) + } + } + + private class func setWireguardConfig(handle: Int32, commands: [WireguardCommand]) { + // Ignore empty payloads + guard !commands.isEmpty else { return } + + let rawConfig = commands.toRawWireguardConfigString() + + os_log(.info, log: wireguardDeviceLog, "wgSetConfig:\n%{public}s", rawConfig) + + _ = rawConfig.withGoString { wgSetConfig(handle, $0) } + } + + // MARK: - Network monitoring + + private func startNetworkMonitor() { + self.networkMonitor?.cancel() + + let networkMonitor = NWPathMonitor() + networkMonitor.pathUpdateHandler = { [weak self] (path) in + self?.didReceiveNetworkPathUpdate(path: path) + } + networkMonitor.start(queue: networkMonitorQueue) + self.networkMonitor = networkMonitor + } + + private func didReceiveNetworkPathUpdate(path: Network.NWPath) { + workQueue.async { + guard let handle = self.wireguardHandle else { return } + + os_log(.debug, log: wireguardDeviceLog, + "Network change detected. Status: %{public}s, interfaces %{public}s.", + String(describing: path.status), + String(describing: path.availableInterfaces)) + + // Re-resolve endpoints on network changes and update Wireguard configuration + if let activeConfiguration = self.configuration { + self.peerResolutionSubscriber = activeConfiguration + .withReresolvedPeers(maxRetryOnFailure: 1) + .mapError { WireguardDevice.Error.resolveEndpoints($0) } + .sink(receiveCompletion: { (completion) in + switch completion { + case .finished: + os_log(.debug, log: wireguardDeviceLog, "Re-resolved endpoints") + + case .failure(let error): + os_log(.error, log: wireguardDeviceLog, + "Failed to re-resolve endpoints: %{public}s", + error.localizedDescription) + } + }, receiveValue: { (reresolvedConfiguration) in + let commands = activeConfiguration + .transition(to: reresolvedConfiguration) + + Self.setWireguardConfig( + handle: handle, + commands: commands + ) + }) + } + + // Tell Wireguard to re-open sockets and bind them to the new network interface + wgBumpSockets(handle) + } + } +} + +/// A enum describing Wireguard log levels defined in `api-ios.go` from `wireguard-apple` repository +enum WireguardLogLevel: Int32 { + case debug = 0 + case info = 1 + case error = 2 + + var osLogType: OSLogType { + switch self { + case .debug: + return .debug + case .info: + return .info + case .error: + return .error + } + } +} + +private extension String { + func withGoString<R>(_ block: (_ goString: gostring_t) throws -> R) rethrows -> R { + return try withCString { try block(gostring_t(p: $0, n: utf8.count)) } + } +} + +extension Network.NWPath.Status: CustomDebugStringConvertible { + public var debugDescription: String { + var output = "NWPath.Status." + + switch self { + case .requiresConnection: + output += "requiresConnection" + case .satisfied: + output += "satisfied" + case .unsatisfied: + output += "unsatisfied" + @unknown default: + output += "unknown" + } + + return output + } +} |
