summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorNiklas Berglund <niklas.berglund@gmail.com>2024-04-30 13:00:55 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-02-06 15:43:20 +0100
commite38efe366a5f4147bf49d18c0df27630fba6d4ba (patch)
tree05c24b9cdbbc7ca22807d68c73f6ff11b7e3f454 /ios
parentd2317ac641f487aa83affc6c6570d29961db4668 (diff)
downloadmullvadvpn-e38efe366a5f4147bf49d18c0df27630fba6d4ba.tar.xz
mullvadvpn-e38efe366a5f4147bf49d18c0df27630fba6d4ba.zip
Implement basic leak tests
Diffstat (limited to 'ios')
-rw-r--r--ios/Configurations/UITests.xcconfig.template8
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj38
-rw-r--r--ios/MullvadVPNUITests/Base/BaseUITestCase.swift62
-rw-r--r--ios/MullvadVPNUITests/ConnectivityTests.swift2
-rw-r--r--ios/MullvadVPNUITests/Info.plist4
-rw-r--r--ios/MullvadVPNUITests/LeakTests.swift88
-rw-r--r--ios/MullvadVPNUITests/Networking/FirewallClient.swift (renamed from ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift)50
-rw-r--r--ios/MullvadVPNUITests/Networking/FirewallRule.swift16
-rw-r--r--ios/MullvadVPNUITests/Networking/LeakCheck.swift45
-rw-r--r--ios/MullvadVPNUITests/Networking/Networking.swift36
-rw-r--r--ios/MullvadVPNUITests/Networking/PacketCapture.swift333
-rw-r--r--ios/MullvadVPNUITests/Networking/TestRouterAPIClient.swift56
-rw-r--r--ios/MullvadVPNUITests/Networking/TrafficGenerator.swift74
-rw-r--r--ios/MullvadVPNUITests/RelayTests.swift10
-rw-r--r--ios/MullvadVPNUITests/tests.json1
15 files changed, 742 insertions, 81 deletions
diff --git a/ios/Configurations/UITests.xcconfig.template b/ios/Configurations/UITests.xcconfig.template
index af31a89b26..988a9a65d7 100644
--- a/ios/Configurations/UITests.xcconfig.template
+++ b/ios/Configurations/UITests.xcconfig.template
@@ -23,8 +23,11 @@ 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
+
+// An IP address which should always be reachable. Must be running a server on port 80.
+SHOULD_BE_REACHABLE_IP_ADDRESS = 45.83.223.209
-// 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.
@@ -32,3 +35,6 @@ 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
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 25a47cd2ee..a19ea40998 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -687,21 +687,25 @@
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */; };
8542CE242B95F7B9006FCA14 /* VPNSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */; };
8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542F7522BCFBD050035C042 /* SelectLocationFilterPage.swift */; };
- 85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */; };
+ 85557B0E2B591B2600795FE1 /* FirewallClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B0D2B591B2600795FE1 /* FirewallClient.swift */; };
85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B0F2B59215F00795FE1 /* FirewallRule.swift */; };
85557B122B594FC900795FE1 /* ConnectivityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B112B594FC900795FE1 /* ConnectivityTests.swift */; };
85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */; };
85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */; };
85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */; };
+ 8555C6602D1030040092DAD0 /* LeakCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8555C65F2D102FFE0092DAD0 /* LeakCheck.swift */; };
8556EB522B9A1C6900D26DD4 /* MullvadApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */; };
8556EB542B9A1D7100D26DD4 /* BridgingHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */; };
8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8556EB552B9B0AC500D26DD4 /* RevokedDevicePage.swift */; };
855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */; };
+ 85607C892D131CD500037E34 /* TestRouterAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85607C882D131CCD00037E34 /* TestRouterAPIClient.swift */; };
856952DC2BD2922A008C1F84 /* PartnerAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */; };
856952E22BD6B04C008C1F84 /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856952E12BD6B04C008C1F84 /* XCUIElement+Extensions.swift */; };
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 */; };
85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C7A2E82B89024B00035D5A /* SettingsTests.swift */; };
@@ -709,6 +713,7 @@
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 */; };
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0B2B6903990015DCED /* WelcomePage.swift */; };
85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */; };
A902E7A62D3FB0D9007F844A /* LogFileOutputStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A902E7A52D3FB0D9007F844A /* LogFileOutputStreamTests.swift */; };
@@ -2082,17 +2087,19 @@
8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmittedPage.swift; sourceTree = "<group>"; };
8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsPage.swift; sourceTree = "<group>"; };
8542F7522BCFBD050035C042 /* SelectLocationFilterPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationFilterPage.swift; sourceTree = "<group>"; };
- 85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallAPIClient.swift; sourceTree = "<group>"; };
+ 85557B0D2B591B2600795FE1 /* FirewallClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallClient.swift; sourceTree = "<group>"; };
85557B0F2B59215F00795FE1 /* FirewallRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallRule.swift; sourceTree = "<group>"; };
85557B112B594FC900795FE1 /* ConnectivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityTests.swift; sourceTree = "<group>"; };
85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAPIWrapper.swift; sourceTree = "<group>"; };
85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElementQuery+Extensions.swift"; sourceTree = "<group>"; };
85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBar.swift; sourceTree = "<group>"; };
85557B1F2B5FBBD700795FE1 /* AccountPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPage.swift; sourceTree = "<group>"; };
+ 8555C65F2D102FFE0092DAD0 /* LeakCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeakCheck.swift; sourceTree = "<group>"; };
8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MullvadApi.swift; path = MullvadVPNUITests/MullvadApi.swift; sourceTree = "<group>"; };
8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
8556EB552B9B0AC500D26DD4 /* RevokedDevicePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDevicePage.swift; sourceTree = "<group>"; };
855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportPage.swift; sourceTree = "<group>"; };
+ 85607C882D131CCD00037E34 /* TestRouterAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRouterAPIClient.swift; sourceTree = "<group>"; };
856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerAPIClient.swift; sourceTree = "<group>"; };
856952E12BD6B04C008C1F84 /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = "<group>"; };
8585CBE22BC684180015B6A4 /* EditAccessMethodPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodPage.swift; sourceTree = "<group>"; };
@@ -2100,11 +2107,14 @@
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>"; };
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>"; };
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>"; };
@@ -4203,21 +4213,22 @@
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 */,
+ 85557B0C2B591B0F00795FE1 /* Networking */,
+ 852969312B4E9220007EAD4C /* Pages */,
+ 7A45CFCD2C08697100D80B21 /* Screenshots */,
);
path = MullvadVPNUITests;
sourceTree = "<group>";
@@ -4265,11 +4276,15 @@
85557B0C2B591B0F00795FE1 /* Networking */ = {
isa = PBXGroup;
children = (
- 85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */,
+ 85607C882D131CCD00037E34 /* TestRouterAPIClient.swift */,
+ 8555C65F2D102FFE0092DAD0 /* LeakCheck.swift */,
+ 85557B0D2B591B2600795FE1 /* FirewallClient.swift */,
85557B0F2B59215F00795FE1 /* FirewallRule.swift */,
85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */,
85E3BDE42B70E18C00FA71FD /* Networking.swift */,
856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */,
+ 85978A532BE0F10E00F999A7 /* PacketCapture.swift */,
+ 8590A5432C2AF43400B9BF7B /* TrafficGenerator.swift */,
);
path = Networking;
sourceTree = "<group>";
@@ -6486,6 +6501,7 @@
8556EB522B9A1C6900D26DD4 /* MullvadApi.swift in Sources */,
85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */,
A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */,
+ 85607C892D131CD500037E34 /* TestRouterAPIClient.swift in Sources */,
85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */,
F09084682C6E88ED001CD36E /* DaitaPromptAlert.swift in Sources */,
8529693C2B4F0257007EAD4C /* Alert.swift in Sources */,
@@ -6508,20 +6524,24 @@
852969352B4E9270007EAD4C /* LoginPage.swift in Sources */,
A998DA832BD2B055001D61A2 /* EditCustomListLocationsPage.swift in Sources */,
7A8A19242CF4C9BF000BCB5B /* MultihopPage.swift in Sources */,
+ 8590A5442C2AF43400B9BF7B /* TrafficGenerator.swift in Sources */,
7ACD79392C0DAADD00DBEE14 /* AddCustomListLocationsPage.swift in Sources */,
8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */,
A9BFAFFF2BD004ED00F2BCA1 /* CustomListsTests.swift in Sources */,
85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */,
- 85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */,
+ 85557B0E2B591B2600795FE1 /* FirewallClient.swift in Sources */,
852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */,
8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */,
85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */,
7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */,
7A8A19262CF4D37B000BCB5B /* DAITAPage.swift in Sources */,
856952DC2BD2922A008C1F84 /* PartnerAPIClient.swift in Sources */,
+ 85F1E17E2C0A256200DB8F55 /* LeakTests.swift in Sources */,
85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */,
855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */,
8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */,
+ 85978A542BE0F10E00F999A7 /* PacketCapture.swift in Sources */,
+ 8555C6602D1030040092DAD0 /* LeakCheck.swift in Sources */,
85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */,
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */,
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */,
diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
index 69f86e52ca..d7443bea8c 100644
--- a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
+++ b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
@@ -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
@@ -136,7 +143,7 @@ class BaseUITestCase: XCTestCase {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
if springboard.buttons["Allow"].waitForExistence(timeout: Self.shortTimeout) {
- let alertAllowButton = springboard.buttons.element(boundBy: 0)
+ let alertAllowButton = springboard.buttons["Allow"]
if alertAllowButton.waitForExistence(timeout: Self.defaultTimeout) {
alertAllowButton.tap()
}
@@ -160,6 +167,29 @@ class BaseUITestCase: XCTestCase {
}
}
+ /// Start packet capture for this test case
+ func startPacketCapture() {
+ currentTestCaseShouldCapturePackets = true
+ packetCaptureSessionIsActive = true
+ let packetCaptureClient = PacketCaptureClient()
+ packetCaptureSession = packetCaptureClient.startCapture()
+ }
+
+ /// Stop the current packet capture and return captured traffic
+ func stopPacketCapture() -> [Stream] {
+ packetCaptureSessionIsActive = false
+ guard let packetCaptureSession else {
+ XCTFail("Trying to stop capture when there is no active capture")
+ return []
+ }
+
+ let packetCaptureAPIClient = PacketCaptureClient()
+ packetCaptureAPIClient.stopCapture(session: packetCaptureSession)
+ let capturedData = packetCaptureAPIClient.getParsedCaptureObjects(session: packetCaptureSession)
+
+ return capturedData
+ }
+
// MARK: - Setup & teardown
/// Override this class function to change the uninstall behaviour in suite level teardown
@@ -176,12 +206,42 @@ 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 = PacketCaptureClient()
+
+ // 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)
+ }
+
+ 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 {
diff --git a/ios/MullvadVPNUITests/ConnectivityTests.swift b/ios/MullvadVPNUITests/ConnectivityTests.swift
index 1cafff9ff3..bcb683583f 100644
--- a/ios/MullvadVPNUITests/ConnectivityTests.swift
+++ b/ios/MullvadVPNUITests/ConnectivityTests.swift
@@ -11,7 +11,7 @@ import Network
import XCTest
class ConnectivityTests: LoggedOutUITestCase {
- let firewallAPIClient = FirewallAPIClient()
+ let firewallAPIClient = FirewallClient()
/// Verifies that the app still functions when API has been blocked
func testAPIConnectionViaBridges() throws {
diff --git a/ios/MullvadVPNUITests/Info.plist b/ios/MullvadVPNUITests/Info.plist
index 229e948327..2bdf415ada 100644
--- a/ios/MullvadVPNUITests/Info.plist
+++ b/ios/MullvadVPNUITests/Info.plist
@@ -24,10 +24,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>
<key>TestDeviceIsIPad</key>
diff --git a/ios/MullvadVPNUITests/LeakTests.swift b/ios/MullvadVPNUITests/LeakTests.swift
new file mode 100644
index 0000000000..a6daffcff3
--- /dev/null
+++ b/ios/MullvadVPNUITests/LeakTests.swift
@@ -0,0 +1,88 @@
+//
+// 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() {
+ FirewallClient().removeRules()
+ super.tearDown()
+ }
+
+ /// 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 testNoLeak() throws {
+ let targetIPAddress = Networking.getAlwaysReachableIPAddress()
+ startPacketCapture()
+ let trafficGenerator = TrafficGenerator(destinationHost: targetIPAddress, port: 80)
+ trafficGenerator.startGeneratingUDPTraffic(interval: 30.0)
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ allowAddVPNConfigurationsIfAsked()
+
+ TunnelControlPage(app)
+ .waitForSecureConnectionLabel()
+
+ // Keep the tunnel connection for a while
+ Thread.sleep(forTimeInterval: 30.0)
+
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+
+ trafficGenerator.stopGeneratingUDPTraffic()
+
+ var capturedStreams = stopPacketCapture()
+ // For now cut the beginning and and end of the stream to trim out the part where the tunnel connection was not up
+ capturedStreams = PacketCaptureClient.trimPackets(streams: capturedStreams, secondsStart: 8, secondsEnd: 3)
+ LeakCheck.assertNoLeaks(streams: capturedStreams, rules: [NoTrafficToHostLeakRule(host: targetIPAddress)])
+ }
+
+ /// Send UDP traffic to a host, connect to relay and then disconnect to intentionally leak traffic and make sure that the test catches the leak
+ func testShouldLeak() throws {
+ let targetIPAddress = Networking.getAlwaysReachableIPAddress()
+ startPacketCapture()
+ let trafficGenerator = TrafficGenerator(destinationHost: targetIPAddress, port: 80)
+ trafficGenerator.startGeneratingUDPTraffic(interval: 1.0)
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ allowAddVPNConfigurationsIfAsked()
+
+ TunnelControlPage(app)
+ .waitForSecureConnectionLabel()
+
+ Thread.sleep(forTimeInterval: 2.0)
+
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+
+ // Give it some time to generate traffic outside of tunnel
+ Thread.sleep(forTimeInterval: 5.0)
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ // Keep the tunnel connection for a while
+ Thread.sleep(forTimeInterval: 5.0)
+
+ app.launch()
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+
+ // Keep the capture open for a while
+ Thread.sleep(forTimeInterval: 15.0)
+ trafficGenerator.stopGeneratingUDPTraffic()
+
+ var capturedStreams = stopPacketCapture()
+ // For now cut the beginning and and end of the stream to trim out the part where the tunnel connection was not up
+ capturedStreams = PacketCaptureClient.trimPackets(streams: capturedStreams, secondsStart: 8, secondsEnd: 3)
+ LeakCheck.assertLeaks(streams: capturedStreams, rules: [NoTrafficToHostLeakRule(host: targetIPAddress)])
+ }
+}
diff --git a/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift b/ios/MullvadVPNUITests/Networking/FirewallClient.swift
index 917ac13fd7..35d8d25968 100644
--- a/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift
+++ b/ios/MullvadVPNUITests/Networking/FirewallClient.swift
@@ -11,20 +11,16 @@ import SystemConfiguration
import UIKit
import XCTest
-class FirewallAPIClient {
+class FirewallClient: TestRouterAPIClient {
// swiftlint:disable force_cast
- let baseURL = URL(
- string:
- Bundle(for: FirewallAPIClient.self).infoDictionary?["FirewallApiBaseURL"] as! String
- )!
- let testDeviceIdentifier = Bundle(for: FirewallAPIClient.self).infoDictionary?["TestDeviceIdentifier"] as! String
+ let testDeviceIdentifier = Bundle(for: FirewallClient.self).infoDictionary?["TestDeviceIdentifier"] as! String
// swiftlint:enable force_cast
lazy var sessionIdentifier = "urn:uuid:" + testDeviceIdentifier
/// Create a new rule associated to the device under test
public func createRule(_ firewallRule: FirewallRule) {
- let createRuleURL = baseURL.appendingPathComponent("rule")
+ let createRuleURL = TestRouterAPIClient.baseURL.appendingPathComponent("rule")
var request = URLRequest(url: createRuleURL)
request.httpMethod = "POST"
@@ -64,7 +60,9 @@ class FirewallAPIClient {
} else {
if let response = requestResponse as? HTTPURLResponse {
if response.statusCode != 201 {
- XCTFail("Failed to create firewall rule - unexpected server response")
+ XCTFail(
+ "Failed to create firewall rule - unexpected response status code \(response.statusCode)"
+ )
}
}
@@ -77,43 +75,9 @@ class FirewallAPIClient {
}
}
- /// Gets the IP address of the device under test
- public func getDeviceIPAddress() throws -> String {
- let deviceIPURL = baseURL.appendingPathComponent("own-ip")
- let request = URLRequest(url: deviceIPURL)
- let completionHandlerInvokedExpectation = XCTestExpectation(
- description: "Completion handler for the request is invoked"
- )
- var deviceIPAddress = ""
- var requestError: Error?
-
- let dataTask = URLSession.shared.dataTask(with: request) { data, _, _ in
- defer { completionHandlerInvokedExpectation.fulfill() }
- guard let data else {
- requestError = NetworkingError.internalError(reason: "Could not get device IP")
- return
- }
-
- deviceIPAddress = String(data: data, encoding: .utf8)!
- }
-
- dataTask.resume()
-
- let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30)
- if waitResult != .completed {
- XCTFail("Failed to get device IP address - timeout")
- }
-
- if let requestError {
- throw requestError
- }
-
- return deviceIPAddress
- }
-
/// Remove all firewall rules associated to this device under test
public func removeRules() {
- let removeRulesURL = baseURL.appendingPathComponent("remove-rules/\(sessionIdentifier)")
+ let removeRulesURL = TestRouterAPIClient.baseURL.appendingPathComponent("remove-rules/\(sessionIdentifier)")
var request = URLRequest(url: removeRulesURL)
request.httpMethod = "DELETE"
diff --git a/ios/MullvadVPNUITests/Networking/FirewallRule.swift b/ios/MullvadVPNUITests/Networking/FirewallRule.swift
index ed5cf01bc7..51d79c1931 100644
--- a/ios/MullvadVPNUITests/Networking/FirewallRule.swift
+++ b/ios/MullvadVPNUITests/Networking/FirewallRule.swift
@@ -9,22 +9,16 @@
import Foundation
import XCTest
-enum NetworkingProtocol: String {
- case TCP = "tcp"
- case UDP = "udp"
- case ICMP = "icmp"
-}
-
struct FirewallRule {
let fromIPAddress: String
let toIPAddress: String
- let protocols: [NetworkingProtocol]
+ let protocols: [NetworkTransportProtocol]
/// - Parameters:
/// - fromIPAddress: Block traffic originating from this source IP address.
/// - toIPAddress: Block traffic to this destination IP address.
/// - protocols: Protocols which should be blocked. If none is specified all will be blocked.
- private init(fromIPAddress: String, toIPAddress: String, protocols: [NetworkingProtocol]) {
+ private init(fromIPAddress: String, toIPAddress: String, protocols: [NetworkTransportProtocol]) {
self.fromIPAddress = fromIPAddress
self.toIPAddress = toIPAddress
self.protocols = protocols
@@ -36,7 +30,7 @@ struct FirewallRule {
/// Make a firewall rule blocking API access for the current device under test
public static func makeBlockAPIAccessFirewallRule() throws -> FirewallRule {
- let deviceIPAddress = try FirewallAPIClient().getDeviceIPAddress()
+ let deviceIPAddress = try FirewallClient().getDeviceIPAddress()
let apiIPAddress = try MullvadAPIWrapper.getAPIIPAddress()
return FirewallRule(
fromIPAddress: deviceIPAddress,
@@ -46,7 +40,7 @@ struct FirewallRule {
}
public static func makeBlockAllTrafficRule(toIPAddress: String) throws -> FirewallRule {
- let deviceIPAddress = try FirewallAPIClient().getDeviceIPAddress()
+ let deviceIPAddress = try FirewallClient().getDeviceIPAddress()
return FirewallRule(
fromIPAddress: deviceIPAddress,
@@ -56,7 +50,7 @@ struct FirewallRule {
}
public static func makeBlockUDPTrafficRule(toIPAddress: String) throws -> FirewallRule {
- let deviceIPAddress = try FirewallAPIClient().getDeviceIPAddress()
+ let deviceIPAddress = try FirewallClient().getDeviceIPAddress()
return FirewallRule(
fromIPAddress: deviceIPAddress,
diff --git a/ios/MullvadVPNUITests/Networking/LeakCheck.swift b/ios/MullvadVPNUITests/Networking/LeakCheck.swift
new file mode 100644
index 0000000000..5c751527ef
--- /dev/null
+++ b/ios/MullvadVPNUITests/Networking/LeakCheck.swift
@@ -0,0 +1,45 @@
+//
+// LeakCheck.swift
+// MullvadVPN
+//
+// Created by Niklas Berglund on 2024-12-16.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import XCTest
+
+class LeakCheck {
+ static func assertNoLeaks(streams: [Stream], rules: [LeakRule]) {
+ XCTAssertFalse(streams.isEmpty, "No streams to leak check")
+ XCTAssertFalse(rules.isEmpty, "No leak rules to check")
+
+ for rule in rules where rule.isViolated(streams: streams) {
+ XCTFail("Leak rule violated")
+ }
+ }
+
+ static func assertLeaks(streams: [Stream], rules: [LeakRule]) {
+ XCTAssertFalse(streams.isEmpty, "No streams to leak check")
+ XCTAssertFalse(rules.isEmpty, "No leak rules to check")
+
+ for rule in rules where rule.isViolated(streams: streams) == false {
+ XCTFail("Leak rule unexpectedly not violated when asserting leak")
+ }
+ }
+}
+
+protocol LeakRule {
+ func isViolated(streams: [Stream]) -> Bool
+}
+
+class NoTrafficToHostLeakRule: LeakRule {
+ let host: String
+
+ init(host: String) {
+ self.host = host
+ }
+
+ func isViolated(streams: [Stream]) -> Bool {
+ streams.filter { $0.destinationAddress == host }.isEmpty == false
+ }
+}
diff --git a/ios/MullvadVPNUITests/Networking/Networking.swift b/ios/MullvadVPNUITests/Networking/Networking.swift
index c260906670..27a47f716d 100644
--- a/ios/MullvadVPNUITests/Networking/Networking.swift
+++ b/ios/MullvadVPNUITests/Networking/Networking.swift
@@ -10,6 +10,12 @@ import Foundation
import Network
import XCTest
+enum NetworkTransportProtocol: String, Codable {
+ case TCP = "tcp"
+ case UDP = "udp"
+ case ICMP = "icmp"
+}
+
enum NetworkingError: Error {
case notConfiguredError
case internalError(reason: String)
@@ -32,16 +38,6 @@ class Networking {
return adServingDomain
}
- /// Get configured domain to use for Internet connectivity checks
- private static func getAlwaysReachableDomain() throws -> String {
- guard let shouldBeReachableDomain = Bundle(for: Networking.self)
- .infoDictionary?["ShouldBeReachableDomain"] as? String else {
- throw NetworkingError.notConfiguredError
- }
-
- return shouldBeReachableDomain
- }
-
/// 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)
@@ -79,6 +75,26 @@ class Networking {
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()
diff --git a/ios/MullvadVPNUITests/Networking/PacketCapture.swift b/ios/MullvadVPNUITests/Networking/PacketCapture.swift
new file mode 100644
index 0000000000..dd80f06580
--- /dev/null
+++ b/ios/MullvadVPNUITests/Networking/PacketCapture.swift
@@ -0,0 +1,333 @@
+//
+// PacketCapture.swift
+// MullvadVPNUITests
+//
+// Created by Niklas Berglund on 2024-04-30.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import XCTest
+
+struct PacketCaptureSession {
+ var identifier = UUID().uuidString
+}
+
+/// Represents a stream in packet capture
+class Stream: Codable, Equatable {
+ static func == (lhs: Stream, rhs: Stream) -> Bool {
+ return lhs.sourceAddress == rhs.sourceAddress &&
+ lhs.destinationAddress == rhs.destinationAddress &&
+ lhs.flowID == rhs.flowID &&
+ lhs.transportProtocol == rhs.transportProtocol
+ }
+
+ let sourceAddress: String
+ let sourcePort: Int
+ let destinationAddress: String
+ let destinationPort: Int
+ let flowID: String?
+ let transportProtocol: NetworkTransportProtocol
+ var packets: [Packet] {
+ didSet {
+ determineDateInterval()
+ }
+ }
+
+ /// Date interval from first to last packet of this stream
+ var dateInterval: DateInterval
+
+ /// Date interval from first to last tx(sent from test device) packet of this stream
+ var txInterval: DateInterval?
+
+ /// Date interval from frist to last rx(sent to test device) packet of this stream
+ var rxInterval: DateInterval?
+
+ enum CodingKeys: String, CodingKey {
+ case sourceAddress = "peer_addr"
+ case destinationAddress = "other_addr"
+ case flowID = "flow_id"
+ case transportProtocol = "transport_protocol"
+ case packets
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.flowID = try container.decodeIfPresent(String.self, forKey: .flowID)
+ self.transportProtocol = try container.decode(NetworkTransportProtocol.self, forKey: .transportProtocol)
+ self.packets = try container.decode([Packet].self, forKey: .packets)
+ dateInterval = DateInterval()
+
+ // Separate source address and port
+ let sourceValue = try container.decode(String.self, forKey: .sourceAddress)
+ let sourceSplit = sourceValue.components(separatedBy: ":")
+ self.sourceAddress = try XCTUnwrap(sourceSplit.first)
+ self.sourcePort = try XCTUnwrap(Int(try XCTUnwrap(sourceSplit.last)))
+
+ // Separate destination address and port
+ let destinationValue = try container.decode(String.self, forKey: .destinationAddress)
+ let destinationSplit = destinationValue.components(separatedBy: ":")
+ self.destinationAddress = try XCTUnwrap(destinationSplit.first)
+ self.destinationPort = try XCTUnwrap(Int(try XCTUnwrap(destinationSplit.last)))
+
+ // Set date interval based on packets' time window
+ determineDateInterval()
+ }
+
+ /// Determine the stream's date interval from the time between first to the last packet
+ private func determineDateInterval() {
+ guard packets.isEmpty == false else {
+ XCTFail("Stream unexpectedly have no packets")
+ return
+ }
+
+ // Identify first tx and rx packets to set as initial values
+ let txPackets = packets.filter { $0.fromPeer == true }.sorted { $0.date < $1.date }
+ let rxPackets = packets.filter { $0.fromPeer == false }.sorted { $0.date < $1.date }
+ let allPackets = packets.sorted { $0.date < $1.date }
+
+ if let firstTxPacket = txPackets.first, let lastTxPacket = txPackets.last {
+ txInterval = DateInterval(start: firstTxPacket.date, end: lastTxPacket.date)
+ }
+
+ if let firstRxPacket = rxPackets.first, let lastRxPacket = rxPackets.last {
+ rxInterval = DateInterval(start: firstRxPacket.date, end: lastRxPacket.date)
+ }
+
+ if let firstPacket = allPackets.first, let lastPacket = allPackets.last {
+ dateInterval = DateInterval(start: firstPacket.date, end: lastPacket.date)
+ }
+ }
+}
+
+/// Represents a packet in packet capture
+class Packet: Codable, Equatable {
+ /// True when packet is sent from device under test, false if from another host
+ public let fromPeer: Bool
+
+ /// Timestamp in microseconds
+ private var timestamp: Int64
+
+ public var date: Date
+
+ enum CodingKeys: String, CodingKey {
+ case fromPeer = "from_peer"
+ case timestamp
+ }
+
+ required init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ fromPeer = try container.decode(Bool.self, forKey: .fromPeer)
+ timestamp = try container.decode(Int64.self, forKey: .timestamp) / 1000000
+ date = Date(timeIntervalSince1970: TimeInterval(timestamp))
+ }
+
+ static func == (lhs: Packet, rhs: Packet) -> Bool {
+ return lhs.fromPeer == rhs.fromPeer &&
+ lhs.timestamp == rhs.timestamp &&
+ lhs.date == rhs.date
+ }
+}
+
+class PacketCaptureClient: TestRouterAPIClient {
+ /// Start a new capture session
+ func startCapture() -> PacketCaptureSession {
+ let session = PacketCaptureSession()
+
+ let jsonDictionary = [
+ "label": session.identifier,
+ ]
+
+ _ = sendRequest(
+ httpMethod: "POST",
+ endpoint: "capture",
+ contentType: "application/json",
+ jsonData: jsonDictionary
+ )
+
+ return session
+ }
+
+ /// Stop capture for session
+ func stopCapture(session: PacketCaptureSession) {
+ _ = sendJSONRequest(httpMethod: "POST", endpoint: "stop-capture/\(session.identifier)", jsonData: nil)
+ }
+
+ /// Cut specified number of seconds from the beginning and end of data capture
+ static func trimPackets(streams: [Stream], secondsStart: Double, secondsEnd: Double) -> [Stream] {
+ var collectionStartDate: Date?
+ var collectionEndDate: Date?
+
+ for stream in streams {
+ if collectionStartDate != nil {
+ collectionStartDate = min(collectionStartDate!, stream.dateInterval.start)
+ } else {
+ collectionStartDate = stream.dateInterval.start
+ }
+
+ if collectionEndDate != nil {
+ collectionEndDate = max(collectionEndDate!, stream.dateInterval.end)
+ } else {
+ collectionEndDate = stream.dateInterval.end
+ }
+ }
+
+ let cutStartDate = collectionStartDate!.addingTimeInterval(secondsStart)
+ let cutEndDate = collectionEndDate!.addingTimeInterval(-secondsEnd)
+
+ var trimmedStreams: [Stream] = []
+ for stream in streams {
+ let packetsWithinTimeframe = stream.packets.filter { packet in
+ return packet.date >= cutStartDate && packet.date <= cutEndDate
+ }
+
+ if packetsWithinTimeframe.isEmpty == false {
+ stream.packets = packetsWithinTimeframe
+ trimmedStreams.append(stream)
+ }
+ }
+
+ return trimmedStreams
+ }
+
+ /// Get captured traffic from this session parsed to objects
+ func getParsedCaptureObjects(session: PacketCaptureSession) -> [Stream] {
+ let parsedData = getParsedCapture(session: session)
+ let decoder = JSONDecoder()
+
+ do {
+ let streams = try decoder.decode([Stream].self, from: parsedData)
+ return streams
+ } catch {
+ XCTFail("Failed to decode parsed capture")
+ return []
+ }
+ }
+
+ /// Get captured traffic from this session parsed to JSON
+ func getParsedCapture(session: PacketCaptureSession) -> Data {
+ var deviceIPAddress: String
+
+ do {
+ deviceIPAddress = try getDeviceIPAddress()
+ } catch {
+ XCTFail("Failed to get device IP address")
+ return Data()
+ }
+
+ let responseData = sendJSONRequest(
+ httpMethod: "PUT",
+ endpoint: "parse-capture/\(session.identifier)",
+ jsonData: [deviceIPAddress]
+ )
+
+ return responseData
+ }
+
+ /// Get PCAP file contents for the capture of this session
+ func getPCAP(session: PacketCaptureSession) -> Data {
+ let response = sendPCAPRequest(httpMethod: "GET", endpoint: "last-capture/\(session.identifier)", jsonData: nil)
+ return response
+ }
+
+ private func sendJSONRequest(httpMethod: String, endpoint: String, jsonData: Any?) -> Data {
+ let responseData = sendRequest(
+ httpMethod: httpMethod,
+ endpoint: endpoint,
+ contentType: "application/json",
+ jsonData: jsonData
+ )
+
+ guard let responseData else {
+ XCTFail("Unexpectedly didn't get any data from JSON request")
+ return Data()
+ }
+
+ return responseData
+ }
+
+ private func sendPCAPRequest(httpMethod: String, endpoint: String, jsonData: Any?) -> Data {
+ let responseData = sendRequest(
+ httpMethod: httpMethod,
+ endpoint: endpoint,
+ contentType: "application/pcap",
+ jsonData: jsonData
+ )
+
+ guard let responseData else {
+ XCTFail("Unexpectedly didn't get any data from response")
+ return Data()
+ }
+
+ XCTAssertFalse(responseData.isEmpty, "PCAP response data should not be empty")
+
+ return responseData
+ }
+
+ private func sendRequest(httpMethod: String, endpoint: String, contentType: String?, jsonData: Any?) -> Data? {
+ let url = TestRouterAPIClient.baseURL.appendingPathComponent(endpoint)
+
+ var request = URLRequest(url: url)
+ request.httpMethod = httpMethod
+
+ if let contentType {
+ request.setValue(contentType, forHTTPHeaderField: "Content-Type")
+ }
+
+ if let jsonData = jsonData {
+ do {
+ request.httpBody = try JSONSerialization.data(withJSONObject: jsonData)
+ } catch {
+ XCTFail("Failed to serialize JSON data")
+ }
+ }
+
+ var requestResponse: URLResponse?
+ var requestError: Error?
+ var responseData: Data?
+
+ let completionHandlerInvokedExpectation = XCTestExpectation(
+ description: "Completion handler for the request is invoked"
+ )
+
+ let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
+ requestResponse = response
+ requestError = error
+
+ guard let data = data,
+ let response = response as? HTTPURLResponse,
+ error == nil else {
+ XCTFail("Error: \(error?.localizedDescription ?? "Unknown error")")
+ return
+ }
+
+ if 200 ... 204 ~= response.statusCode && error == nil {
+ responseData = data
+ } else {
+ XCTFail("Request failed")
+ }
+
+ completionHandlerInvokedExpectation.fulfill()
+ }
+
+ dataTask.resume()
+
+ let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30)
+
+ if waitResult != .completed {
+ XCTFail("Failed to send packet capture API request - timeout")
+ } else {
+ if let response = requestResponse as? HTTPURLResponse {
+ if (200 ... 201 ~= response.statusCode) == false {
+ XCTFail("Packet capture API request failed - unexpected server response")
+ }
+ }
+
+ if let error = requestError {
+ XCTFail("Packet capture API request failed - encountered error \(error.localizedDescription)")
+ }
+ }
+
+ return responseData
+ }
+}
diff --git a/ios/MullvadVPNUITests/Networking/TestRouterAPIClient.swift b/ios/MullvadVPNUITests/Networking/TestRouterAPIClient.swift
new file mode 100644
index 0000000000..7eb1f535dd
--- /dev/null
+++ b/ios/MullvadVPNUITests/Networking/TestRouterAPIClient.swift
@@ -0,0 +1,56 @@
+//
+// TestRouterAPIClient.swift
+// MullvadVPN
+//
+// Created by Niklas Berglund on 2024-12-18.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import XCTest
+
+class TestRouterAPIClient {
+ // swiftlint:disable force_cast
+ static let baseURL = URL(
+ string:
+ Bundle(for: FirewallClient.self).infoDictionary?["FirewallApiBaseURL"] as! String
+ )!
+ // swiftlint:enable force_cast
+
+ static func getIPAddress() throws -> String {
+ return ""
+ }
+
+ /// Gets the IP address of the device under test
+ public func getDeviceIPAddress() throws -> String {
+ let deviceIPURL = TestRouterAPIClient.baseURL.appendingPathComponent("own-ip")
+ let request = URLRequest(url: deviceIPURL)
+ let completionHandlerInvokedExpectation = XCTestExpectation(
+ description: "Completion handler for the request is invoked"
+ )
+ var deviceIPAddress = ""
+ var requestError: Error?
+
+ let dataTask = URLSession.shared.dataTask(with: request) { data, _, _ in
+ defer { completionHandlerInvokedExpectation.fulfill() }
+ guard let data else {
+ requestError = NetworkingError.internalError(reason: "Could not get device IP")
+ return
+ }
+
+ deviceIPAddress = String(data: data, encoding: .utf8)!
+ }
+
+ dataTask.resume()
+
+ let waitResult = XCTWaiter.wait(for: [completionHandlerInvokedExpectation], timeout: 30)
+ if waitResult != .completed {
+ XCTFail("Failed to get device IP address - timeout")
+ }
+
+ if let requestError {
+ throw requestError
+ }
+
+ return deviceIPAddress
+ }
+}
diff --git a/ios/MullvadVPNUITests/Networking/TrafficGenerator.swift b/ios/MullvadVPNUITests/Networking/TrafficGenerator.swift
new file mode 100644
index 0000000000..b339e32819
--- /dev/null
+++ b/ios/MullvadVPNUITests/Networking/TrafficGenerator.swift
@@ -0,0 +1,74 @@
+//
+// TrafficGenerator.swift
+// MullvadVPNUITests
+//
+// Created by Niklas Berglund on 2024-06-25.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Network
+import XCTest
+
+class TrafficGenerator {
+ let destinationHost: String
+ let port: Int
+ let connection: NWConnection
+ let dispatchQueue = DispatchQueue(label: "TrafficGeneratorDispatchQueue", qos: .unspecified)
+ var timer: DispatchSourceTimer
+
+ init(destinationHost: String, port: Int) {
+ self.destinationHost = destinationHost
+ self.port = port
+ connection = NWConnection(
+ host: NWEndpoint.Host(destinationHost),
+ port: NWEndpoint.Port(integerLiteral: UInt16(port)),
+ using: .udp
+ )
+
+ timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
+
+ connect()
+ }
+
+ func connect() {
+ let doneAttemptingConnectExpecation = XCTestExpectation(description: "Done attemping to connect")
+
+ connection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ print("Ready")
+ doneAttemptingConnectExpecation.fulfill()
+ case let .failed(error):
+ print("Failed to connect: \(error)")
+ doneAttemptingConnectExpecation.fulfill()
+ default:
+ break
+ }
+ }
+
+ connection.start(queue: dispatchQueue)
+
+ XCTWaiter().wait(for: [doneAttemptingConnectExpecation], timeout: 10.0)
+ }
+
+ public func startGeneratingUDPTraffic(interval: TimeInterval) {
+ timer.schedule(deadline: .now(), repeating: interval)
+
+ timer.setEventHandler {
+ let data = "dGhpcyBpcyBqdXN0IHNvbWUgZHVtbXkgZGF0YSB0aGlzIGlzIGp1c3Qgc29tZSBkdW".data(using: .utf8)
+ self.connection.send(content: data, completion: .contentProcessed { error in
+ if let error = error {
+ print("Failed to send data: \(error)")
+ } else {
+ print("Data sent")
+ }
+ })
+ }
+
+ timer.activate()
+ }
+
+ public func stopGeneratingUDPTraffic() {
+ timer.cancel()
+ }
+}
diff --git a/ios/MullvadVPNUITests/RelayTests.swift b/ios/MullvadVPNUITests/RelayTests.swift
index 130b252b79..d6b896215c 100644
--- a/ios/MullvadVPNUITests/RelayTests.swift
+++ b/ios/MullvadVPNUITests/RelayTests.swift
@@ -27,7 +27,7 @@ class RelayTests: LoggedInWithTimeUITestCase {
super.tearDown()
if removeFirewallRulesInTearDown {
- FirewallAPIClient().removeRules()
+ FirewallClient().removeRules()
}
}
@@ -102,7 +102,7 @@ class RelayTests: LoggedInWithTimeUITestCase {
}
func testConnectionRetryLogic() throws {
- FirewallAPIClient().removeRules()
+ FirewallClient().removeRules()
removeFirewallRulesInTearDown = true
addTeardownBlock {
@@ -113,7 +113,7 @@ class RelayTests: LoggedInWithTimeUITestCase {
let relayInfo = getDefaultRelayInfo()
// Run actual test
- try FirewallAPIClient().createRule(
+ try FirewallClient().createRule(
FirewallRule.makeBlockAllTrafficRule(toIPAddress: relayInfo.ipAddress)
)
@@ -215,7 +215,7 @@ class RelayTests: LoggedInWithTimeUITestCase {
/// Test automatic switching to TCP is functioning when UDP traffic to relay is blocked. This test first connects to a realy to get the IP address of it, in order to block UDP traffic to this relay.
func testWireGuardOverTCPAutomatically() throws {
- FirewallAPIClient().removeRules()
+ FirewallClient().removeRules()
removeFirewallRulesInTearDown = true
addTeardownBlock {
@@ -226,7 +226,7 @@ class RelayTests: LoggedInWithTimeUITestCase {
let relayInfo = getDefaultRelayInfo()
// Run actual test
- try FirewallAPIClient().createRule(
+ try FirewallClient().createRule(
FirewallRule.makeBlockUDPTrafficRule(toIPAddress: relayInfo.ipAddress)
)
diff --git a/ios/MullvadVPNUITests/tests.json b/ios/MullvadVPNUITests/tests.json
index 3841f81c22..ab6a0ff3de 100644
--- a/ios/MullvadVPNUITests/tests.json
+++ b/ios/MullvadVPNUITests/tests.json
@@ -4,6 +4,7 @@
"AccountTests",
"ConnectivityTests",
"CustomListsTests",
+ "LeakTests",
"RelayTests",
"SettingsTests"
],