Skip to content

Commit

Permalink
Prevent iOS from stopping the tunnel if it remains in connecting stat…
Browse files Browse the repository at this point in the history
…e for too long
  • Loading branch information
Jon Petersson committed Oct 19, 2023
1 parent 58b09e8 commit b7d99c6
Show file tree
Hide file tree
Showing 14 changed files with 79 additions and 200 deletions.
8 changes: 0 additions & 8 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,6 @@
58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7A46F2A8649ED0060C66F /* PingerTests.swift */; };
58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */; };
58C7AF122ABD8480007EDD7A /* TunnelProviderReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2A7290182B000EB5EBA /* TunnelProviderReply.swift */; };
58C7AF132ABD8480007EDD7A /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */; };
58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */; };
58C7AF172ABD84AA007EDD7A /* ProxyURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */; };
58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */; };
Expand Down Expand Up @@ -1268,7 +1266,6 @@
585CA70E25F8C44600B47C62 /* UIMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMetrics.swift; sourceTree = "<group>"; };
585DA87626B024A600B8C587 /* CachedRelays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedRelays.swift; sourceTree = "<group>"; };
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProviderMessage.swift; sourceTree = "<group>"; };
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; };
585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStoreReceiptOperation.swift; sourceTree = "<group>"; };
58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryInAppNotificationProvider.swift; sourceTree = "<group>"; };
586168682976F6BD00EF8598 /* DisplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayError.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1358,7 +1355,6 @@
5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLResponse.swift; sourceTree = "<group>"; };
5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayLocation.swift; sourceTree = "<group>"; };
5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraint.swift; sourceTree = "<group>"; };
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelRelay.swift; sourceTree = "<group>"; };
589A455228E094B300565204 /* OperationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OperationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
589C6A7A2A45ACCA00DAD3EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
589C6A7B2A45AE0100DAD3EF /* TunnelObfuscation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TunnelObfuscation.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2599,8 +2595,6 @@
children = (
7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */,
587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */,
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */,
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */,
5898D2A7290182B000EB5EBA /* TunnelProviderReply.swift */,
);
Expand Down Expand Up @@ -4206,7 +4200,6 @@
58C7AF112ABD8480007EDD7A /* TunnelProviderMessage.swift in Sources */,
58C7AF162ABD84A8007EDD7A /* URLRequestProxy.swift in Sources */,
58FE25D72AA72A8F003D1918 /* State.swift in Sources */,
58C7AF132ABD8480007EDD7A /* PacketTunnelStatus.swift in Sources */,
58C7A4592A863FB90060C66F /* WgStats.swift in Sources */,
7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */,
7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */,
Expand All @@ -4223,7 +4216,6 @@
58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */,
583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */,
58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */,
58C7AF152ABD8480007EDD7A /* PacketTunnelRelay.swift in Sources */,
58FE25D92AA72A8F003D1918 /* AutoCancellingTask.swift in Sources */,
58FE25E12AA72A9B003D1918 /* SettingsReaderProtocol.swift in Sources */,
58C7A4582A863FB90060C66F /* TunnelMonitorProtocol.swift in Sources */,
Expand Down
77 changes: 23 additions & 54 deletions ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,21 @@ class MapConnectionStatusOperation: AsyncOperation {
let tunnelState = interactor.tunnelStatus.state

switch connectionStatus {
case .connecting:
handleConnectingState(tunnelState, tunnel)
return

case .reasserting:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .reconnecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
}
}
return

case .connected:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connected($0) }
} else {
return .waitingForConnectivity(.noConnection)
case .connecting, .reasserting, .connected:
fetchTunnelStatus(tunnel: tunnel) { observedState in
switch observedState {
case let .connected(connectionState):
return connectionState.isNetworkReachable
? .connected(connectionState.selectedRelay)
: .waitingForConnectivity(.noConnection)
case let .connecting(connectionState):
return connectionState.isNetworkReachable
? .connecting(connectionState.selectedRelay)
: .waitingForConnectivity(.noConnection)
case let .error(blockedState):
return .error(blockedState.reason)
default:
return .none
}
}
return
Expand All @@ -78,7 +69,7 @@ class MapConnectionStatusOperation: AsyncOperation {
handleDisconnectedState(tunnelState)

case .disconnecting:
handleDisconnectionState(tunnelState)
handleDisconnectingState(tunnelState)

case .invalid:
setTunnelDisconnectedStatus()
Expand All @@ -94,38 +85,16 @@ class MapConnectionStatusOperation: AsyncOperation {
request?.cancel()
}

private func handleConnectingState(_ tunnelState: TunnelState, _ tunnel: any TunnelProtocol) {
switch tunnelState {
case .connecting:
break

default:
interactor.updateTunnelStatus { tunnelStatus in
tunnelStatus.state = .connecting(nil)
}
}

fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
}
}
}

