Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement packet capture client and models #6317

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ios/Configurations/UITests.xcconfig.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,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
21 changes: 21 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -647,13 +647,16 @@
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 */; };
85978A542BE0F10E00F999A7 /* PacketCaptureAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.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 */; };
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 @@ -1953,11 +1956,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>"; };
85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketCaptureAPIClient.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>"; };
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 @@ -3894,12 +3900,15 @@
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 */,
);
path = MullvadVPNUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -3947,10 +3956,19 @@
85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */,
85E3BDE42B70E18C00FA71FD /* Networking.swift */,
856952DB2BD2922A008C1F84 /* PartnerAPIClient.swift */,
85978A532BE0F10E00F999A7 /* PacketCaptureAPIClient.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 @@ -6021,6 +6039,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 @@ -6045,9 +6064,11 @@
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 */,
855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */,
8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */,
85978A542BE0F10E00F999A7 /* PacketCaptureAPIClient.swift in Sources */,
85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */,
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */,
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */,
Expand Down
61 changes: 61 additions & 0 deletions 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 @@ -142,6 +149,29 @@ class BaseUITestCase: XCTestCase {
}
}

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

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

let packetCaptureAPIClient = PacketCaptureAPIClient()
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
Expand All @@ -158,12 +188,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 = PacketCaptureAPIClient()

// 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
}
}
2 changes: 2 additions & 0 deletions ios/MullvadVPNUITests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<string>$(NO_TIME_ACCOUNT_NUMBER)</string>
<key>PartnerApiToken</key>
<string>$(PARTNER_API_TOKEN)</string>
<key>PacketCaptureAPIBaseURL</key>
<string>$(PACKET_CAPTURE_BASE_URL)</string>
<key>ShouldBeReachableDomain</key>
<string>$(SHOULD_BE_REACHABLE_DOMAIN)</string>
<key>TestDeviceIdentifier</key>
Expand Down
40 changes: 40 additions & 0 deletions ios/MullvadVPNUITests/LeakTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// LeakTests.swift
// MullvadVPNUITests
//
// Created by Niklas Berglund on 2024-05-31.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import XCTest

class LeakTests: LoggedInWithTimeUITestCase {
/// For now just the skeleton of a leak test - traffic is captured and parsed, but not analyzed
func testLeaks() throws {
startPacketCapture()

TunnelControlPage(app)
.tapSecureConnectionButton()

allowAddVPNConfigurationsIfAsked()

TunnelControlPage(app)
.waitForSecureConnectionLabel()

// Trigger traffic by navigating to website in Safari
let safariApp = SafariApp()
safariApp.launch()
safariApp.tapAddressBar()
safariApp.enterText("mullvad.net\n")

app.launch()
TunnelControlPage(app)
.tapDisconnectButton()

// Keep the capture open for a while
Thread.sleep(forTimeInterval: 5.0)
let capturedTraffic = stopPacketCapture()

// Analyze captured traffic
}
}
10 changes: 2 additions & 8 deletions ios/MullvadVPNUITests/Networking/FirewallRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion ios/MullvadVPNUITests/Networking/Networking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -44,7 +50,7 @@ class Networking {
interfaceAddress.sa_family == UInt8(AF_INET) {
// Check if interface is en0 which is the WiFi connection on the iPhone
let name = String(cString: interfacePointer.pointee.ifa_name)
if name == "en0" {
if name.hasPrefix("en") {
// Convert interface address to a human readable string:
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
if getnameinfo(
Expand Down
Loading
Loading