diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-08-28 16:29:37 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-08-28 21:54:55 +0200 |
| commit | a03ff94bda3912b127bcbf00b57fa7a681e11809 (patch) | |
| tree | 99813eaa43e9fb01e92604b6444dfa4100282b10 | |
| parent | 38502c179423ffc6430e898dfd2d4739ba4893e1 (diff) | |
| download | mullvadvpn-a03ff94bda3912b127bcbf00b57fa7a681e11809.tar.xz mullvadvpn-a03ff94bda3912b127bcbf00b57fa7a681e11809.zip | |
Turn off WireGuard when no active network interfaces available.
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardDevice.swift | 153 |
2 files changed, 122 insertions, 32 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 9e0e294233..5e28ab1dcc 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -39,6 +39,7 @@ Line wrap the file at 100 chars. Th ### Changed - Remove the public WireGuard inside the VPN tunnel during the log out, if VPN is active at that time. +- Turn off WireGuard when no active network interfaces available. ## [2020.3] - 2020-06-12 ### Added diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift index c24cd3dab0..5fcbe2e057 100644 --- a/ios/PacketTunnel/WireguardDevice.swift +++ b/ios/PacketTunnel/WireguardDevice.swift @@ -21,6 +21,10 @@ class WireguardDevice { /// A failure to obtain the tunnel device file descriptor case cannotLocateSocketDescriptor + /// A failure to duplicate the socket descriptor. + /// The associated value contains the `errno` from a syscall to `dup` + case cannotDuplicateSocketDescriptor(Int32) + /// A failure to start the Wireguard backend case start(Int32) @@ -36,7 +40,9 @@ class WireguardDevice { var errorDescription: String? { switch self { case .cannotLocateSocketDescriptor: - return "Unable to locate the file descriptor for socket." + return "Cannot locate the socket file descriptor." + case .cannotDuplicateSocketDescriptor(let posixErrorCode): + return "Cannot duplicate the socket file descriptor. Errno: \(posixErrorCode)" case .start(let wgErrorCode): return "Failed to start Wireguard. Return code: \(wgErrorCode)" case .notStarted: @@ -75,8 +81,8 @@ class WireguardDevice { /// Network routes monitor private var networkMonitor: NWPathMonitor? - /// A tunnel device descriptor - private let tunFd: Int32 + /// A tunnel device source socket file descriptor + private let tunnelFileDescriptor: Int32 /// A wireguard internal handle returned by `wgTurnOn` that's used to associate the calls /// with the specific Wireguard tunnel. @@ -85,6 +91,12 @@ class WireguardDevice { /// Active configuration private var configuration: WireguardConfiguration? + /// A flag that indicates that the device has started + private var isStarted = false + + /// A flag that indicates whether the last known network path was satisfied + private var isPathSatisfied = true + /// Returns a Wireguard version class var version: String { String(cString: wgVersion()) @@ -116,15 +128,15 @@ class WireguardDevice { /// 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)) + return .success(.init(tunnelFileDescriptor: fd)) } else { return .failure(.cannotLocateSocketDescriptor) } } /// Private initializer - private init(tunFd: Int32) { - self.tunFd = tunFd + private init(tunnelFileDescriptor: Int32) { + self.tunnelFileDescriptor = tunnelFileDescriptor } deinit { @@ -135,38 +147,39 @@ class WireguardDevice { func start(configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { - guard self.wireguardHandle == nil else { + guard !self.isStarted else { completionHandler(.failure(.alreadyStarted)) return } + assert(self.wireguardHandle == nil) + let resolvedConfiguration = self.resolveConfiguration(configuration) - let handle = resolvedConfiguration - .uapiConfiguration() - .toRawWireguardConfigString() - .withCString { wgTurnOn($0, self.tunFd) } - if handle >= 0 { - self.wireguardHandle = handle + switch self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) { + case .success: + self.isStarted = true + self.isPathSatisfied = true self.configuration = configuration self.startNetworkMonitor() completionHandler(.success(())) - } else { - completionHandler(.failure(.start(handle))) + + case .failure(let error): + completionHandler(.failure(error)) } } } func stop(completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { - if let handle = self.wireguardHandle { + if self.isStarted { self.networkMonitor?.cancel() self.networkMonitor = nil - wgTurnOff(handle) - self.wireguardHandle = nil + self.stopWireguardBackend() + self.isStarted = false completionHandler(.success(())) } else { @@ -177,11 +190,13 @@ class WireguardDevice { func setConfiguration(_ newConfiguration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { - if let handle = self.wireguardHandle { - let resolvedConfiguration = self.resolveConfiguration(newConfiguration) - let commands = resolvedConfiguration.uapiConfiguration() + if self.isStarted { + if let handle = self.wireguardHandle { + let resolvedConfiguration = self.resolveConfiguration(newConfiguration) + let commands = resolvedConfiguration.uapiConfiguration() - Self.setWireguardConfig(handle: handle, commands: commands) + Self.setWireguardConfig(handle: handle, commands: commands) + } self.configuration = newConfiguration @@ -200,7 +215,7 @@ class WireguardDevice { var ifnameSize = socklen_t(IFNAMSIZ) let result = getsockopt( - self.tunFd, + self.tunnelFileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, baseAddress, @@ -216,6 +231,42 @@ class WireguardDevice { // MARK: - Private methods + private func startWireguardBackend(resolvedConfiguration: WireguardConfiguration) -> Result<(), Error> { + assert(self.wireguardHandle == nil) + + // Duplicate the tunnel file descriptor to prevent `wgTurnOff` from closing it + let duplicateFileDescriptor = dup(self.tunnelFileDescriptor) + if duplicateFileDescriptor == -1 { + return .failure(.cannotDuplicateSocketDescriptor(errno)) + } + + let handle = resolvedConfiguration + .uapiConfiguration() + .toRawWireguardConfigString() + .withCString { wgTurnOn($0, duplicateFileDescriptor) } + + if handle >= 0 { + self.wireguardHandle = handle + + return .success(()) + } else { + // `wgTurnOn` does not cover all of the code paths and may leave the file descriptor + // open on failure + if close(duplicateFileDescriptor) == -1 { + self.logger.warning("Failed to close the duplicate tunnel file descriptor. Error: \(errno)") + } + + return .failure(.start(handle)) + } + } + + private func stopWireguardBackend() { + guard let handle = self.wireguardHandle else { return } + + wgTurnOff(handle) + self.wireguardHandle = nil + } + private class func setWireguardConfig(handle: Int32, commands: [WireguardCommand]) { // Ignore empty payloads guard !commands.isEmpty else { return } @@ -283,20 +334,44 @@ class WireguardDevice { private func didReceiveNetworkPathUpdate(path: Network.NWPath) { workQueue.async { - guard let handle = self.wireguardHandle else { return } + guard self.isStarted else { return } + + self.logger.info("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).") + + let oldPathSatisfied = self.isPathSatisfied + let newPathSatisfied = path.status.isSatisfiable + + self.isPathSatisfied = newPathSatisfied - self.logger.debug("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).") + switch (oldPathSatisfied, newPathSatisfied) { + case (true, false): + self.logger.info("Stop wireguard backend") + self.stopWireguardBackend() + + case (false, true), (true, true): + guard let currentConfiguration = self.configuration else { return } + + self.logger.info("Re-resolve endpoints") - // Re-resolve endpoints on network changes - if let currentConfiguration = self.configuration { let resolvedConfiguration = self.resolveConfiguration(currentConfiguration) - let commands = resolvedConfiguration.endpointUapiConfiguration() - Self.setWireguardConfig(handle: handle, commands: commands) - } + if let handle = self.wireguardHandle { + let commands = resolvedConfiguration.endpointUapiConfiguration() + Self.setWireguardConfig(handle: handle, commands: commands) + + wgBumpSockets(handle) + } else { + self.logger.info("Start wireguard backend") - // Tell Wireguard to re-open sockets and bind them to the new network interface - wgBumpSockets(handle) + if case .failure(let error) = self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) { + self.logger.error(chainedError: error, message: "Failed to turn on WireGuard") + } + } + + case (false, false): + // No-op: device remains offline + break + } } } } @@ -319,6 +394,20 @@ enum WireguardLogLevel: Int32 { } } +private extension Network.NWPath.Status { + /// Returns `true` if the path is potentially satisfiable + var isSatisfiable: Bool { + switch self { + case .requiresConnection, .satisfied: + return true + case .unsatisfied: + return false + @unknown default: + return true + } + } +} + extension Network.NWPath.Status: CustomDebugStringConvertible { public var debugDescription: String { var output = "NWPath.Status." |
