Skip to content

Commit

Permalink
Implement packet capture client and models
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasberglund committed Jul 5, 2024
1 parent 966a56c commit dc55447
Show file tree
Hide file tree
Showing 13 changed files with 792 additions and 48 deletions.
5 changes: 4 additions & 1 deletion ios/Configurations/UITests.xcconfig.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ AD_SERVING_DOMAIN = vpnlist.to
// A domain which should be reachable. Used to verify Internet connectivity. Must be running a server on port 80.
SHOULD_BE_REACHABLE_DOMAIN = mullvad.net

// Base URL for the firewall API, Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8
// Base URL for the firewall API. Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8
FIREWALL_API_BASE_URL = http:/${}/8.8.8.8

// URL for Mullvad provided JSON data with information about the connection. https://am.i.mullvad.net/json for production, https://am.i.stagemole.eu/json for staging.
AM_I_JSON_URL = https:/${}/am.i.stagemole.eu/json

// Specify whether app logs should be extracted and attached to test report for failing tests
ATTACH_APP_LOGS_ON_FAILURE = 0

// Base URL for the packet capture API. Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8
PACKET_CAPTURE_BASE_URL = http:/${}/8.8.8.8
38 changes: 33 additions & 5 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,18 @@
8585CBE32BC684180015B6A4 /* EditAccessMethodPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8585CBE22BC684180015B6A4 /* EditAccessMethodPage.swift */; };
8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */; };
8590896F2B61763B003AF5F5 /* LoggedOutUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */; };
8590A5442C2AF43400B9BF7B /* TrafficGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590A5432C2AF43400B9BF7B /* TrafficGenerator.swift */; };
85978A542BE0F10E00F999A7 /* PacketCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85978A532BE0F10E00F999A7 /* PacketCapture.swift */; };
85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */; };
85B267612B849ADB0098E3CD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 85B267602B849ADB0098E3CD /* mullvad-api.h */; };
85BEC23C2C29C41C0065A68D /* StreamCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BEC23B2C29C41C0065A68D /* StreamCollection.swift */; };
85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C7A2E82B89024B00035D5A /* SettingsTests.swift */; };
85D039982BA4711800940E7F /* SettingsMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D039972BA4711800940E7F /* SettingsMigrationTests.swift */; };
85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */; };
85E3BDE52B70E18C00FA71FD /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E3BDE42B70E18C00FA71FD /* Networking.swift */; };
85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */; };
85F1E17E2C0A256200DB8F55 /* LeakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1E17D2C0A256200DB8F55 /* LeakTests.swift */; };
85F1E1812C0A2A0C00DB8F55 /* SafariApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */; };
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0B2B6903990015DCED /* WelcomePage.swift */; };
85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */; };
A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90763A02B2857D50045ADF0 /* Socks5ConnectCommand.swift */; };
Expand Down Expand Up @@ -2043,11 +2048,16 @@
859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedInWithTimeUITestCase.swift; sourceTree = "<group>"; };
8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = "<group>"; };
8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedOutUITestCase.swift; sourceTree = "<group>"; };
8590A5432C2AF43400B9BF7B /* TrafficGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficGenerator.swift; sourceTree = "<group>"; };
85978A532BE0F10E00F999A7 /* PacketCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketCapture.swift; sourceTree = "<group>"; };
85A42B872BB44D31007BABF7 /* DeviceManagementPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementPage.swift; sourceTree = "<group>"; };
85B267602B849ADB0098E3CD /* mullvad-api.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../../mullvad-api/include/mullvad-api.h"; sourceTree = "<group>"; };
85BEC23B2C29C41C0065A68D /* StreamCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamCollection.swift; sourceTree = "<group>"; };
85C7A2E82B89024B00035D5A /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = "<group>"; };
85D039972BA4711800940E7F /* SettingsMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationTests.swift; sourceTree = "<group>"; };
85E3BDE42B70E18C00FA71FD /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = "<group>"; };
85F1E17D2C0A256200DB8F55 /* LeakTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeakTests.swift; sourceTree = "<group>"; };
85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariApp.swift; sourceTree = "<group>"; };
85FB5A0B2B6903990015DCED /* WelcomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage.swift; sourceTree = "<group>"; };
85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionPage.swift; sourceTree = "<group>"; };
A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountsProxy+Stubs.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3981,21 +3991,23 @@
852969262B4D9C1F007EAD4C /* MullvadVPNUITests */ = {
isa = PBXGroup;
children = (
85557B0C2B591B0F00795FE1 /* Networking */,
852969312B4E9220007EAD4C /* Pages */,
7A45CFCD2C08697100D80B21 /* Screenshots */,
852969372B4ED20E007EAD4C /* Info.plist */,
8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */,
85B267602B849ADB0098E3CD /* mullvad-api.h */,
852969372B4ED20E007EAD4C /* Info.plist */,
852969272B4D9C1F007EAD4C /* AccountTests.swift */,
85557B112B594FC900795FE1 /* ConnectivityTests.swift */,
A9BFAFFE2BD004ED00F2BCA1 /* CustomListsTests.swift */,
85F1E17D2C0A256200DB8F55 /* LeakTests.swift */,
850201DA2B503D7700EF8C96 /* RelayTests.swift */,
85D039972BA4711800940E7F /* SettingsMigrationTests.swift */,
85C7A2E82B89024B00035D5A /* SettingsTests.swift */,
8518F6392B601910009EB113 /* Base */,
856952E12BD6B04C008C1F84 /* XCUIElement+Extensions.swift */,
85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */,
8518F6392B601910009EB113 /* Base */,
85F1E17F2C0A29FA00DB8F55 /* External apps */,
85557B0C2B591B0F00795FE1 /* Networking */,
852969312B4E9220007EAD4C /* Pages */,
7A45CFCD2C08697100D80B21 /* Screenshots */,
);
path = MullvadVPNUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -4044,10 +4056,21 @@
85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */,
85E3BDE42B70E18C00FA71FD /* Networking.swift */,
856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */,
85978A532BE0F10E00F999A7 /* PacketCapture.swift */,
85BEC23B2C29C41C0065A68D /* StreamCollection.swift */,
8590A5432C2AF43400B9BF7B /* TrafficGenerator.swift */,
);
path = Networking;
sourceTree = "<group>";
};
85F1E17F2C0A29FA00DB8F55 /* External apps */ = {
isa = PBXGroup;
children = (
85F1E1802C0A2A0C00DB8F55 /* SafariApp.swift */,
);
path = "External apps";
sourceTree = "<group>";
};
A907639F2B2857D50045ADF0 /* Socks5 */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -6203,6 +6226,7 @@
8529693C2B4F0257007EAD4C /* Alert.swift in Sources */,
8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */,
850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */,
85F1E1812C0A2A0C00DB8F55 /* SafariApp.swift in Sources */,
85D039982BA4711800940E7F /* SettingsMigrationTests.swift in Sources */,
85021CAE2BDBC4290098B400 /* AppLogsPage.swift in Sources */,
850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */,
Expand All @@ -6217,6 +6241,7 @@
85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */,
852969352B4E9270007EAD4C /* LoginPage.swift in Sources */,
A998DA832BD2B055001D61A2 /* EditCustomListLocationsPage.swift in Sources */,
8590A5442C2AF43400B9BF7B /* TrafficGenerator.swift in Sources */,
7ACD79392C0DAADD00DBEE14 /* AddCustomListLocationsPage.swift in Sources */,
8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */,
A9BFAFFF2BD004ED00F2BCA1 /* CustomListsTests.swift in Sources */,
Expand All @@ -6227,10 +6252,12 @@
85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */,
7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */,
856952DC2BD2922A008C1F84 /* PartnerAPIClient.swift in Sources */,
85F1E17E2C0A256200DB8F55 /* LeakTests.swift in Sources */,
85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */,
F0DAC8AF2C1712C300F80144 /* MultihopPromptAlert.swift in Sources */,
855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */,
8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */,
85978A542BE0F10E00F999A7 /* PacketCapture.swift in Sources */,
85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */,
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */,
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */,
Expand All @@ -6244,6 +6271,7 @@
852969332B4E9232007EAD4C /* Page.swift in Sources */,
A9A557F32B7E19B10017ADA8 /* SettingsPage.swift in Sources */,
852D054F2BC43DF7008578D2 /* AddAccessMethodPage.swift in Sources */,
85BEC23C2C29C41C0065A68D /* StreamCollection.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down

This file was deleted.

63 changes: 62 additions & 1 deletion ios/MullvadVPNUITests/Base/BaseUITestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class BaseUITestCase: XCTestCase {
/// Default relay to use in tests
static let testsDefaultRelayName = "se-got-wg-001"

/// True when the current test case is capturing packets
private var currentTestCaseShouldCapturePackets = false

/// True when a packet capture session is active
private var packetCaptureSessionIsActive = false
private var packetCaptureSession: PacketCaptureSession?

// swiftlint:disable force_cast
let displayName = Bundle(for: BaseUITestCase.self)
.infoDictionary?["DisplayName"] as! String
Expand Down Expand Up @@ -100,7 +107,7 @@ class BaseUITestCase: XCTestCase {
func allowAddVPNConfigurationsIfAsked() {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

if springboard.buttons["Allow"].waitForExistence(timeout: Self.shortTimeout) {
if springboard.buttons["Allowi"].waitForExistence(timeout: Self.shortTimeout) {
let alertAllowButton = springboard.buttons.element(boundBy: 0)
if alertAllowButton.waitForExistence(timeout: Self.defaultTimeout) {
alertAllowButton.tap()
Expand All @@ -125,6 +132,29 @@ class BaseUITestCase: XCTestCase {
}
}

/// Start packet capture for this test case
func startPacketCapture() {
currentTestCaseShouldCapturePackets = true
packetCaptureSessionIsActive = true
let packetCaptureClient = PacketCapture()
packetCaptureSession = packetCaptureClient.startCapture()
}

/// Stop the current packet capture and return captured data
func stopPacketCapture() -> StreamCollection {
packetCaptureSessionIsActive = false
guard let packetCaptureSession else {
XCTFail("Trying to stop capture when there is no active capture")
return StreamCollection(streams: [])
}

let packetCaptureAPIClient = PacketCapture()
packetCaptureAPIClient.stopCapture(session: packetCaptureSession)
let capturedData = packetCaptureAPIClient.getParsedCaptureObjects(session: packetCaptureSession)

return StreamCollection(streams: capturedData)
}

// MARK: - Setup & teardown

/// Override this class function to change the uninstall behaviour in suite level teardown
Expand All @@ -141,12 +171,43 @@ class BaseUITestCase: XCTestCase {

/// Test level setup
override func setUp() {
currentTestCaseShouldCapturePackets = false // Reset for each test case run
continueAfterFailure = false
app.launch()
}

/// Test level teardown
override func tearDown() {
if currentTestCaseShouldCapturePackets {
guard let packetCaptureSession = packetCaptureSession else {
XCTFail("Packet capture session unexpectedly not set up")
return
}

let packetCaptureClient = PacketCapture()

// If there's a an active session due to cancelled/failed test run make sure to end it
if packetCaptureSessionIsActive {
packetCaptureSessionIsActive = false
packetCaptureClient.stopCapture(session: packetCaptureSession)
}

packetCaptureClient.stopCapture(session: packetCaptureSession)
let pcap = packetCaptureClient.getPCAP(session: packetCaptureSession)
let parsedCapture = packetCaptureClient.getParsedCapture(session: packetCaptureSession)
self.packetCaptureSession = nil

let pcapAttachment = XCTAttachment(data: pcap)
pcapAttachment.name = self.name + ".pcap"
pcapAttachment.lifetime = .keepAlways
self.add(pcapAttachment)

let jsonAttachment = XCTAttachment(data: parsedCapture)
jsonAttachment.name = self.name + ".json"
jsonAttachment.lifetime = .keepAlways
self.add(jsonAttachment)
}

app.terminate()

if let testRun = self.testRun, testRun.failureCount > 0, attachAppLogsOnFailure == true {
Expand Down
27 changes: 27 additions & 0 deletions ios/MullvadVPNUITests/External apps/SafariApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// SafariApp.swift
// MullvadVPNUITests
//
// Created by Niklas Berglund on 2024-05-31.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import XCTest

class SafariApp {
let app = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")

func launch() {
app.launch()
}

@discardableResult func tapAddressBar() -> Self {
app.textFields.firstMatch.tap()
return self
}

@discardableResult func enterText(_ text: String) -> Self {
app.typeText(text)
return self
}
}
4 changes: 4 additions & 0 deletions ios/MullvadVPNUITests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
<string>$(IOS_DEVICE_PIN_CODE)</string>
<key>NoTimeAccountNumber</key>
<string>$(NO_TIME_ACCOUNT_NUMBER)</string>
<key>PacketCaptureAPIBaseURL</key>
<string>$(PACKET_CAPTURE_BASE_URL)</string>
<key>PartnerApiToken</key>
<string>$(PARTNER_API_TOKEN)</string>
<key>ShouldBeReachableDomain</key>
<string>$(SHOULD_BE_REACHABLE_DOMAIN)</string>
<key>ShouldBeReachableIPAddress</key>
<string>$(SHOULD_BE_REACHABLE_IP_ADDRESS)</string>
<key>TestDeviceIdentifier</key>
<string>$(TEST_DEVICE_IDENTIFIER_UUID</string>
</dict>
Expand Down
76 changes: 76 additions & 0 deletions ios/MullvadVPNUITests/LeakTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// LeakTests.swift
// MullvadVPNUITests
//
// Created by Niklas Berglund on 2024-05-31.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import XCTest

class LeakTests: LoggedInWithTimeUITestCase {
override func tearDown() {
FirewallAPIClient().removeRules()
}

/// Send UDP traffic to a host, connect to relay and make sure while connected to relay no traffic leaked went directly to the host
func testNegativeLeaks() throws {
let testIpAddress = Networking.getAlwaysReachableIPAddress()
FirewallAPIClient().createRule(try FirewallRule.makeBlockAllTrafficRule(toIPAddress: testIpAddress))
startPacketCapture()
let trafficGenerator = TrafficGenerator(destinationHost: testIpAddress, port: 80)
trafficGenerator.startGeneratingUDPTraffic(interval: 1.0)

TunnelControlPage(app)
.tapSecureConnectionButton()

allowAddVPNConfigurationsIfAsked()

TunnelControlPage(app)
.waitForSecureConnectionLabel()
let connectedDate = Date()

let relayIPAddress = TunnelControlPage(app)
.getInIPAddressFromConnectionStatus()

// Keep the tunnel connection for a while
Thread.sleep(forTimeInterval: 5.0)

app.launch()
TunnelControlPage(app)
.tapDisconnectButton()
let disconnectedDate = Date()

// Keep the capture open for a while
Thread.sleep(forTimeInterval: 3.0)
trafficGenerator.stopGeneratingUDPTraffic()

let capturedStreamCollection = stopPacketCapture()

do {
let relayConnectionDateInterval = try capturedStreamCollection
.getConnectedThroughRelayDateInterval(
relayIPAddress: relayIPAddress
)

// Get traffic from time window of connection with some leeway
let secondsLeeway = 2.0
let connectedDateWithLeeway = relayConnectionDateInterval.start.addingTimeInterval(secondsLeeway)
let disconnectedDateWithLeeway = relayConnectionDateInterval.end.addingTimeInterval(-secondsLeeway)
let connectedToRelayDateIntervalWithLeeway = DateInterval(
start: connectedDateWithLeeway,
end: disconnectedDateWithLeeway
)
let connectedThroughRelayStreamCollection = capturedStreamCollection.extractStreamCollectionFrom(
connectedToRelayDateIntervalWithLeeway,
cutOffPacketsOverflow: true
)

// Treat any traffic to the test IP address during the connected time window as leak
connectedThroughRelayStreamCollection.dontAllowTrafficFromTestDevice(to: testIpAddress)
connectedThroughRelayStreamCollection.verifyDontHaveLeaks()
} catch {
XCTFail("Unexpectedly didn't find any traffic between test device and relay")
}
}
}
Loading

0 comments on commit dc55447

Please sign in to comment.