From cc8b8f91658bb7575fb977a5a8841914b7b84e2d Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Fri, 10 May 2024 17:15:41 +0900 Subject: [PATCH] Add QUIC testing to test preparer In addition to verifying that the wifi network is validated, also verify that it can use QUIC on both IPv4 and IPv6. This makes setup issues more obvious and avoids blaming individual tests, by failing the whole module in case devices are connected to access points that do not meet the test prerequisites. The QUIC test (including the test packet) is mostly taken from NativeMultinetworkJni.cpp and written in Kotlin. Test: atest Change-Id: I1c0381e1ad4146d4827fbc1bf095da13dfe3d124 --- .../ConnectivityCheckTest.kt | 152 +++++++++++++++++- .../com/android/testutils/ConnectUtil.kt | 7 +- 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt index e634f0e20b..8e27c627bc 100644 --- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt +++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt @@ -16,27 +16,167 @@ package com.android.testutils.connectivitypreparer +import android.Manifest.permission.NETWORK_SETTINGS import android.content.pm.PackageManager.FEATURE_TELEPHONY import android.content.pm.PackageManager.FEATURE_WIFI +import android.net.LinkAddress +import android.net.Network +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.wifi.WifiInfo import android.telephony.TelephonyManager +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.android.net.module.util.HexDump +import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY +import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY +import com.android.testutils.AutoReleaseNetworkCallbackRule import com.android.testutils.ConnectUtil +import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged +import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged +import com.android.testutils.runAsShell +import com.android.testutils.tryTest +import java.io.IOException +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.util.Random +import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +private const val QUIC_SOCKET_TIMEOUT_MS = 5_000 +private const val QUIC_RETRY_COUNT = 5 + @RunWith(AndroidJUnit4::class) class ConnectivityCheckTest { + @get:Rule + val networkCallbackRule = AutoReleaseNetworkCallbackRule() + + private val logTag = ConnectivityCheckTest::class.simpleName private val context by lazy { InstrumentationRegistry.getInstrumentation().context } private val pm by lazy { context.packageManager } private val connectUtil by lazy { ConnectUtil(context) } + // Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will + // still fail even if the preparer does not. + private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains( + WifiInfo.sanitizeSsid(wifiSsid)) + @Test fun testCheckWifiSetup() { if (!pm.hasSystemFeature(FEATURE_WIFI)) return connectUtil.ensureWifiValidated() + + val (wifiNetwork, wifiSsid) = runAsShell(NETWORK_SETTINGS) { + val cb = networkCallbackRule.requestNetwork( + NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET) + .build() + ) + val capChanged = cb.eventuallyExpect(from = 0) + val network = capChanged.network + val ssid = capChanged.caps.ssid + assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network") + // Expect a global IPv6 address, and native or stacked IPv4 + val lpChange = cb.history.poll( + pos = 0, + timeoutMs = 30_000L + ) { + it is LinkPropertiesChanged && + it.network == network && + it.lp.allLinkAddresses.any(LinkAddress::isIpv4) && + (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address()) + } + assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" + + if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address") + + Pair(network, ssid) + } + + // Checking QUIC is more important on Wi-Fi than cellular, as it finds firewall + // configuration problems on Wi-Fi, but cellular is not actionable by the test lab. + checkQuic(wifiNetwork, wifiSsid, ipv6 = false) + if (!ipv6Unsupported(wifiSsid)) { + checkQuic(wifiNetwork, wifiSsid, ipv6 = true) + } + } + + /** + * Check that QUIC is working on the specified network. + * + * Some tests require QUIC (UDP), and some lab networks have been observed to not let it + * through due to firewalling. Ensure that devices are setup on a network that has the proper + * allowlists before trying to run the tests. + */ + private fun checkQuic(network: Network, ssid: String, ipv6: Boolean) { + // Same endpoint as used in MultinetworkApiTest in CTS + val hostname = "connectivitycheck.android.com" + val targetAddrs = network.getAllByName(hostname) + val bindAddr = if (ipv6) IPV6_ADDR_ANY else IPV4_ADDR_ANY + if (targetAddrs.isEmpty()) { + Log.d(logTag, "No addresses found for $hostname") + return + } + + val socket = DatagramSocket(0, bindAddr) + tryTest { + socket.soTimeout = QUIC_SOCKET_TIMEOUT_MS + network.bindSocket(socket) + + // For reference see Version-Independent Properties of QUIC: + // https://datatracker.ietf.org/doc/html/rfc8999 + // This packet just contains a long header with an unsupported version number, to force + // a version-negotiation packet in response. + val connectionId = ByteArray(8).apply { Random().nextBytes(this) } + val quicData = byteArrayOf( + // long header + 0xc0.toByte(), + // version number (should be an unknown version for the server) + 0xaa.toByte(), 0xda.toByte(), 0xca.toByte(), 0xca.toByte(), + // destination connection ID length + 0x08, + ) + connectionId + byteArrayOf( + // source connection ID length + 0x00, + ) + ByteArray(1185) // Ensure the packet is 1200 bytes long + val targetAddr = targetAddrs.firstOrNull { it.javaClass == bindAddr.javaClass } + ?: fail("No ${bindAddr.javaClass} found for $hostname " + + "(got ${targetAddrs.joinToString()})") + repeat(QUIC_RETRY_COUNT) { i -> + socket.send(DatagramPacket(quicData, quicData.size, targetAddr, 443)) + + val receivedPacket = DatagramPacket(ByteArray(1500), 1500) + try { + socket.receive(receivedPacket) + } catch (e: IOException) { + Log.d(logTag, "No response from $hostname ($targetAddr) on QUIC try $i", e) + return@repeat + } + + val receivedConnectionId = receivedPacket.data.copyOfRange(7, 7 + 8) + if (connectionId.contentEquals(receivedConnectionId)) { + return@tryTest + } else { + val headerBytes = receivedPacket.data.copyOfRange( + 0, receivedPacket.length.coerceAtMost(15)) + Log.d(logTag, "Received invalid connection ID on QUIC try $i: " + + HexDump.toHexString(headerBytes)) + } + } + fail("QUIC is not working on SSID $ssid connecting to $targetAddr " + + "with local source port ${socket.localPort}: check the firewall for UDP port " + + "443 access." + ) + } cleanup { + socket.close() + } } @Test @@ -53,12 +193,16 @@ class ConnectivityCheckTest { if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) { fail("The device has no SIM card inserted. $commonError") } else if (tm.simState != TelephonyManager.SIM_STATE_READY) { - fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " + - commonError) + fail( + "The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " + + commonError + ) } - assertTrue(tm.isDataConnectivityPossible, + assertTrue( + tm.isDataConnectivityPossible, "The device has a SIM card, but it does not supports data connectivity. " + - "Check the data plan, and verify that mobile data is working. " + commonError) + "Check the data plan, and verify that mobile data is working. " + commonError + ) connectUtil.ensureCellularValidated() } } diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt index 3857810b08..d60ab5960f 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt @@ -53,6 +53,10 @@ private const val WIFI_ERROR_IN_PROGRESS = 1 private const val WIFI_ERROR_BUSY = 2 class ConnectUtil(private val context: Context) { + companion object { + @JvmStatic + val VIRTUAL_SSIDS = listOf("VirtWifi", "AndroidWifi") + } private val TAG = ConnectUtil::class.java.simpleName private val cm = context.getSystemService(ConnectivityManager::class.java) @@ -207,9 +211,8 @@ class ConnectUtil(private val context: Context) { */ private fun maybeConfigureVirtualNetwork(scanResults: List): WifiConfiguration? { // Virtual wifi networks used on the emulator and cloud testing infrastructure - val virtualSsids = listOf("VirtWifi", "AndroidWifi") Log.d(TAG, "Wifi scan results: $scanResults") - val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) } + val virtualScanResult = scanResults.firstOrNull { VIRTUAL_SSIDS.contains(it.SSID) } ?: return null // Only add the virtual configuration if the virtual AP is detected in scans