summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-12-09 16:23:35 +0100
committerAndrej Mihajlov <and@mullvad.net>2019-12-09 16:23:35 +0100
commit8f49b59331e9168aebd6085f136303be7d5f0502 (patch)
treebe6731b16e7e1dc500d97e16896394deecd57ea8
parentbe89341733b58f2e992141fe99713e4d6e4ba7fd (diff)
parent9c4bd3cdacc5e7ac32f99e4334500e82b606e20a (diff)
downloadmullvadvpn-8f49b59331e9168aebd6085f136303be7d5f0502.tar.xz
mullvadvpn-8f49b59331e9168aebd6085f136303be7d5f0502.zip
Merge branch 'add-packet-tunnel-ios'
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme96
-rw-r--r--ios/MullvadVPN/AutoDisposableSink.swift67
-rw-r--r--ios/MullvadVPN/Data+HexCoding.swift21
-rw-r--r--ios/MullvadVPN/IPEndpoint.swift63
-rw-r--r--ios/MullvadVPN/MullvadEndpoint.swift4
-rw-r--r--ios/MullvadVPN/MullvadVPN.entitlements4
-rw-r--r--ios/MullvadVPN/PacketTunnelIpc.swift4
-rw-r--r--ios/MullvadVPN/RelaySelector.swift5
-rw-r--r--ios/PacketTunnel/AnyIPEndpoint+DNS64.swift62
-rw-r--r--ios/PacketTunnel/AnyIPEndpoint+Wireguard.swift24
-rw-r--r--ios/PacketTunnel/Info.plist4
-rw-r--r--ios/PacketTunnel/PacketTunnel.entitlements12
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift437
-rw-r--r--ios/PacketTunnel/PacketTunnelSettingsGenerator.swift156
-rw-r--r--ios/PacketTunnel/WireguardCommand.swift63
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")
+ }
+}