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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
|
//
// Networking.swift
// MullvadVPNUITests
//
// Created by Niklas Berglund on 2024-02-05.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import Foundation
@preconcurrency import Network
import XCTest
enum TransportProtocol: Codable {
case transport(NetworkTransportProtocol)
case application(ApplicationProtocol)
var rawValue: String {
switch self {
case let .transport(transport): transport.rawValue
case let .application(application): application.rawValue
}
}
}
enum ApplicationProtocol: String, Codable {
case wireguard
}
enum NetworkTransportProtocol: String, Codable {
case TCP = "tcp"
case UDP = "udp"
case ICMP = "icmp"
case ICMP6 = "icmp_v6"
}
enum NetworkingError: Error {
case notConfiguredError
case internalError(reason: String)
}
struct DNSServerEntry: Decodable {
let organization: String
let mullvad_dns: Bool
}
/// Class with methods for verifying network connectivity
class Networking {
/// Get configured ad serving domain
private static func getAdServingDomain() throws -> String {
guard
let adServingDomain = Bundle(for: Networking.self)
.infoDictionary?["AdServingDomain"] as? String
else {
throw NetworkingError.notConfiguredError
}
return adServingDomain
}
/// Check whether host and port is reachable by attempting to connect a socket
private static func canConnectSocket(host: String, port: String) throws -> Bool {
let socketHost = NWEndpoint.Host(host)
let socketPort = try XCTUnwrap(NWEndpoint.Port(port))
let connection = NWConnection(host: socketHost, port: socketPort, using: .tcp)
nonisolated(unsafe) var connectionError: Error?
let connectionStateDeterminedExpectation = XCTestExpectation(
description: "Completion handler for the reach ad serving domain request is invoked"
)
connection.stateUpdateHandler = { state in
print("State: \(state)")
switch state {
case let .failed(error):
connection.cancel()
connectionError = error
connectionStateDeterminedExpectation.fulfill()
case .ready:
connection.cancel()
connectionStateDeterminedExpectation.fulfill()
default:
break
}
}
connection.start(queue: .global())
let waitResult = XCTWaiter.wait(for: [connectionStateDeterminedExpectation], timeout: 15)
if waitResult != .completed || connectionError != nil {
return false
}
return true
}
/// Get configured domain to use for Internet connectivity checks
public static func getAlwaysReachableDomain() throws -> String {
guard
let shouldBeReachableDomain = Bundle(for: Networking.self)
.infoDictionary?["ShouldBeReachableDomain"] as? String
else {
throw NetworkingError.notConfiguredError
}
return shouldBeReachableDomain
}
public static func getAlwaysReachableIPAddress() -> String {
guard
let shouldBeReachableIPAddress = Bundle(for: Networking.self)
.infoDictionary?["ShouldBeReachableIPAddress"] as? String
else {
XCTFail("Should be reachable IP address not configured")
return String()
}
return shouldBeReachableIPAddress
}
/// Verify API can be accessed by attempting to connect a socket to the configured API host and port
public static func verifyCanAccessAPI() throws {
let apiIPAddress = try MullvadAPIWrapper.getAPIIPAddress()
let apiPort = try MullvadAPIWrapper.getAPIPort()
XCTAssertTrue(
try canConnectSocket(host: apiIPAddress, port: apiPort),
"Failed to verify that API can be accessed"
)
}
/// Verify API cannot be accessed by attempting to connect a socket to the configured API host and port
public static func verifyCannotAccessAPI() throws {
let apiIPAddress = try MullvadAPIWrapper.getAPIIPAddress()
let apiPort = try MullvadAPIWrapper.getAPIPort()
XCTAssertFalse(
try canConnectSocket(host: apiIPAddress, port: apiPort),
"Failed to verify that API cannot be accessed"
)
}
/// Verify that the device has Internet connectivity
public static func verifyCanAccessInternet() throws {
XCTAssertTrue(
try canConnectSocket(host: getAlwaysReachableDomain(), port: "80"),
"Failed to verify that the Internet can be acccessed"
)
}
/// Verify that the device does not have Internet connectivity
public static func verifyCannotAccessInternet() throws {
XCTAssertFalse(
try canConnectSocket(host: getAlwaysReachableDomain(), port: "80"),
"Failed to verify that the Internet cannot be accessed"
)
}
/// Verify that an ad serving domain is reachable by making sure a connection can be established on port 80
public static func verifyCanReachAdServingDomain() throws {
XCTAssertTrue(
try Self.canConnectSocket(host: try Self.getAdServingDomain(), port: "80"),
"Failed to verify that ad serving domain can be accessed"
)
}
/// Verify that an ad serving domain is NOT reachable by making sure a connection can not be established on port 80
public static func verifyCannotReachAdServingDomain() throws {
XCTAssertFalse(
try Self.canConnectSocket(host: try Self.getAdServingDomain(), port: "80"),
"Failed to verify that ad serving domain cannot be accessed"
)
}
/// Verify that the expected DNS server is used by verifying provider name and whether it is a Mullvad DNS server or not
public static func verifyDNSServerProvider(_ providerName: String, isMullvad: Bool) throws {
guard let mullvadDNSLeakURL = URL(string: "https://am.i.mullvad.net/dnsleak") else {
throw NetworkingError.internalError(reason: "Failed to create URL object")
}
var request = URLRequest(url: mullvadDNSLeakURL)
request.setValue("application/json", forHTTPHeaderField: "accept")
nonisolated(unsafe) var requestData: Data?
nonisolated(unsafe) var requestResponse: URLResponse?
nonisolated(unsafe) var requestError: Error?
let completionHandlerInvokedExpectation = XCTestExpectation(
description: "Completion handler for the request is invoked"
)
do {
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
requestData = data
requestResponse = response
requestError = error
completionHandlerInvokedExpectation.fulfill()
}
dataTask.resume()
let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30)
if waitResult != .completed {
XCTFail("Failed to verify DNS server provider - timeout")
} else {
if let response = requestResponse as? HTTPURLResponse {
if response.statusCode != 200 {
XCTFail("Failed to verify DNS server provider - unexpected server response")
}
}
if let error = requestError {
XCTFail("Failed to verify DNS server provider - encountered error \(error.localizedDescription)")
}
if let requestData = requestData {
let dnsServerEntries = try JSONDecoder().decode([DNSServerEntry].self, from: requestData)
XCTAssertGreaterThanOrEqual(dnsServerEntries.count, 1)
for dnsServerEntry in dnsServerEntries {
XCTAssertEqual(dnsServerEntry.organization, providerName, "Expected organization name")
XCTAssertEqual(
dnsServerEntry.mullvad_dns,
isMullvad,
"Verifying that it is or isn't a Mullvad DNS server"
)
}
}
}
} catch {
XCTFail("Failed to verify DNS server provider - couldn't serialize JSON")
}
}
public static func verifyConnectedThroughMullvad() throws {
let mullvadConnectionJsonEndpoint = try XCTUnwrap(
Bundle(for: Networking.self)
.infoDictionary?["AmIJSONUrl"] as? String,
"Read am I JSON URL from Info"
)
guard let url = URL(string: mullvadConnectionJsonEndpoint) else {
XCTFail("Failed to unwrap URL")
return
}
let request = URLRequest(url: url)
let completionHandlerInvokedExpectation = XCTestExpectation(
description: "Completion handler for the request is invoked"
)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
if response.statusCode != 200 {
XCTFail("Request to connection check API failed - unexpected server response")
}
}
if let error = error {
XCTFail("Request to connection check API failed - encountered error \(error.localizedDescription)")
}
guard let data = data else {
XCTFail("Didn't receive any data")
return
}
do {
let jsonObject = try JSONSerialization.jsonObject(with: data)
if let dictionary = jsonObject as? [String: Any] {
guard let isConnectedThroughMullvad = dictionary["mullvad_exit_ip"] as? Bool else {
XCTFail("Unexpected JSON format")
return
}
XCTAssertTrue(isConnectedThroughMullvad)
}
} catch {
XCTFail("Failed to verify whether connected through Mullvad or not")
}
completionHandlerInvokedExpectation.fulfill()
}
dataTask.resume()
let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30)
if waitResult != .completed {
XCTFail("Request to connection check API failed - timeout")
}
}
public static func verifyCanAccessLocalNetwork() throws {
let apiIPAddress = "192.168.105.1"
let apiPort = "80"
XCTAssertTrue(
try canConnectSocket(host: apiIPAddress, port: apiPort),
"Failed to verify that local network can be accessed"
)
}
public static func verifyCannotAccessLocalNetwork() throws {
let apiIPAddress = "192.168.105.1"
let apiPort = "80"
XCTAssertFalse(
try canConnectSocket(host: apiIPAddress, port: apiPort),
"Failed to verify that local network cannot be accessed"
)
}
}
|