blob: a70b85ede6207981c1188c6d3d850f2d9eb1b537 [file] [log] [blame]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ktlint does not allow annotating function argument literals inline. Disable the specific rule
// since this negatively affects readability.
@file:Suppress("ktlint:standard:comment-wrapping")
package android.net.cts
import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.apf.ApfCapabilities
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.system.Os
import android.system.OsConstants
import android.system.OsConstants.AF_INET
import android.system.OsConstants.IPPROTO_ICMP
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_NONBLOCK
import android.util.Log
import androidx.test.filters.RequiresDevice
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
import com.android.compatibility.common.util.SystemUtil.runShellCommand
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.internal.util.HexDump
import com.android.net.module.util.PacketReader
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.NetworkStackModuleTest
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.SkipPresubmit
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import com.google.common.truth.TruthJUnit.assume
import java.io.FileDescriptor
import java.lang.Thread
import java.net.InetSocketAddress
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.random.Random
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
private const val TAG = "ApfIntegrationTest"
private const val TIMEOUT_MS = 2000L
private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
private const val POLLING_INTERVAL_MS: Int = 100
private const val RCV_BUFFER_SIZE = 1480
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
@RunWith(DevSdkIgnoreRunner::class)
@RequiresDevice
@NetworkStackModuleTest
// ByteArray.toHexString is experimental API
@kotlin.ExperimentalStdlibApi
class ApfIntegrationTest {
companion object {
private val PING_DESTINATION = InetSocketAddress("8.8.8.8", 0)
@BeforeClass
@JvmStatic
@Suppress("ktlint:standard:no-multi-spaces")
fun setupOnce() {
// TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
// created.
// APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
// LegacyApfFilter.java from being used.
runAsShell(WRITE_DEVICE_CONFIG) {
DeviceConfig.setProperty(
NAMESPACE_CONNECTIVITY,
APF_NEW_RA_FILTER_VERSION,
"1", // value => force enabled
false // makeDefault
)
}
}
}
class IcmpPacketReader(
handler: Handler,
private val network: Network
) : PacketReader(handler, RCV_BUFFER_SIZE) {
private var sockFd: FileDescriptor? = null
private var futureReply: CompletableFuture<ByteArray>? = null
override fun createFd(): FileDescriptor {
// sockFd is closed by calling super.stop()
val sock = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_ICMP)
// APF runs only on WiFi, so make sure the socket is bound to the right network.
network.bindSocket(sock)
sockFd = sock
return sock
}
override fun handlePacket(recvbuf: ByteArray, length: Int) {
// Only copy the ping data and complete the future.
val result = recvbuf.sliceArray(8..<length)
Log.i(TAG, "Received ping reply: ${result.toHexString()}")
futureReply!!.complete(recvbuf.sliceArray(8..<length))
}
fun sendPing(data: ByteArray) {
require(data.size == 56)
// rfc792: Echo (type 0x08) or Echo Reply (type 0x00) Message:
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Code | Checksum |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Identifier | Sequence Number |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Data ...
// +-+-+-+-+-
val icmpHeader = byteArrayOf(0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val packet = icmpHeader + data
Log.i(TAG, "Sent ping: ${packet.toHexString()}")
futureReply = CompletableFuture<ByteArray>()
Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
}
fun expectPingReply(): ByteArray {
return futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
fun expectPingDropped() {
assertFailsWith(TimeoutException::class) {
futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
}
override fun start(): Boolean {
// Ignore the fact start() could return false or throw an exception.
handler.post({ super.start() })
handler.waitForIdle(TIMEOUT_MS)
return true
}
override fun stop() {
handler.post({ super.stop() })
handler.waitForIdle(TIMEOUT_MS)
}
}
@get:Rule
val ignoreRule = DevSdkIgnoreRule()
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
private val pm by lazy { context.packageManager }
private val powerManager by lazy { context.getSystemService(PowerManager::class.java)!! }
private val wakeLock by lazy { powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG) }
private lateinit var network: Network
private lateinit var ifname: String
private lateinit var networkCallback: TestableNetworkCallback
private lateinit var caps: ApfCapabilities
private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
private val handler = Handler(handlerThread.looper)
private lateinit var packetReader: IcmpPacketReader
fun getApfCapabilities(): ApfCapabilities {
val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
if (caps.isEmpty()) {
return ApfCapabilities(0, 0, 0)
}
val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
return ApfCapabilities(version, maxLen, packetFormat)
}
fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
var polling_time = 0
do {
Thread.sleep(POLLING_INTERVAL_MS.toLong())
polling_time += POLLING_INTERVAL_MS
if (condition()) return true
} while (polling_time < timeout_ms)
return false
}
fun turnScreenOff() {
if (!wakeLock.isHeld()) wakeLock.acquire()
runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
assertThat(result).isTrue()
}
fun turnScreenOn() {
if (wakeLock.isHeld()) wakeLock.release()
runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
assertThat(result).isTrue()
}
@Before
fun setUp() {
assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
// APF must run when the screen is off and the device is not interactive.
// TODO: consider running some of the tests with screen on (capabilities, read / write).
turnScreenOff()
networkCallback = TestableNetworkCallback()
cm.requestNetwork(
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(),
networkCallback
)
network = networkCallback.expect<Available>().network
networkCallback.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
ifname = assertNotNull(it.lp.interfaceName)
true
}
// It's possible the device does not support APF, in which case this command will not be
// successful. Ignore the error as testApfCapabilities() already asserts APF support on the
// respective VSR releases and all other tests are based on the capabilities indicated.
runShellCommand("cmd network_stack apf $ifname pause")
caps = getApfCapabilities()
packetReader = IcmpPacketReader(handler, network)
packetReader.start()
}
@After
fun tearDown() {
if (::packetReader.isInitialized) {
packetReader.stop()
}
handlerThread.quitSafely()
handlerThread.join()
if (::ifname.isInitialized) {
runShellCommand("cmd network_stack apf $ifname resume")
}
if (::networkCallback.isInitialized) {
cm.unregisterNetworkCallback(networkCallback)
}
turnScreenOn()
}
@Test
fun testApfCapabilities() {
// APF became mandatory in Android 14 VSR.
assume().that(getVsrApiLevel()).isAtLeast(34)
// ApfFilter does not support anything but ARPHRD_ETHER.
assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
// DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
// - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
// the getApfPacketFilterCapabilities HAL method.
// - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
// TODO: check whether above text should be changed "34 or higher"
// This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
// currently no tests for APFv6 and there cannot be a valid implementation as the
// interpreter has yet to be finalized.
assertThat(caps.apfVersionSupported).isEqualTo(4)
assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
// DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2000 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
if (getVsrApiLevel() >= 202404) {
assertThat(caps.maximumApfProgramSize).isAtLeast(2000)
}
}
// APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
fun assumeApfVersionSupportAtLeast(version: Int) {
assume().that(caps.apfVersionSupported).isAtLeast(version)
}
fun installProgram(bytes: ByteArray) {
val prog = bytes.toHexString()
val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
// runShellCommandOrThrow only throws on S+.
assertThat(result).isEqualTo("success")
}
fun readProgram(): ByteArray {
val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
// runShellCommandOrThrow only throws on S+.
assertThat(progHexString).isNotEmpty()
return HexDump.hexStringToByteArray(progHexString)
}
@SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
// APF integration is mostly broken before V, only run the full read / write test on V+.
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
fun testReadWriteProgram() {
assumeApfVersionSupportAtLeast(4)
// Only test down to 2 bytes. The first byte always stays PASS.
val program = ByteArray(caps.maximumApfProgramSize)
for (i in caps.maximumApfProgramSize downTo 2) {
// Randomize bytes in range [1, i). And install first [0, i) bytes of program.
// Note that only the very first instruction (PASS) is valid APF bytecode.
Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
installProgram(program.sliceArray(0..<i))
// Compare entire memory region.
val readResult = readProgram()
assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
}
}
// TODO: this is a placeholder test to test the IcmpPacketReader functionality and will soon be
// replaced by a real test.
@Test
fun testPing() {
val data = ByteArray(56)
Random.nextBytes(data)
packetReader.sendPing(data)
assertThat(packetReader.expectPingReply()).isEqualTo(data)
}
}