diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-12-09 16:23:35 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-12-09 16:23:35 +0100 |
| commit | 8f49b59331e9168aebd6085f136303be7d5f0502 (patch) | |
| tree | be6731b16e7e1dc500d97e16896394deecd57ea8 | |
| parent | be89341733b58f2e992141fe99713e4d6e4ba7fd (diff) | |
| parent | 9c4bd3cdacc5e7ac32f99e4334500e82b606e20a (diff) | |
| download | mullvadvpn-8f49b59331e9168aebd6085f136303be7d5f0502.tar.xz mullvadvpn-8f49b59331e9168aebd6085f136303be7d5f0502.zip | |
Merge branch 'add-packet-tunnel-ios'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme | 96 | ||||
| -rw-r--r-- | ios/MullvadVPN/AutoDisposableSink.swift | 67 | ||||
| -rw-r--r-- | ios/MullvadVPN/Data+HexCoding.swift | 21 | ||||
| -rw-r--r-- | ios/MullvadVPN/IPEndpoint.swift | 63 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadEndpoint.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadVPN.entitlements | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/PacketTunnelIpc.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelaySelector.swift | 5 | ||||
| -rw-r--r-- | ios/PacketTunnel/AnyIPEndpoint+DNS64.swift | 62 | ||||
| -rw-r--r-- | ios/PacketTunnel/AnyIPEndpoint+Wireguard.swift | 24 | ||||
| -rw-r--r-- | ios/PacketTunnel/Info.plist | 4 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnel.entitlements | 12 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 437 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelSettingsGenerator.swift | 156 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardCommand.swift | 63 |
15 files changed, 1001 insertions, 21 deletions
diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme new file mode 100644 index 0000000000..1c7c49e85b --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1120" + wasCreatedForAppExtension = "YES" + version = "2.0"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58CE5E78224146470008646E" + BuildableName = "PacketTunnel.appex" + BlueprintName = "PacketTunnel" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58CE5E5F224146200008646E" + BuildableName = "MullvadVPN.app" + BlueprintName = "MullvadVPN" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "" + selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" + launchStyle = "0" + askForAppToLaunch = "Yes" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES" + launchAutomaticallySubstyle = "2"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58CE5E5F224146200008646E" + BuildableName = "MullvadVPN.app" + BlueprintName = "MullvadVPN" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES" + launchAutomaticallySubstyle = "2"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58CE5E5F224146200008646E" + BuildableName = "MullvadVPN.app" + BlueprintName = "MullvadVPN" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/ios/MullvadVPN/AutoDisposableSink.swift b/ios/MullvadVPN/AutoDisposableSink.swift new file mode 100644 index 0000000000..de424b9d1b --- /dev/null +++ b/ios/MullvadVPN/AutoDisposableSink.swift @@ -0,0 +1,67 @@ +// +// AutoDisposableSink.swift +// MullvadVPN +// +// Created by pronebird on 01/11/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Combine +import Foundation + +/// A thread safe storage for a set of `AnyCancellable` objects +final class CancellableSet { + private let lock = NSLock() + private var storage = Set<AnyCancellable>() + + func append(_ cancellable: AnyCancellable) { + lock.lock() + storage.insert(cancellable) + lock.unlock() + } + + func remove(_ cancellable: AnyCancellable) { + lock.lock() + storage.remove(cancellable) + lock.unlock() + } +} + +extension Publisher { + + /// Make a `Publishers.Sink` subscriber and put it in the given `CancellableSet`, automatically + /// remove it upon completion. + func autoDisposableSink(cancellableSet: CancellableSet, receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> Void { + var sharedCancellable: AnyCancellable? + + let disposeSubscriber = { + if let sharedCancellable = sharedCancellable { + cancellableSet.remove(sharedCancellable) + } + } + + let cancellable = handleEvents(receiveCancel: { + disposeSubscriber() + }).sink(receiveCompletion: { (completion) in + receiveCompletion(completion) + + disposeSubscriber() + }, receiveValue: receiveValue) + + sharedCancellable = cancellable + cancellableSet.append(cancellable) + } + +} + +extension Publisher where Output == Void, Failure: Error { + + func sink(receiveCompletion: @escaping ((Subscribers.Completion<Failure>) -> Void)) -> AnyCancellable { + return sink(receiveCompletion: receiveCompletion, receiveValue: { _ in }) + } + + func autoDisposableSink(cancellableSet: CancellableSet, receiveCompletion: @escaping ((Subscribers.Completion<Failure>) -> Void)) -> Void { + return autoDisposableSink(cancellableSet: cancellableSet, receiveCompletion: receiveCompletion, receiveValue: { _ in }) + } + +} diff --git a/ios/MullvadVPN/Data+HexCoding.swift b/ios/MullvadVPN/Data+HexCoding.swift new file mode 100644 index 0000000000..e1ed224631 --- /dev/null +++ b/ios/MullvadVPN/Data+HexCoding.swift @@ -0,0 +1,21 @@ +// +// Data+HexCoding.swift +// MullvadVPN +// +// Created by pronebird on 20/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +extension Data { + struct HexEncodingOptions: OptionSet { + let rawValue: Int + static let upperCase = HexEncodingOptions(rawValue: 1 << 0) + } + + func hexEncodedString(options: HexEncodingOptions = []) -> String { + let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" + return map { String(format: format, $0) }.joined() + } +} diff --git a/ios/MullvadVPN/IPEndpoint.swift b/ios/MullvadVPN/IPEndpoint.swift new file mode 100644 index 0000000000..21ed353aa6 --- /dev/null +++ b/ios/MullvadVPN/IPEndpoint.swift @@ -0,0 +1,63 @@ +// +// IPEndpoint.swift +// MullvadVPN +// +// Created by pronebird on 06/12/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network + +/// An abstract struct describing IP address based endpoint +struct IPEndpont<T> where T: IPAddress { + let ip: T + let port: UInt16 +} + +extension IPEndpont: Codable where T: Codable {} + +extension IPEndpont: Equatable { + static func == (lhs: IPEndpont<T>, rhs: IPEndpont<T>) -> Bool { + lhs.ip.rawValue == rhs.ip.rawValue && lhs.port == rhs.port + } +} + +extension IPEndpont: CustomStringConvertible { + var description: String { + return "\(ip):\(port)" + } +} + +/// An alias for IPv4 endpoint +typealias IPv4Endpoint = IPEndpont<IPv4Address> + +/// An alias for IPv6 endpoint +typealias IPv6Endpoint = IPEndpont<IPv6Address> + +/// A enum describing any IP endpoint +enum AnyIPEndpoint { + case ipv4(IPv4Endpoint) + case ipv6(IPv6Endpoint) +} + +/// Convenience methods for accessing endpoint fields +extension AnyIPEndpoint { + var ip: IPAddress { + switch self { + case .ipv4(let ipv4Endpoint): + return ipv4Endpoint.ip + case .ipv6(let ipv6Endpoint): + return ipv6Endpoint.ip + } + } + + var port: UInt16 { + switch self { + case .ipv4(let ipv4Endpoint): + return ipv4Endpoint.port + case .ipv6(let ipv6Endpoint): + return ipv6Endpoint.port + } + } +} diff --git a/ios/MullvadVPN/MullvadEndpoint.swift b/ios/MullvadVPN/MullvadEndpoint.swift index e61fea7475..3aacca9806 100644 --- a/ios/MullvadVPN/MullvadEndpoint.swift +++ b/ios/MullvadVPN/MullvadEndpoint.swift @@ -11,8 +11,8 @@ import Network /// Contains server data needed to connect to a single mullvad endpoint struct MullvadEndpoint { - let ipv4Relay: NWEndpoint - let ipv6Relay: NWEndpoint? + let ipv4Relay: IPv4Endpoint + let ipv6Relay: IPv6Endpoint? let ipv4Gateway: IPv4Address let ipv6Gateway: IPv6Address let publicKey: Data diff --git a/ios/MullvadVPN/MullvadVPN.entitlements b/ios/MullvadVPN/MullvadVPN.entitlements index d5ed5f9c41..a34daec96d 100644 --- a/ios/MullvadVPN/MullvadVPN.entitlements +++ b/ios/MullvadVPN/MullvadVPN.entitlements @@ -2,6 +2,10 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>com.apple.developer.networking.networkextension</key> + <array> + <string>packet-tunnel-provider</string> + </array> <key>com.apple.security.application-groups</key> <array> <string>group.net.mullvad.MullvadVPN</string> diff --git a/ios/MullvadVPN/PacketTunnelIpc.swift b/ios/MullvadVPN/PacketTunnelIpc.swift index 4da14d4051..ac194b5372 100644 --- a/ios/MullvadVPN/PacketTunnelIpc.swift +++ b/ios/MullvadVPN/PacketTunnelIpc.swift @@ -49,8 +49,8 @@ enum PacketTunnelIpcError: Error { /// A struct that holds the basic information regarding the tunnel connection struct TunnelConnectionInfo: Codable, Equatable { - let ipv4Relay: String - let ipv6Relay: String? + let ipv4Relay: IPv4Endpoint + let ipv6Relay: IPv6Endpoint? let hostname: String } diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift index 255502446c..2af9813fe8 100644 --- a/ios/MullvadVPN/RelaySelector.swift +++ b/ios/MullvadVPN/RelaySelector.swift @@ -112,11 +112,8 @@ struct RelaySelector { return nil } - let networkPort = NWEndpoint.Port(integerLiteral: randomPort) - let ipv4Endpoint = NWEndpoint.hostPort(host: .ipv4(randomRelay.ipv4AddrIn), port: networkPort) - let endpoint = MullvadEndpoint( - ipv4Relay: ipv4Endpoint, + ipv4Relay: IPv4Endpoint(ip: randomRelay.ipv4AddrIn, port: randomPort), ipv6Relay: nil, ipv4Gateway: randomTunnel.ipv4Gateway, ipv6Gateway: randomTunnel.ipv6Gateway, diff --git a/ios/PacketTunnel/AnyIPEndpoint+DNS64.swift b/ios/PacketTunnel/AnyIPEndpoint+DNS64.swift new file mode 100644 index 0000000000..ff45d25654 --- /dev/null +++ b/ios/PacketTunnel/AnyIPEndpoint+DNS64.swift @@ -0,0 +1,62 @@ +// +// AnyIPEndpoint+DNS64.swift +// PacketTunnel +// +// Created by pronebird on 24/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network +import os + +extension AnyIPEndpoint { + + /// Returns new `AnyIPEndpoint` resolved using DNS64 + func withReresolvedIP() -> Result<AnyIPEndpoint, Error> { + var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0)) + var hints = addrinfo( + ai_flags: 0, // We set this to zero so that we actually resolve this using DNS64 + ai_family: AF_UNSPEC, + ai_socktype: SOCK_DGRAM, + ai_protocol: IPPROTO_UDP, + ai_addrlen: 0, + ai_canonname: nil, + ai_addr: nil, + ai_next: nil) + + let err = getaddrinfo("\(self.ip)", "\(self.port)", &hints, &resultPointer) + if err != 0 || resultPointer == nil { + return .failure(NSError( + domain: NSPOSIXErrorDomain, + code: Int(err), + userInfo: [ + NSLocalizedDescriptionKey: String(cString: gai_strerror(err)) + ])) + } + + var resolvedAddress = self + let result = resultPointer!.pointee + if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size { + var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee + let addr = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size)) + + resolvedAddress = .ipv4(IPv4Endpoint(ip: addr!, port: self.port)) + } else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size { + var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee + let addr = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size)) + + resolvedAddress = .ipv6(IPv6Endpoint(ip: addr!, port: self.port)) + } + + freeaddrinfo(resultPointer) + + if "\(resolvedAddress.ip)" == "\(self.ip)" { + os_log(.debug, "DNS64: mapped %{public}s to itself", "\(resolvedAddress.ip)") + } else { + os_log(.debug, "DNS64: mapped %{public}s to %{public}s", "\(self.ip)", "\(resolvedAddress.ip)") + } + + return .success(resolvedAddress) + } +} diff --git a/ios/PacketTunnel/AnyIPEndpoint+Wireguard.swift b/ios/PacketTunnel/AnyIPEndpoint+Wireguard.swift new file mode 100644 index 0000000000..52a446a4ee --- /dev/null +++ b/ios/PacketTunnel/AnyIPEndpoint+Wireguard.swift @@ -0,0 +1,24 @@ +// +// AnyIPEndpoint+Wireguard.swift +// PacketTunnel +// +// Created by pronebird on 24/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network + +extension AnyIPEndpoint { + + /// String representation that supports IPv6-style formatting (i.e [::1]:80) used by Wireguard + var wireguardStringRepresentation: String { + switch self { + case .ipv4(let ipv4Endpoint): + return "\(ipv4Endpoint.ip):\(ipv4Endpoint.port)" + case .ipv6(let ipv6Endpoint): + return "[\(ipv6Endpoint.ip)]:\(ipv6Endpoint.port)" + } + } + +} diff --git a/ios/PacketTunnel/Info.plist b/ios/PacketTunnel/Info.plist index 368f1046e1..59c8676089 100644 --- a/ios/PacketTunnel/Info.plist +++ b/ios/PacketTunnel/Info.plist @@ -17,9 +17,9 @@ <key>CFBundlePackageType</key> <string>XPC!</string> <key>CFBundleShortVersionString</key> - <string>1.0</string> + <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> - <string>1</string> + <string>$(CURRENT_PROJECT_VERSION)</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/ios/PacketTunnel/PacketTunnel.entitlements b/ios/PacketTunnel/PacketTunnel.entitlements index a8a939a254..a34daec96d 100644 --- a/ios/PacketTunnel/PacketTunnel.entitlements +++ b/ios/PacketTunnel/PacketTunnel.entitlements @@ -2,9 +2,13 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>com.apple.security.application-groups</key> - <array> - <string>group.net.mullvad.MullvadVPN</string> - </array> + <key>com.apple.developer.networking.networkextension</key> + <array> + <string>packet-tunnel-provider</string> + </array> + <key>com.apple.security.application-groups</key> + <array> + <string>group.net.mullvad.MullvadVPN</string> + </array> </dict> </plist> diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 4b4fedf307..00a55a594d 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -6,24 +6,158 @@ // Copyright © 2019 Amagicom AB. All rights reserved. // +import Combine +import Foundation +import Network import NetworkExtension +import os + +enum PacketTunnelProviderError: Error { + /// Failure to read the relay cache + case readRelayCache(RelayCacheError) + + /// Failure to satisfy the relay constraint + case noRelaySatisfyingConstraint + + /// Missing the persistent keychain reference to the tunnel configuration + case missingKeychainConfigurationReference + + /// Failure to read the tunnel configuration from Keychain + case cannotReadTunnelConfiguration(TunnelConfigurationManagerError) + + /// 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 + + /// IPC handler failure + case ipcHandler(PacketTunnelIpcHandlerError) + + var localizedDescription: String { + switch self { + case .readRelayCache(let relayError): + return "Failure to read the relay cache: \(relayError.localizedDescription)" + + case .noRelaySatisfyingConstraint: + return "No relay satisfying the given constraint" + + case .missingKeychainConfigurationReference: + return "Invalid protocol configuration" + + case .cannotReadTunnelConfiguration(let readError): + return "Cannot read tunnel configuration: \(readError.localizedDescription)" + + case .setNetworkSettings(let systemError): + return "Failed to set network settings: \(systemError.localizedDescription)" + + case .tunnelDeviceNotFound: + return "Failed to find the tunnel device descriptor" + + case .startWireGuardBackend: + return "Failure to start the wireguard backend" + + case .ipcHandler(let ipcError): + return "Failure to handle the IPC request: \(ipcError.localizedDescription)" + } + } +} + +/// A wireguard events log +let wireguardLog = OSLog(subsystem: "net.mullvad.vpn.packet-tunnel", category: "Wireguard") + +/// A general tunnel provider log +let tunnelProviderLog = OSLog(subsystem: "net.mullvad.vpn.packet-tunnel", category: "Tunnel Provider") class PacketTunnelProvider: NEPacketTunnelProvider { + private var handle: Int32? + private var networkMonitor: NWPathMonitor? + private var tunnelInterfaceName: String? + private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator? + private var connectionInfo: TunnelConnectionInfo? + private let cancellableSet = CancellableSet() + + private var startStopTunnelSubscriber: AnyCancellable? + private var startedTunnel = false + + 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() + + self.configureLogger() + } + + deinit { + networkMonitor?.cancel() + } + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - // Add code here to start the process of connecting the tunnel. + 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") + + completionHandler(nil) + + case .failure(let error): + os_log(.error, log: tunnelProviderLog, "Failed to start the tunnel: %{public}s", error.localizedDescription) + + completionHandler(error) + } + }) } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - // Add code here to start the process of stopping the tunnel. - completionHandler() + os_log(.default, log: tunnelProviderLog, "Stop tunnel received.") + + startStopTunnelSubscriber = stopTunnel().sink(receiveCompletion: { (completion) in + os_log(.default, log: tunnelProviderLog, "Stopped the tunnel") + + completionHandler() + }) } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - // Add code here to handle the message. - if let handler = completionHandler { - handler(messageData) - } + PacketTunnelIpcHandler.decodeRequest(messageData: messageData) + .mapError { PacketTunnelProviderError.ipcHandler($0) } + .receive(on: executionQueue) + .flatMap { (request) -> AnyPublisher<AnyEncodable, PacketTunnelProviderError> in + os_log(.default, log: tunnelProviderLog, + "Received IPC request: %{public}s", "\(request)") + + switch request { + + case .reloadConfiguration: + return self.reloadTunnel() + .map { AnyEncodable(true) } + .eraseToAnyPublisher() + + case .tunnelInformation: + return Result.Publisher(AnyEncodable(self.connectionInfo)) + .eraseToAnyPublisher() + + } + }.flatMap({ (response) in + return PacketTunnelIpcHandler.encodeResponse(response: response) + .mapError { PacketTunnelProviderError.ipcHandler($0) } + }).autoDisposableSink(cancellableSet: cancellableSet, receiveCompletion: { (completion) in + if case .failure(let error) = completion { + os_log(.error, log: tunnelProviderLog, "Failed to handle the app message: %{public}s", error.localizedDescription) + completionHandler?(nil) + } + }, receiveValue: { (responseData) in + completionHandler?(responseData) + }) } override func sleep(completionHandler: @escaping () -> Void) { @@ -34,4 +168,293 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func wake() { // Add code here to wake up. } + + 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) + } + } + + private func startTunnel() -> AnyPublisher<(), PacketTunnelProviderError> { + MutuallyExclusive( + exclusivityQueue: exclusivityQueue, + executionQueue: executionQueue + ) { () -> AnyPublisher<(), PacketTunnelProviderError> in + os_log(.default, log: tunnelProviderLog, "Starting the tunnel") + + self.startedTunnel = true + + return self.setupTunnelNetworkSettings() + .flatMap { self.startWireguard().publisher } + .eraseToAnyPublisher() + }.eraseToAnyPublisher() + } + + private func stopTunnel() -> AnyPublisher<(), Never> { + MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { () -> Just<()> in + os_log(.default, log: tunnelProviderLog, "Stopping the tunnel") + + self.startedTunnel = false + + self.networkMonitor?.cancel() + self.networkMonitor = nil + + if let handle = self.handle { + wgTurnOff(handle) + } + + self.handle = nil + + return Just(()) + }.eraseToAnyPublisher() + } + + private func reloadTunnel() -> AnyPublisher<(), PacketTunnelProviderError> { + MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { + () -> AnyPublisher<(), PacketTunnelProviderError> in + guard self.startedTunnel else { + os_log(.default, log: tunnelProviderLog, + "Ignore reloading tunnel settings. The tunnel has not started yet.") + + 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 } + + os_log(.default, log: tunnelProviderLog, "Replace Wireguard endpoints") + + _ = self.packetTunnelSettingsGenerator? + .wireguardConfigurationForChangingRelays() + .withGoString { wgSetConfig(handle, $0) } + + 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() + }.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) + + return result.endpoint + }) + .flatMap({ (endpoint) -> AnyPublisher<(), PacketTunnelProviderError> in + let settingsGenerator = PacketTunnelSettingsGenerator( + mullvadEndpoint: endpoint, + tunnelConfiguration: tunnelConfiguration) + + let networkSettings = settingsGenerator.networkSettings() + + self.packetTunnelSettingsGenerator = settingsGenerator + + os_log(.default, log: tunnelProviderLog, "Set tunnel network settings") + + return self.setTunnelNetworkSettings(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() + }) + }.eraseToAnyPublisher() + } + + private func readTunnelConfigurationFromKeychain() -> Result<TunnelConfiguration, PacketTunnelProviderError> { + guard let keychainReference = (protocolConfiguration as? NETunnelProviderProtocol)?.passwordReference else { + return .failure(.missingKeychainConfigurationReference) + } + + return TunnelConfigurationManager.load(persistentKeychainRef: keychainReference) + .mapError { PacketTunnelProviderError.cannotReadTunnelConfiguration($0) } + } + + private func selectRelayEndpoint(tunnelConfiguration: TunnelConfiguration) -> AnyPublisher<RelaySelectorResult, PacketTunnelProviderError> { + return RelaySelector.loadedFromRelayCache() + .mapError { PacketTunnelProviderError.readRelayCache($0) } + .flatMap { (relaySelector) -> Result<RelaySelectorResult, PacketTunnelProviderError>.Publisher in + return relaySelector + .evaluate(with: tunnelConfiguration.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 + + var asOSLogType: OSLogType { + switch self { + case .debug: + return .debug + case .info: + return .info + case .error: + return .error + } + } +} + +extension NETunnelProvider { + + func setTunnelNetworkSettings(_ tunnelNetworkSettings: NETunnelNetworkSettings?) -> Future<(), Error> { + return Future { (fulfill) in + self.setTunnelNetworkSettings(tunnelNetworkSettings) { (error) in + if let error = error { + fulfill(.failure(error)) + } else { + fulfill(.success(())) + } + } + } + } + } diff --git a/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift new file mode 100644 index 0000000000..a431eba5de --- /dev/null +++ b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift @@ -0,0 +1,156 @@ +// +// PacketTunnelSettingsGenerator.swift +// PacketTunnel +// +// Created by pronebird on 13/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network +import NetworkExtension +import os + +struct PacketTunnelSettingsGenerator { + let mullvadEndpoint: MullvadEndpoint + let tunnelConfiguration: TunnelConfiguration + + func networkSettings() -> NEPacketTunnelNetworkSettings { + let tunnelRemoteAddress = "\(mullvadEndpoint.ipv4Relay.ip)" + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelRemoteAddress) + + networkSettings.mtu = 1280 + networkSettings.dnsSettings = dnsSettings() + networkSettings.ipv4Settings = ipv4Settings() + networkSettings.ipv6Settings = ipv6Settings() + + 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) } + + let dnsSettings = NEDNSSettings(servers: serverAddresses) + + // All DNS queries must first go through the tunnel's DNS + dnsSettings.matchDomains = [""] + + return dnsSettings + } + + private func ipv4Settings() -> NEIPv4Settings { + let interfaceAddresses = tunnelConfiguration.interface.addresses + let ipv4AddressRanges = interfaceAddresses.filter { $0.address is IPv4Address } + + let ipv4Settings = NEIPv4Settings( + addresses: ipv4AddressRanges.map { "\($0.address)" }, + subnetMasks: ipv4AddressRanges.map { self.ipv4SubnetMaskString(of: $0) }) + + ipv4Settings.includedRoutes = [ + NEIPv4Route.default() // 0.0.0.0/0 + ] + + let relayAddressRange = IPAddressRange(address: mullvadEndpoint.ipv4Relay.ip, networkPrefixLength: 32) + + ipv4Settings.excludedRoutes = [ + NEIPv4Route( + destinationAddress: "\(relayAddressRange.address)", + subnetMask: ipv4SubnetMaskString(of: relayAddressRange)) + ] + + return ipv4Settings + } + + private func ipv6Settings() -> NEIPv6Settings { + let interfaceAddresses = tunnelConfiguration.interface.addresses + let ipv6AddressRanges = interfaceAddresses.filter { $0.address is IPv6Address } + + let ipv6Settings = NEIPv6Settings( + addresses: ipv6AddressRanges.map { "\($0.address)" }, + networkPrefixLengths: ipv6AddressRanges.map { NSNumber(value: $0.networkPrefixLength) } + ) + + ipv6Settings.includedRoutes = [ + NEIPv6Route.default() // ::0 + ] + + if let ipv6Relay = mullvadEndpoint.ipv6Relay { + ipv6Settings.excludedRoutes = [ + NEIPv6Route(destinationAddress: "\(ipv6Relay.ip)", networkPrefixLength: 128) + ] + } + + return ipv6Settings + } + + private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String { + let length: UInt8 = addressRange.networkPrefixLength + assert(length <= 32) + var octets: [UInt8] = [0, 0, 0, 0] + let subnetMask: UInt32 = length > 0 ? ~UInt32(0) << (32 - length) : UInt32(0) + octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24) + octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16) + octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8) + octets[3] = UInt8(truncatingIfNeeded: subnetMask) + return octets.map { String($0) }.joined(separator: ".") + } +} diff --git a/ios/PacketTunnel/WireguardCommand.swift b/ios/PacketTunnel/WireguardCommand.swift new file mode 100644 index 0000000000..45993586b0 --- /dev/null +++ b/ios/PacketTunnel/WireguardCommand.swift @@ -0,0 +1,63 @@ +// +// WireguardCommand.swift +// PacketTunnel +// +// Created by pronebird on 24/06/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation +import Network + +struct WireguardPeer { + let endpoint: AnyIPEndpoint + let publicKey: Data +} + +enum WireguardCommand { + case privateKey(WireguardPrivateKey) + case listenPort(UInt16) + case replacePeers + case peer(WireguardPeer) + case replaceAllowedIPs + case allowedIP(IPAddressRange) +} + +extension WireguardCommand { + + func toRawWireguardCommand() -> String { + switch self { + case .privateKey(let privateKey): + let keyString = privateKey.rawRepresentation.hexEncodedString() + + return "private_key=\(keyString)" + + case .listenPort(let port): + return "listen_port=\(port)" + + case .replacePeers: + return "replace_peers=true" + + case .peer(let peer): + let keyString = peer.publicKey.hexEncodedString() + let endpointString = peer.endpoint.wireguardStringRepresentation + + return ["public_key=\(keyString)", "endpoint=\(endpointString)"] + .joined(separator: "\n") + + case .replaceAllowedIPs: + return "replace_allowed_ips=true" + + case .allowedIP(let ipAddressRange): + return "allowed_ip=\(ipAddressRange)" + } + } + +} + +extension Array where Element == WireguardCommand { + func toWireguardConfig() -> String { + map { $0.toRawWireguardCommand() } + .joined(separator: "\n") + } +} |
