diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-05-16 16:06:35 +0200 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-05-19 14:37:07 +0200 |
| commit | 35d2737bec9c5e7fd31cc81fef227cf923c3ccde (patch) | |
| tree | bac63c18ca0d81c4b366414d41e4863b1237fd22 | |
| parent | 48dbb105836a36c770e584317dd811f40e9503b6 (diff) | |
| download | mullvadvpn-35d2737bec9c5e7fd31cc81fef227cf923c3ccde.tar.xz mullvadvpn-35d2737bec9c5e7fd31cc81fef227cf923c3ccde.zip | |
Add API blocked e2e connection test
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, + ) + } } } |
