aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardKit/WireGuardAdapter.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/WireGuardKit/WireGuardAdapter.swift')
-rw-r--r--Sources/WireGuardKit/WireGuardAdapter.swift349
1 files changed, 349 insertions, 0 deletions
diff --git a/Sources/WireGuardKit/WireGuardAdapter.swift b/Sources/WireGuardKit/WireGuardAdapter.swift
new file mode 100644
index 0000000..ef644bd
--- /dev/null
+++ b/Sources/WireGuardKit/WireGuardAdapter.swift
@@ -0,0 +1,349 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import NetworkExtension
+import WireGuardKitGo
+
+public enum WireGuardAdapterError: Error {
+ /// Failure to locate tunnel file descriptor.
+ case cannotLocateTunnelFileDescriptor
+
+ /// Failure to perform an operation in such state.
+ case invalidState
+
+ /// Failure to resolve endpoints.
+ case dnsResolution([DNSResolutionError])
+
+ /// Failure to set network settings.
+ case setNetworkSettings(Error)
+
+ /// Timeout when calling to set network settings.
+ case setNetworkSettingsTimeout
+
+ /// Failure to start WireGuard backend.
+ case startWireGuardBackend(Int32)
+}
+
+public class WireGuardAdapter {
+ public typealias LogHandler = (WireGuardLogLevel, String) -> Void
+
+ /// Network routes monitor.
+ private var networkMonitor: NWPathMonitor?
+
+ /// Packet tunnel provider.
+ private weak var packetTunnelProvider: NEPacketTunnelProvider?
+
+ /// Log handler closure.
+ private let logHandler: LogHandler
+
+ /// WireGuard internal handle returned by `wgTurnOn` that's used to associate the calls
+ /// with the specific WireGuard tunnel.
+ private var wireguardHandle: Int32?
+
+ /// Private queue used to synchronize access to `WireGuardAdapter` members.
+ private let workQueue = DispatchQueue(label: "WireGuardAdapterWorkQueue")
+
+ /// Packet tunnel settings generator.
+ private var settingsGenerator: PacketTunnelSettingsGenerator?
+
+ /// Tunnel device file descriptor.
+ private var tunnelFileDescriptor: Int32? {
+ return self.packetTunnelProvider?.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32
+ }
+
+ /// Returns a WireGuard version.
+ class var version: String {
+ return String(cString: wgVersion())
+ }
+
+ /// Returns the tunnel device interface name, or nil on error.
+ /// - Returns: String.
+ public var interfaceName: String? {
+ guard let tunnelFileDescriptor = self.tunnelFileDescriptor else { return nil }
+
+ 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(
+ tunnelFileDescriptor,
+ 2 /* SYSPROTO_CONTROL */,
+ 2 /* UTUN_OPT_IFNAME */,
+ baseAddress,
+ &ifnameSize)
+
+ if result == 0 {
+ return String(cString: baseAddress)
+ } else {
+ return nil
+ }
+ }
+ }
+
+ // MARK: - Initialization
+
+ /// Designated initializer.
+ /// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored
+ /// as a weak reference.
+ /// - Parameter logHandler: a log handler closure.
+ public init(with packetTunnelProvider: NEPacketTunnelProvider, logHandler: @escaping LogHandler) {
+ self.packetTunnelProvider = packetTunnelProvider
+ self.logHandler = logHandler
+
+ setupLogHandler()
+ }
+
+ deinit {
+ // Force remove logger to make sure that no further calls to the instance of this class
+ // can happen after deallocation.
+ wgSetLogger(nil, nil)
+
+ // Cancel network monitor
+ networkMonitor?.cancel()
+
+ // Shutdown the tunnel
+ if let handle = self.wireguardHandle {
+ wgTurnOff(handle)
+ }
+ }
+
+ // MARK: - Public methods
+
+ /// Returns a runtime configuration from WireGuard.
+ /// - Parameter completionHandler: completion handler.
+ public func getRuntimeConfiguration(completionHandler: @escaping (String?) -> Void) {
+ workQueue.async {
+ guard let handle = self.wireguardHandle else {
+ completionHandler(nil)
+ return
+ }
+
+ if let settings = wgGetConfig(handle) {
+ completionHandler(String(cString: settings))
+ free(settings)
+ } else {
+ completionHandler(nil)
+ }
+ }
+ }
+
+ /// Start the tunnel tunnel.
+ /// - Parameters:
+ /// - tunnelConfiguration: tunnel configuration.
+ /// - completionHandler: completion handler.
+ public func start(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
+ workQueue.async {
+ guard self.wireguardHandle == nil else {
+ completionHandler(.invalidState)
+ return
+ }
+
+ guard let tunnelFileDescriptor = self.tunnelFileDescriptor else {
+ completionHandler(.cannotLocateTunnelFileDescriptor)
+ return
+ }
+
+ #if os(macOS)
+ wgEnableRoaming(true)
+ #endif
+
+ let networkMonitor = NWPathMonitor()
+ networkMonitor.pathUpdateHandler = { [weak self] path in
+ self?.didReceivePathUpdate(path: path)
+ }
+
+ networkMonitor.start(queue: self.workQueue)
+ self.networkMonitor = networkMonitor
+
+ self.updateNetworkSettings(tunnelConfiguration: tunnelConfiguration) { settingsGenerator, error in
+ if let error = error {
+ completionHandler(error)
+ } else {
+ var returnError: WireGuardAdapterError?
+ let handle = wgTurnOn(settingsGenerator!.uapiConfiguration(), tunnelFileDescriptor)
+
+ if handle >= 0 {
+ self.wireguardHandle = handle
+ } else {
+ returnError = .startWireGuardBackend(handle)
+ }
+
+ completionHandler(returnError)
+ }
+ }
+ }
+ }
+
+ /// Stop the tunnel.
+ /// - Parameter completionHandler: completion handler.
+ public func stop(completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
+ workQueue.async {
+ guard let handle = self.wireguardHandle else {
+ completionHandler(.invalidState)
+ return
+ }
+
+ self.networkMonitor?.cancel()
+ self.networkMonitor = nil
+
+ wgTurnOff(handle)
+ self.wireguardHandle = nil
+
+ completionHandler(nil)
+ }
+ }
+
+ /// Update runtime configuration.
+ /// - Parameters:
+ /// - tunnelConfiguration: tunnel configuration.
+ /// - completionHandler: completion handler.
+ public func update(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (WireGuardAdapterError?) -> Void) {
+ workQueue.async {
+ guard let handle = self.wireguardHandle else {
+ completionHandler(.invalidState)
+ return
+ }
+
+ // Tell the system that the tunnel is going to reconnect using new WireGuard
+ // configuration.
+ // This will broadcast the `NEVPNStatusDidChange` notification to the GUI process.
+ self.packetTunnelProvider?.reasserting = true
+
+ self.updateNetworkSettings(tunnelConfiguration: tunnelConfiguration) { settingsGenerator, error in
+ if let error = error {
+ completionHandler(error)
+ } else {
+ wgSetConfig(handle, settingsGenerator!.uapiConfiguration())
+ completionHandler(nil)
+ }
+
+ self.packetTunnelProvider?.reasserting = false
+ }
+ }
+ }
+
+ // MARK: - Private methods
+
+ /// Setup WireGuard log handler.
+ private func setupLogHandler() {
+ let context = Unmanaged.passUnretained(self).toOpaque()
+ wgSetLogger(context) { context, logLevel, message in
+ guard let context = context, let message = message else { return }
+
+ let unretainedSelf = Unmanaged<WireGuardAdapter>.fromOpaque(context)
+ .takeUnretainedValue()
+
+ let swiftString = String(cString: message).trimmingCharacters(in: .newlines)
+ let tunnelLogLevel = WireGuardLogLevel(rawValue: logLevel) ?? .debug
+
+ unretainedSelf.logHandler(tunnelLogLevel, swiftString)
+ }
+ }
+
+ /// Resolve endpoints and update network configuration.
+ /// - Parameters:
+ /// - tunnelConfiguration: tunnel configuration
+ /// - completionHandler: completion handler
+ private func updateNetworkSettings(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (PacketTunnelSettingsGenerator?, WireGuardAdapterError?) -> Void) {
+ let resolvedEndpoints: [Endpoint?]
+
+ let resolvePeersResult = Result { try self.resolvePeers(for: tunnelConfiguration) }
+ .mapError { error -> WireGuardAdapterError in
+ // swiftlint:disable:next force_cast
+ return error as! WireGuardAdapterError
+ }
+
+ switch resolvePeersResult {
+ case .success(let endpoints):
+ resolvedEndpoints = endpoints
+ case .failure(let error):
+ completionHandler(nil, error)
+ return
+ }
+
+ let settingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
+ let networkSettings = settingsGenerator.generateNetworkSettings()
+
+ var systemError: Error?
+ let condition = NSCondition()
+
+ // Activate the condition
+ condition.lock()
+ defer { condition.unlock() }
+
+ self.packetTunnelProvider?.setTunnelNetworkSettings(networkSettings) { error in
+ systemError = error
+ condition.signal()
+ }
+
+ // Packet tunnel's `setTunnelNetworkSettings` times out in certain
+ // scenarios & never calls the given callback.
+ let setTunnelNetworkSettingsTimeout: TimeInterval = 5 // seconds
+
+ if condition.wait(until: Date().addingTimeInterval(setTunnelNetworkSettingsTimeout)) {
+ let returnError = systemError.map { WireGuardAdapterError.setNetworkSettings($0) }
+
+ // Only assign `settingsGenerator` when `setTunnelNetworkSettings` succeeded.
+ if returnError == nil {
+ self.settingsGenerator = settingsGenerator
+ }
+
+ completionHandler(settingsGenerator, returnError)
+ } else {
+ completionHandler(nil, .setNetworkSettingsTimeout)
+ }
+ }
+
+ /// Resolve peers of the given tunnel configuration.
+ /// - Parameter tunnelConfiguration: tunnel configuration.
+ /// - Throws: an error of type `WireGuardAdapterError`.
+ /// - Returns: The list of resolved endpoints.
+ private func resolvePeers(for tunnelConfiguration: TunnelConfiguration) throws -> [Endpoint?] {
+ let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
+ let resolutionResults = DNSResolver.resolveSync(endpoints: endpoints)
+ let resolutionErrors = resolutionResults.compactMap { result -> DNSResolutionError? in
+ if case .failure(let error) = result {
+ return error
+ } else {
+ return nil
+ }
+ }
+ assert(endpoints.count == resolutionResults.count)
+ guard resolutionErrors.isEmpty else {
+ throw WireGuardAdapterError.dnsResolution(resolutionErrors)
+ }
+
+ let resolvedEndpoints = resolutionResults.map { result -> Endpoint? in
+ // swiftlint:disable:next force_try
+ return try! result?.get()
+ }
+
+ return resolvedEndpoints
+ }
+
+ /// Helper method used by network path monitor.
+ /// - Parameter path: new network path
+ private func didReceivePathUpdate(path: Network.NWPath) {
+ guard let handle = self.wireguardHandle else { return }
+
+ self.logHandler(.debug, "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
+
+ #if os(iOS)
+ if let settingsGenerator = self.settingsGenerator {
+ wgSetConfig(handle, settingsGenerator.endpointUapiConfiguration())
+ }
+ #endif
+
+ wgBumpSockets(handle)
+ }
+}
+
+/// A enum describing WireGuard log levels defined in `api-ios.go`.
+public enum WireGuardLogLevel: Int32 {
+ case debug = 0
+ case info = 1
+ case error = 2
+}