private func handleDisconnectionState(_ tunnelState: TunnelState) {
private func handleDisconnectingState(_ tunnelState: TunnelState) {
switch tunnelState {
case .disconnecting:
break
default:
interactor.updateTunnelStatus { tunnelStatus in
let packetTunnelStatus = tunnelStatus.packetTunnelStatus
let isNetworkReachable = tunnelStatus.observedState.connectionState?.isNetworkReachable ?? false

tunnelStatus = TunnelStatus()
tunnelStatus.state = packetTunnelStatus.isNetworkReachable
tunnelStatus.state = isNetworkReachable
? .disconnecting(.nothing)
: .waitingForConnectivity(.noNetwork)
}
Expand Down Expand Up @@ -161,17 +130,17 @@ class MapConnectionStatusOperation: AsyncOperation {

private func fetchTunnelStatus(
tunnel: any TunnelProtocol,
mapToState: @escaping (PacketTunnelStatus) -> TunnelState?
mapToState: @escaping (ObservedState) -> TunnelState?
) {
request = tunnel.getTunnelStatus { [weak self] completion in
guard let self else { return }

dispatchQueue.async {
if case let .success(packetTunnelStatus) = completion, !self.isCancelled {
if case let .success(observedState) = completion, !self.isCancelled {
self.interactor.updateTunnelStatus { tunnelStatus in
tunnelStatus.packetTunnelStatus = packetTunnelStatus
tunnelStatus.observedState = observedState

if let newState = mapToState(packetTunnelStatus) {
if let newState = mapToState(observedState) {
tunnelStatus.state = newState
}
}
Expand Down
3 changes: 1 addition & 2 deletions ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ class StartTunnelOperation: ResultOperation<Void> {

interactor.updateTunnelStatus { tunnelStatus in
tunnelStatus = TunnelStatus()
tunnelStatus.packetTunnelStatus.tunnelRelay = selectedRelay?.packetTunnelRelay
tunnelStatus.state = .connecting(selectedRelay?.packetTunnelRelay)
tunnelStatus.state = .connecting(selectedRelay)
}

try tunnel.start(options: tunnelOptions.rawOptions())
Expand Down
2 changes: 1 addition & 1 deletion ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension TunnelProtocol {

/// Request status from packet tunnel process.
func getTunnelStatus(
completionHandler: @escaping (Result<PacketTunnelStatus, Error>) -> Void
completionHandler: @escaping (Result<ObservedState, Error>) -> Void
) -> Cancellable {
let operation = SendTunnelProviderMessageOperation(
dispatchQueue: dispatchQueue,
Expand Down
4 changes: 2 additions & 2 deletions ios/MullvadVPN/TunnelManager/TunnelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ final class TunnelManager: StorePaymentObserver {

// Packet tunnel may have attempted or rotated the key.
// In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
let newPacketTunnelKeyRotation = newTunnelStatus.packetTunnelStatus.lastKeyRotation
let newPacketTunnelKeyRotation = newTunnelStatus.observedState.connectionState?.lastKeyRotation
if lastPacketTunnelKeyRotation != newPacketTunnelKeyRotation {
lastPacketTunnelKeyRotation = newPacketTunnelKeyRotation
refreshDeviceState()
Expand Down Expand Up @@ -816,7 +816,7 @@ final class TunnelManager: StorePaymentObserver {
let selectorResult = try RelaySelector.evaluate(
relays: cachedRelays.relays,
constraints: settings.relayConstraints,
numberOfFailedAttempts: tunnelStatus.packetTunnelStatus.numberOfFailedAttempts
numberOfFailedAttempts: tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0
)

return SelectedRelay(
Expand Down
12 changes: 6 additions & 6 deletions ios/MullvadVPN/TunnelManager/TunnelState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import PacketTunnelCore
/// A struct describing the tunnel status.
struct TunnelStatus: Equatable, CustomStringConvertible {
/// Tunnel status returned by tunnel process.
var packetTunnelStatus = PacketTunnelStatus()
var observedState: ObservedState = .disconnected

/// Tunnel state.
var state: TunnelState = .disconnected

var description: String {
var s = "\(state), network "

if packetTunnelStatus.isNetworkReachable {
if observedState.connectionState?.isNetworkReachable == true {
s += "reachable"
} else {
s += "unreachable"
Expand All @@ -44,10 +44,10 @@ enum TunnelState: Equatable, CustomStringConvertible {
case pendingReconnect

/// Connecting the tunnel.
case connecting(PacketTunnelRelay?)
case connecting(SelectedRelay?)

/// Connected the tunnel
case connected(PacketTunnelRelay)
case connected(SelectedRelay)

/// Disconnecting the tunnel
case disconnecting(ActionAfterDisconnect)
Expand All @@ -60,7 +60,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// 1. Asking the running tunnel to reconnect to new relay via IPC.
/// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be
/// dysfunctional.
case reconnecting(PacketTunnelRelay)
case reconnecting(SelectedRelay)

/// Waiting for connectivity to come back up.
case waitingForConnectivity(WaitingForConnectionReason)
Expand Down Expand Up @@ -102,7 +102,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
}
}

var relay: PacketTunnelRelay? {
var relay: SelectedRelay? {
switch self {
case let .connected(relay), let .reconnecting(relay):
return relay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ final class TunnelControlView: UIView {
)

connectionPanel.dataSource = ConnectionPanelData(
inAddress: "\(tunnelRelay.ipv4Relay) UDP",
inAddress: "\(tunnelRelay.endpoint.ipv4Relay) UDP",
outAddress: nil
)
connectionPanel.isHidden = false
Expand Down
16 changes: 15 additions & 1 deletion ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,21 @@ class PacketTunnelProvider: NEPacketTunnelProvider {

actor.start(options: startOptions)

await actor.waitUntilConnected()
for await state in await actor.observedStates {
switch state {
case .connected:
return
case let .connecting(connectionState):
// Give the tunnel a few tries to connect, otherwise return immediately. This will enable VPN in
// device settings, but the app will still report the true state via ObservedState over IPC.
// In essence, this prevents the 60s tunnel timeout to trigger.
if connectionState.connectionAttemptCount > 1 {
return
}
default:
break
}
}
}

override func stopTunnel(with reason: NEProviderStopReason) async {
Expand Down
31 changes: 0 additions & 31 deletions ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,6 @@ import Foundation
import MullvadTypes

extension ObservedState {
public var packetTunnelStatus: PacketTunnelStatus {
var status = PacketTunnelStatus()

switch self {
case let .connecting(connState),
let .connected(connState),
let .reconnecting(connState),
let .disconnecting(connState):
switch connState.networkReachability {
case .reachable:
status.isNetworkReachable = true
case .unreachable:
status.isNetworkReachable = false
case .undetermined:
// TODO: fix me
status.isNetworkReachable = true
}

status.numberOfFailedAttempts = connState.connectionAttemptCount
status.tunnelRelay = connState.selectedRelay.packetTunnelRelay

case .disconnected, .initial:
break

case let .error(blockedState):
status.blockedStateReason = blockedState.reason
}

return status
}

public var relayConstraints: RelayConstraints? {
switch self {
case let .connecting(connState), let .connected(connState), let .reconnecting(connState):
Expand Down
30 changes: 29 additions & 1 deletion ios/PacketTunnelCore/Actor/ObservedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ public enum ObservedState: Equatable, Codable {
case disconnecting(ObservedConnectionState)
case disconnected
case error(ObservedBlockedState)

public var connectionState: ObservedConnectionState? {
switch self {
case
let .connecting(connectionState),
let .reconnecting(connectionState),
let .connected(connectionState),
let .disconnecting(connectionState):
connectionState
default:
nil
}
}

public var blockedState: ObservedBlockedState? {
switch self {
case let .error(blockedState):
blockedState
default:
nil
}
}
}

/// A serializable representation of internal connection state.
Expand All @@ -28,6 +50,11 @@ public struct ObservedConnectionState: Equatable, Codable {
public var relayConstraints: RelayConstraints
public var networkReachability: NetworkReachability
public var connectionAttemptCount: UInt
public var lastKeyRotation: Date?

public var isNetworkReachable: Bool {
networkReachability != .unreachable
}
}

/// A serializable representation of internal blocked state.
Expand Down Expand Up @@ -65,7 +92,8 @@ extension ConnectionState {
selectedRelay: selectedRelay,
relayConstraints: relayConstraints,
networkReachability: networkReachability,
connectionAttemptCount: connectionAttemptCount
connectionAttemptCount: connectionAttemptCount,
lastKeyRotation: lastKeyRotation
)
}
}
Expand Down
12 changes: 0 additions & 12 deletions ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,3 @@ public struct SelectedRelay: Equatable, Codable {
self.location = location
}
}

extension SelectedRelay {
/// Converts `SelectedRelay` to `PacketTunnelRelay` for sharing with UI.
public var packetTunnelRelay: PacketTunnelRelay {
PacketTunnelRelay(
ipv4Relay: endpoint.ipv4Relay,
ipv6Relay: endpoint.ipv6Relay,
hostname: hostname,
location: location
)
}
}
Loading

0 comments on commit b7d99c6

Please sign in to comment.