summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-12-20 16:30:26 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-01-03 11:39:21 +0100
commit3a10abba95512f30df021260c68165bc58a770af (patch)
tree882735e7db3cf0db8a92b453a204c2a861ff0eb8
parent04c26a00c0fc030f523d147fb1471001a52d8923 (diff)
downloadmullvadvpn-3a10abba95512f30df021260c68165bc58a770af.tar.xz
mullvadvpn-3a10abba95512f30df021260c68165bc58a770af.zip
Move WireGuard related routine to WireguardDevice
-rw-r--r--ios/MullvadVPN/IPEndpoint.swift17
-rw-r--r--ios/MullvadVPN/MullvadEndpoint.swift2
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift406
-rw-r--r--ios/PacketTunnel/PacketTunnelSettingsGenerator.swift58
-rw-r--r--ios/PacketTunnel/WireguardCommand.swift30
-rw-r--r--ios/PacketTunnel/WireguardConfiguration.swift86
-rw-r--r--ios/PacketTunnel/WireguardDevice.swift343
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
+ }
+}