diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-08-15 13:12:48 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-08-17 14:44:09 +0200 |
| commit | f0694bf653c8caf920e27715fa0821670cc67908 (patch) | |
| tree | 766b5bc8fef291f130401399d3d1fd3c7c9dbc05 | |
| parent | 24fff949d10a7a865ae29029453298f56cc2f6db (diff) | |
| download | mullvadvpn-f0694bf653c8caf920e27715fa0821670cc67908.tar.xz mullvadvpn-f0694bf653c8caf920e27715fa0821670cc67908.zip | |
TunnelMonitor: add initial connection test
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 28 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Pinger/Pinger.swift | 2 | ||||
| -rw-r--r-- | ios/PacketTunnelCore/Pinger/PingerProtocol.swift | 2 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift | 48 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/Mocks/MockPinger.swift | 126 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift | 26 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift | 54 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/TunnelMonitorTests.swift | 119 |
8 files changed, 403 insertions, 2 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index c8b8755373..f905c92b0c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ 580810E82A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; }; 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E72A30E15500B74552 /* DeviceCheckRemoteServiceProtocol.swift */; }; 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; }; + 58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */; }; 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; }; 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; }; 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; @@ -78,6 +79,8 @@ 581DA2732A1E227D0046ED47 /* RESTTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2722A1E227D0046ED47 /* RESTTypes.swift */; }; 581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; }; 581DA2762A1E2FD10046ED47 /* WgKeyRotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */; }; + 581F23AD2A8CF92100788AB6 /* MockDefaultPathObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */; }; + 581F23AF2A8CF94D00788AB6 /* MockPinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */; }; 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; }; 5820EDA9288FE064006BF4E4 /* DeviceManagementInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDA8288FE064006BF4E4 /* DeviceManagementInteractor.swift */; }; 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */; }; @@ -373,6 +376,8 @@ 58E45A5729F12C5100281ECF /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */; }; 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; + 58EC067A2A8D208D00BEB973 /* MockTunnelDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */; }; + 58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */; }; 58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */; }; 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; @@ -952,6 +957,7 @@ 5808273B284888BC006B77A4 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = "<group>"; }; 5808273C284888E5006B77A4 /* PacketTunnel.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PacketTunnel.xcconfig; sourceTree = "<group>"; }; 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceViewController.swift; sourceTree = "<group>"; }; + 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorTests.swift; sourceTree = "<group>"; }; 580CBFB72848D503007878F0 /* OperationConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConditionTests.swift; sourceTree = "<group>"; }; 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; }; 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = "<group>"; }; @@ -974,6 +980,8 @@ 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; }; 581DA2722A1E227D0046ED47 /* RESTTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTypes.swift; sourceTree = "<group>"; }; 581DA2742A1E283E0046ED47 /* WgKeyRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgKeyRotation.swift; sourceTree = "<group>"; }; + 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDefaultPathObserver.swift; sourceTree = "<group>"; }; + 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPinger.swift; sourceTree = "<group>"; }; 5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; }; 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; }; @@ -1224,6 +1232,8 @@ 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingErrors+CustomErrorDescription.swift"; sourceTree = "<group>"; }; 58E511EA28DDE18400B0BCDE /* Error+Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Chain.swift"; sourceTree = "<group>"; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; + 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelDeviceInfo.swift; sourceTree = "<group>"; }; + 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkCounters.swift; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; 58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDataSource.swift; sourceTree = "<group>"; }; 58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsDataSourceDelegate.swift; sourceTree = "<group>"; }; @@ -2198,6 +2208,8 @@ isa = PBXGroup; children = ( 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, + 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, + 58EC067D2A8D2B0700BEB973 /* Mocks */, ); path = PacketTunnelCoreTests; sourceTree = "<group>"; @@ -2395,6 +2407,17 @@ path = TunnelMonitor; sourceTree = "<group>"; }; + 58EC067D2A8D2B0700BEB973 /* Mocks */ = { + isa = PBXGroup; + children = ( + 581F23AC2A8CF92100788AB6 /* MockDefaultPathObserver.swift */, + 581F23AE2A8CF94D00788AB6 /* MockPinger.swift */, + 58EC06792A8D208D00BEB973 /* MockTunnelDeviceInfo.swift */, + 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */, + ); + path = Mocks; + sourceTree = "<group>"; + }; 58ECD29023F178FD004298B6 /* Configurations */ = { isa = PBXGroup; children = ( @@ -3436,6 +3459,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 58EC067A2A8D208D00BEB973 /* MockTunnelDeviceInfo.swift in Sources */, + 58EC067C2A8D2A0B00BEB973 /* NetworkCounters.swift in Sources */, + 581F23AD2A8CF92100788AB6 /* MockDefaultPathObserver.swift in Sources */, + 58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */, + 581F23AF2A8CF94D00788AB6 /* MockPinger.swift in Sources */, 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/PacketTunnelCore/Pinger/Pinger.swift b/ios/PacketTunnelCore/Pinger/Pinger.swift index c312f822a4..e9a960a102 100644 --- a/ios/PacketTunnelCore/Pinger/Pinger.swift +++ b/ios/PacketTunnelCore/Pinger/Pinger.swift @@ -150,7 +150,7 @@ public final class Pinger: PingerProtocol { throw Error.sendPacket(errno) } - return PingerSendResult(sequenceNumber: sequenceNumber, bytesSent: UInt16(bytesSent)) + return PingerSendResult(sequenceNumber: sequenceNumber, bytesSent: UInt(bytesSent)) } private func nextSequenceNumber() -> UInt16 { diff --git a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift index 6b7a4f9764..9df205ab6d 100644 --- a/ios/PacketTunnelCore/Pinger/PingerProtocol.swift +++ b/ios/PacketTunnelCore/Pinger/PingerProtocol.swift @@ -16,7 +16,7 @@ public enum PingerReply { public struct PingerSendResult { public var sequenceNumber: UInt16 - public var bytesSent: UInt16 + public var bytesSent: UInt } public protocol PingerProtocol { diff --git a/ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift b/ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift new file mode 100644 index 0000000000..2e6c4feb6e --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/MockDefaultPathObserver.swift @@ -0,0 +1,48 @@ +// +// MockDefaultPathObserver.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 16/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension +import PacketTunnelCore + +struct MockNetworkPath: NetworkPath { + var status: NetworkExtension.NWPathStatus = .satisfied +} + +/// Mock implementation of a default path observer. +class MockDefaultPathObserver: DefaultPathObserverProtocol { + var defaultPath: NetworkPath? { + return stateLock.withLock { innerPath } + } + + private var innerPath: NetworkPath = MockNetworkPath() + private var stateLock = NSLock() + + private var defaultPathHandler: ((NetworkPath) -> Void)? + + func start(_ body: @escaping (NetworkPath) -> Void) { + stateLock.withLock { + defaultPathHandler = body + } + } + + func stop() { + stateLock.withLock { + defaultPathHandler = nil + } + } + + /// Simulate network path update. + func updatePath(_ newPath: NetworkPath) { + let pathHandler = stateLock.withLock { + innerPath = newPath + return defaultPathHandler + } + pathHandler?(newPath) + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift b/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift new file mode 100644 index 0000000000..e75918d0ad --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/MockPinger.swift @@ -0,0 +1,126 @@ +// +// MockPinger.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 16/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Network +@testable import PacketTunnelCore + +/// Ping client mock implementation that can be used to simulate network transmission errors and delays. +class MockPinger: PingerProtocol { + typealias OutcomeDecider = (IPv4Address, UInt16) -> Outcome + + private let decideOutcome: OutcomeDecider + private let networkStatsReporting: NetworkStatsReporting + private let stateLock = NSLock() + private var state = State() + + var onReply: ((PingerReply) -> Void)? { + get { + stateLock.withLock { state.onReply } + } + set { + stateLock.withLock { state.onReply = newValue } + } + } + + init(networkStatsReporting: NetworkStatsReporting, decideOutcome: @escaping OutcomeDecider) { + self.networkStatsReporting = networkStatsReporting + self.decideOutcome = decideOutcome + } + + func openSocket(bindTo interfaceName: String?) throws { + stateLock.withLock { + state.isSocketOpen = true + } + } + + func closeSocket() { + stateLock.withLock { + state.isSocketOpen = false + } + } + + func send(to address: IPv4Address) throws -> PingerSendResult { + // Used for simulation. In reality can be any number. + // But for realism it is: IPv4 header (20 bytes) + ICMP header (8 bytes) + let icmpPacketSize: UInt = 28 + + let nextSequenceId = try stateLock.withLock { + guard state.isSocketOpen else { throw POSIXError(.ENOTCONN) } + + return state.incrementSequenceId() + } + + switch decideOutcome(address, nextSequenceId) { + case let .sendReply(reply, delay): + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self else { return } + + networkStatsReporting.reportBytesReceived(UInt64(icmpPacketSize)) + + switch reply { + case .normal: + onReply?(.success(address, nextSequenceId)) + case .malformed: + onReply?(.parseError(ParseError())) + } + } + + case .ignore: + break + + case .sendFailure: + throw POSIXError(.ECONNREFUSED) + } + + networkStatsReporting.reportBytesSent(UInt64(icmpPacketSize)) + + return PingerSendResult(sequenceNumber: nextSequenceId, bytesSent: icmpPacketSize) + } + + // MARK: - Types + + /// Internal state + private struct State { + var sequenceId: UInt16 = 0 + var isSocketOpen = false + var onReply: ((PingerReply) -> Void)? + + mutating func incrementSequenceId() -> UInt16 { + sequenceId += 1 + return sequenceId + } + } + + /// Simulated ICMP reply. + enum Reply { + /// Simulate normal ping reply. + case normal + + /// Simulate malformed ping reply. + case malformed + } + + /// The outcome of ping request simulation. + enum Outcome { + /// Simulate ping reply transmission. + case sendReply(reply: Reply = .normal, afterDelay: TimeInterval = 0.1) + + /// Simulate packet that was lost or left unanswered. + case ignore + + /// Simulate failure to send ICMP packet (i.e `sendto()` error). + case sendFailure + } + + struct ParseError: LocalizedError { + var errorDescription: String? { + return "ICMP response parse error" + } + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift b/ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift new file mode 100644 index 0000000000..bb61dfb443 --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/MockTunnelDeviceInfo.swift @@ -0,0 +1,26 @@ +// +// MockTunnelDeviceInfo.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 16/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import PacketTunnelCore + +/// Mock implementation of a tunnel device. +struct MockTunnelDeviceInfo: TunnelDeviceInfoProtocol { + let networkStatsProviding: NetworkStatsProviding + + var interfaceName: String? { + return "utun0" + } + + func getStats() throws -> WgStats { + return WgStats( + bytesReceived: networkStatsProviding.bytesReceived, + bytesSent: networkStatsProviding.bytesSent + ) + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift b/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift new file mode 100644 index 0000000000..d20a1ef20f --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/NetworkCounters.swift @@ -0,0 +1,54 @@ +// +// NetworkCounters.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 16/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Protocol describing a type capable of receiving and updating network counters. +protocol NetworkStatsReporting { + /// Increment number of bytes sent. + func reportBytesSent(_ byteCount: UInt64) + + /// Increment number of bytes received. + func reportBytesReceived(_ byteCount: UInt64) +} + +/// Protocol describing a type providing network statistics. +protocol NetworkStatsProviding { + /// Returns number of bytes sent. + var bytesSent: UInt64 { get } + + /// Returns number of bytes received. + var bytesReceived: UInt64 { get } +} + +/// Class that holds network statistics (bytes sent and received) for a simulated network adapter. +final class NetworkCounters: NetworkStatsProviding, NetworkStatsReporting { + private let stateLock = NSLock() + private var _bytesSent: UInt64 = 0 + private var _bytesReceived: UInt64 = 0 + + var bytesSent: UInt64 { + stateLock.withLock { _bytesSent } + } + + var bytesReceived: UInt64 { + stateLock.withLock { _bytesReceived } + } + + func reportBytesSent(_ byteCount: UInt64) { + stateLock.withLock { + _bytesSent += byteCount + } + } + + func reportBytesReceived(_ byteCount: UInt64) { + stateLock.withLock { + _bytesReceived += byteCount + } + } +} diff --git a/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift new file mode 100644 index 0000000000..462c2f1476 --- /dev/null +++ b/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift @@ -0,0 +1,119 @@ +// +// TunnelMonitorTests.swift +// PacketTunnelCoreTests +// +// Created by pronebird on 15/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Network +@testable import PacketTunnelCore +import XCTest + +final class TunnelMonitorTests: XCTestCase { + let networkCounters = NetworkCounters() + + func testShouldDetermineConnectionEstablished() throws { + let connectedExpectation = expectation(description: "Should report connected.") + let connectionLostExpectation = expectation(description: "Should not report connection loss") + connectionLostExpectation.isInverted = true + + let pinger = MockPinger(networkStatsReporting: networkCounters) { destination, sequence in + return .sendReply() + } + + let tunnelMonitor = TunnelMonitor( + eventQueue: .main, + pinger: pinger, + tunnelDeviceInfo: MockTunnelDeviceInfo(networkStatsProviding: networkCounters), + defaultPathObserver: MockDefaultPathObserver() + ) + + tunnelMonitor.onEvent = { event in + switch event { + case .connectionEstablished: + connectedExpectation.fulfill() + + case .connectionLost: + connectionLostExpectation.fulfill() + + case .networkReachabilityChanged: + break + } + } + + tunnelMonitor.start(probeAddress: .loopback) + + waitForExpectations(timeout: 1) + } + + func testInitialConnectionTimings() { + // Setup pinger so that it never receives any replies. + let pinger = MockPinger(networkStatsReporting: networkCounters) { destination, sequence in + return .ignore + } + + let tunnelMonitor = TunnelMonitor( + eventQueue: .main, + pinger: pinger, + tunnelDeviceInfo: MockTunnelDeviceInfo(networkStatsProviding: networkCounters), + defaultPathObserver: MockDefaultPathObserver() + ) + + /* + Tunnel monitor uses shorter timeout intervals during the initial connection sequence and picks next relay more + aggressively in order to reduce connection time. + + First connection attempt starts at 4 second timeout, then doubles with each subsequent attempt, while being + capped at 15s max. + */ + var expectedTimings = [4, 8, 15, 15] + + // Calculate the amount of time necessary to perform the test adding some leeway. + let timeout = expectedTimings.reduce(1, +) + + let expectation = self.expectation(description: "Should respect all timings.") + expectation.expectedFulfillmentCount = expectedTimings.count + + // This date will be used to measure the amount of time elapsed between `.connectionLost` events. + var startDate = Date() + + tunnelMonitor.onEvent = { [weak tunnelMonitor] event in + guard case .connectionLost = event else { return } + + switch event { + case .connectionLost: + let expectedDuration = expectedTimings.removeFirst() + + // Compute amount of time elapsed between `.connectionLost` events rounding it down towards zero. + let timeElapsed = Int(Date().timeIntervalSince(startDate).rounded(.down)) + + XCTAssertEqual( + timeElapsed, + expectedDuration, + "Expected to report connection loss after \(expectedDuration)s, instead reported it after \(timeElapsed)s." + ) + + expectation.fulfill() + + if !expectedTimings.isEmpty { + startDate = Date() + + // Continue monitoring by calling start() again. + tunnelMonitor?.start(probeAddress: .loopback) + } + + case .connectionEstablished: + XCTFail() + + case .networkReachabilityChanged: + break + } + } + + // Start monitoring. + tunnelMonitor.start(probeAddress: .loopback) + + waitForExpectations(timeout: TimeInterval(timeout)) + } +} |
