summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-05-16 16:06:35 +0200
committerKalle Lindström <karl.lindstrom@mullvad.net>2025-05-19 14:37:07 +0200
commit35d2737bec9c5e7fd31cc81fef227cf923c3ccde (patch)
treebac63c18ca0d81c4b366414d41e4863b1237fd22
parent48dbb105836a36c770e584317dd811f40e9503b6 (diff)
downloadmullvadvpn-35d2737bec9c5e7fd31cc81fef227cf923c3ccde.tar.xz
mullvadvpn-35d2737bec9c5e7fd31cc81fef227cf923c3ccde.zip
Add API blocked e2e connection test
-rw-r--r--android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/misc/ActivityLifecycleCallbacksAdapter.kt21
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt39
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt47
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/relay/RelayApi.kt55
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt11
5 files changed, 167 insertions, 6 deletions
diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/misc/ActivityLifecycleCallbacksAdapter.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/misc/ActivityLifecycleCallbacksAdapter.kt
new file mode 100644
index 0000000000..0d7fc98b5b
--- /dev/null
+++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/misc/ActivityLifecycleCallbacksAdapter.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.test.common.misc
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.os.Bundle
+
+open class ActivityLifecycleCallbacksAdapter : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
+
+ override fun onActivityStarted(activity: Activity) = Unit
+
+ override fun onActivityResumed(activity: Activity) = Unit
+
+ override fun onActivityPaused(activity: Activity) = Unit
+
+ override fun onActivityStopped(activity: Activity) = Unit
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
+
+ override fun onActivityDestroyed(activity: Activity) = Unit
+}
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
index 0d0f3d6262..4720fc5ce6 100644
--- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
@@ -16,10 +16,10 @@ import net.mullvad.mullvadvpn.test.common.page.on
import net.mullvad.mullvadvpn.test.common.rule.ForgetAllVpnAppsInSettingsTestRule
import net.mullvad.mullvadvpn.test.e2e.annotations.HasDependencyOnLocalAPI
import net.mullvad.mullvadvpn.test.e2e.api.connectioncheck.ConnectionCheckApi
+import net.mullvad.mullvadvpn.test.e2e.api.relay.RelayApi
import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule
import net.mullvad.mullvadvpn.test.e2e.misc.ClearFirewallRules
import net.mullvad.mullvadvpn.test.e2e.router.firewall.DropRule
-import net.mullvad.mullvadvpn.test.e2e.router.firewall.FirewallClient
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -33,7 +33,7 @@ class ConnectionTest : EndToEndTest() {
val forgetAllVpnAppsInSettingsTestRule = ForgetAllVpnAppsInSettingsTestRule()
private val connCheckClient = ConnectionCheckApi()
- private val firewallClient = FirewallClient()
+ private val relayClient = RelayApi()
@Test
fun testConnect() {
@@ -241,6 +241,41 @@ class ConnectionTest : EndToEndTest() {
}
}
+ @Test
+ @HasDependencyOnLocalAPI
+ @ClearFirewallRules
+ fun testApiUnavailable() = runTest {
+ val testRelayIp = relayClient.getDefaultRelayIpAddress()
+
+ app.launchAndLogIn(accountTestRule.validAccountNumber)
+ on<ConnectPage>()
+
+ // Block everything except the default relay IP. After this the API is no longer reachable.
+ val firewallRule = DropRule.blockAllTrafficExceptToDestinationRule(testRelayIp)
+ firewallClient.createRule(firewallRule)
+
+ // Restarting the activity will re-create the daemon which will try to reach the API.
+ targetActivity.finishAffinity()
+ app.launch()
+
+ on<ConnectPage> { clickSelectLocation() }
+
+ on<SelectLocationPage> {
+ clickLocationExpandButton(DEFAULT_COUNTRY)
+ clickLocationExpandButton(DEFAULT_CITY)
+ clickLocationCell(DEFAULT_RELAY)
+ }
+
+ device.acceptVpnPermissionDialog()
+
+ // Test that we can still connect to the relay even though the API is blocked.
+ on<ConnectPage> {
+ waitForConnectedLabel()
+ clickDisconnect()
+ waitForDisconnectedLabel()
+ }
+ }
+
companion object {
const val VERY_FORGIVING_WIREGUARD_OFF_CONNECTION_TIMEOUT = 60000L
const val UNSUCCESSFUL_CONNECTION_TIMEOUT = 60000L
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt
index e944dd3606..cbad74981c 100644
--- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt
@@ -1,16 +1,22 @@
package net.mullvad.mullvadvpn.test.e2e
import android.Manifest
-import android.content.Context
+import android.app.Activity
+import android.app.Application
import android.os.Build
+import android.os.Bundle
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import co.touchlab.kermit.Logger
import de.mannodermaus.junit5.extensions.GrantPermissionExtension
+import kotlinx.coroutines.runBlocking
import net.mullvad.mullvadvpn.test.common.interactor.AppInteractor
+import net.mullvad.mullvadvpn.test.common.misc.ActivityLifecycleCallbacksAdapter
import net.mullvad.mullvadvpn.test.common.misc.CaptureScreenRecordingsExtension
import net.mullvad.mullvadvpn.test.common.rule.CaptureScreenshotOnFailedTestRule
import net.mullvad.mullvadvpn.test.e2e.constant.LOG_TAG
+import net.mullvad.mullvadvpn.test.e2e.router.firewall.FirewallClient
+import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.RegisterExtension
@@ -33,7 +39,8 @@ abstract class EndToEndTest {
})
lateinit var device: UiDevice
- lateinit var targetContext: Context
+ lateinit var targetApplication: Application
+ lateinit var targetActivity: Activity
lateinit var app: AppInteractor
@BeforeEach
@@ -41,11 +48,25 @@ abstract class EndToEndTest {
Logger.setTag(LOG_TAG)
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ targetApplication =
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ as Application
- app = AppInteractor(device, targetContext)
+ app = AppInteractor(device, targetApplication)
+
+ registerActivityLifecycleCallbacks(targetApplication)
}
+ private fun registerActivityLifecycleCallbacks(app: Application) =
+ app.registerActivityLifecycleCallbacks(
+ object : ActivityLifecycleCallbacksAdapter() {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ Logger.d("onActivityCreated")
+ targetActivity = activity
+ }
+ }
+ )
+
companion object {
const val DEFAULT_COUNTRY = "Sweden"
const val DEFAULT_CITY = "Gothenburg"
@@ -54,5 +75,23 @@ abstract class EndToEndTest {
const val DAITA_COMPATIBLE_COUNTRY = "Relay Software Country"
const val DAITA_COMPATIBLE_CITY = "Relay Software city"
const val DAITA_COMPATIBLE_RELAY = "se-got-wg-002"
+
+ val firewallClient = FirewallClient()
+
+ @JvmStatic
+ @BeforeAll
+ // There are certain scenarios where old rules from previous tests runs may remain on
+ // the router and cause issues, so attempt to clear them before any test setup is done.
+ fun clearFirewallRules() {
+ runBlocking {
+ try {
+ firewallClient.removeAllRules()
+ } catch (e: Exception) {
+ // If the router can't be reached we ignore the error because the e2e
+ // test that is about to be run may not require router access.
+ Logger.e("firewallClient.removeAllRules() failed")
+ }
+ }
+ }
}
}
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/relay/RelayApi.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/relay/RelayApi.kt
new file mode 100644
index 0000000000..2b2017f210
--- /dev/null
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/relay/RelayApi.kt
@@ -0,0 +1,55 @@
+package net.mullvad.mullvadvpn.test.e2e.api.relay
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.request.get
+import io.ktor.http.ContentType
+import io.ktor.http.URLProtocol.Companion.HTTPS
+import io.ktor.http.contentType
+import io.ktor.http.path
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import net.mullvad.mullvadvpn.test.e2e.BuildConfig
+import net.mullvad.mullvadvpn.test.e2e.EndToEndTest.Companion.DEFAULT_RELAY
+import net.mullvad.mullvadvpn.test.e2e.misc.KermitLogger
+
+class RelayApi {
+ private val client: HttpClient =
+ HttpClient(CIO) {
+ install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
+ install(Logging) {
+ logger = KermitLogger()
+ level = LogLevel.INFO
+ }
+ defaultRequest {
+ url {
+ protocol = HTTPS
+ host = BASE_URL
+ }
+ contentType(ContentType.Application.Json)
+ }
+ expectSuccess = true
+ }
+
+ suspend fun getDefaultRelayIpAddress(): String =
+ withContext(Dispatchers.IO) {
+ val body = client.get { url { path(RELAY_PATH) } }.body<String>()
+ val ipRegex =
+ """${DEFAULT_RELAY}.+?ipv4_addr_in":"(.+?)"""".toRegex(RegexOption.DOT_MATCHES_ALL)
+
+ ipRegex.find(body)?.groups?.get(1)?.value
+ ?: error("Could not find $DEFAULT_RELAY IP address in relay list")
+ }
+
+ companion object {
+ private const val BASE_URL = "api.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}"
+ private const val RELAY_PATH = "app/${BuildConfig.API_VERSION}/relays"
+ }
+}
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt
index 341fe7b394..6bdd4bedf0 100644
--- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt
@@ -16,6 +16,7 @@ data class DropRule(
@SerialName("dst") val destination: String,
val protocols: List<NetworkingProtocol>,
@EncodeDefault val label: String = "urn:uuid:${SessionIdentifier.fromDeviceIdentifier()}",
+ @SerialName("block_all_except_dst") val blockAllExceptDestination: Boolean = false,
) {
companion object {
fun blockUDPTrafficRule(to: String): DropRule {
@@ -29,5 +30,15 @@ data class DropRule(
fun blockWireGuardTrafficRule(to: String): DropRule =
blockUDPTrafficRule(to).copy(protocols = listOf(NetworkingProtocol.WireGuard))
+
+ fun blockAllTrafficExceptToDestinationRule(to: String): DropRule {
+ val testDeviceIpAddress = Networking.getDeviceIpv4Address()
+ return DropRule(
+ source = testDeviceIpAddress,
+ destination = to,
+ protocols = emptyList(),
+ blockAllExceptDestination = true,
+ )
+ }
}
}