blob: 6b7766e4daa58d99dbf45926eab4d86930723145 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
//
// PingerMock.swift
// PacketTunnelCoreTests
//
// Created by pronebird on 16/08/2023.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import Foundation
import MullvadTypes
import Network
@testable import PacketTunnelCore
/// Ping client mock that can be used to simulate network transmission errors and delays.
class PingerMock: PingerProtocol, @unchecked Sendable {
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 startPinging(destAddress: IPv4Address) throws {
stateLock.withLock {
state.destAddress = destAddress
state.isSocketOpen = true
}
}
func stopPinging() {
stateLock.withLock {
state.isSocketOpen = false
}
}
func send() 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
guard let address = state.destAddress else {
fatalError("Address somehow not set when sending ping")
}
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(wallDeadline: .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)
}
// MARK: - Types
/// Internal state
private struct State {
var sequenceId: UInt16 = 0
var isSocketOpen = false
var onReply: ((PingerReply) -> Void)?
var destAddress: IPv4Address?
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: Duration = .milliseconds(100))
/// 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"
}
}
}
|