1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 // ktlint does not allow annotating function argument literals inline. Disable the specific rule
17 // since this negatively affects readability.
18 @file:Suppress("ktlint:standard:comment-wrapping")
19 
20 package android.net.cts
21 
22 import android.Manifest.permission.WRITE_DEVICE_CONFIG
23 import android.content.pm.PackageManager.FEATURE_WIFI
24 import android.net.ConnectivityManager
25 import android.net.Network
26 import android.net.NetworkCapabilities
27 import android.net.NetworkRequest
28 import android.net.apf.ApfCapabilities
29 import android.net.apf.ApfConstants.ETH_ETHERTYPE_OFFSET
30 import android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET
31 import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
32 import android.net.apf.ApfV4Generator
33 import android.net.apf.BaseApfGenerator
34 import android.net.apf.BaseApfGenerator.MemorySlot
35 import android.net.apf.BaseApfGenerator.Register.R0
36 import android.net.apf.BaseApfGenerator.Register.R1
37 import android.os.Build
38 import android.os.Handler
39 import android.os.HandlerThread
40 import android.os.PowerManager
41 import android.platform.test.annotations.AppModeFull
42 import android.provider.DeviceConfig
43 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
44 import android.system.Os
45 import android.system.OsConstants
46 import android.system.OsConstants.AF_INET6
47 import android.system.OsConstants.ETH_P_IPV6
48 import android.system.OsConstants.IPPROTO_ICMPV6
49 import android.system.OsConstants.SOCK_DGRAM
50 import android.system.OsConstants.SOCK_NONBLOCK
51 import android.util.Log
52 import androidx.test.filters.RequiresDevice
53 import androidx.test.platform.app.InstrumentationRegistry
54 import com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel
55 import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
56 import com.android.compatibility.common.util.SystemUtil.runShellCommand
57 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
58 import com.android.compatibility.common.util.VsrTest
59 import com.android.internal.util.HexDump
60 import com.android.net.module.util.PacketReader
61 import com.android.testutils.DevSdkIgnoreRule
62 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
63 import com.android.testutils.DevSdkIgnoreRunner
64 import com.android.testutils.NetworkStackModuleTest
65 import com.android.testutils.RecorderCallback.CallbackEntry.Available
66 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
67 import com.android.testutils.SkipPresubmit
68 import com.android.testutils.TestableNetworkCallback
69 import com.android.testutils.runAsShell
70 import com.android.testutils.waitForIdle
71 import com.google.common.truth.Expect
72 import com.google.common.truth.Truth.assertThat
73 import com.google.common.truth.Truth.assertWithMessage
74 import com.google.common.truth.TruthJUnit.assume
75 import java.io.FileDescriptor
76 import java.lang.Thread
77 import java.net.InetSocketAddress
78 import java.nio.ByteBuffer
79 import java.util.concurrent.CompletableFuture
80 import java.util.concurrent.TimeUnit
81 import java.util.concurrent.TimeoutException
82 import kotlin.random.Random
83 import kotlin.test.assertFailsWith
84 import kotlin.test.assertNotNull
85 import org.junit.After
86 import org.junit.AfterClass
87 import org.junit.Before
88 import org.junit.BeforeClass
89 import org.junit.Rule
90 import org.junit.Test
91 import org.junit.runner.RunWith
92 
93 private const val TAG = "ApfIntegrationTest"
94 private const val TIMEOUT_MS = 2000L
95 private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
96 private const val POLLING_INTERVAL_MS: Int = 100
97 private const val RCV_BUFFER_SIZE = 1480
98 private const val PING_HEADER_LENGTH = 8
99 
100 @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
101 @RunWith(DevSdkIgnoreRunner::class)
102 @RequiresDevice
103 @NetworkStackModuleTest
104 // ByteArray.toHexString is experimental API
105 @kotlin.ExperimentalStdlibApi
106 class ApfIntegrationTest {
107     companion object {
108         private val PING_DESTINATION = InetSocketAddress("2001:4860:4860::8888", 0)
109 
110         private val context = InstrumentationRegistry.getInstrumentation().context
111         private val powerManager = context.getSystemService(PowerManager::class.java)!!
112         private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
113 
pollingChecknull114         fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
115             var polling_time = 0
116             do {
117                 Thread.sleep(POLLING_INTERVAL_MS.toLong())
118                 polling_time += POLLING_INTERVAL_MS
119                 if (condition()) return true
120             } while (polling_time < timeout_ms)
121             return false
122         }
123 
turnScreenOffnull124         fun turnScreenOff() {
125             if (!wakeLock.isHeld()) wakeLock.acquire()
126             runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
127             val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
128             assertThat(result).isTrue()
129         }
130 
turnScreenOnnull131         fun turnScreenOn() {
132             if (wakeLock.isHeld()) wakeLock.release()
133             runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
134             val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
135             assertThat(result).isTrue()
136         }
137 
138         @BeforeClass
139         @JvmStatic
140         @Suppress("ktlint:standard:no-multi-spaces")
setupOncenull141         fun setupOnce() {
142             // TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
143             // test infrastructure. Consider saving excepion and throwing it in setUp().
144             // APF must run when the screen is off and the device is not interactive.
145             turnScreenOff()
146             // Wait for APF to become active.
147             Thread.sleep(1000)
148             // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
149             // created.
150             // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
151             // LegacyApfFilter.java from being used.
152             runAsShell(WRITE_DEVICE_CONFIG) {
153                 DeviceConfig.setProperty(
154                         NAMESPACE_CONNECTIVITY,
155                         APF_NEW_RA_FILTER_VERSION,
156                         "1",  // value => force enabled
157                         false // makeDefault
158                 )
159             }
160         }
161 
162         @AfterClass
163         @JvmStatic
tearDownOncenull164         fun tearDownOnce() {
165             turnScreenOn()
166         }
167     }
168 
169     class Icmp6PacketReader(
170             handler: Handler,
171             private val network: Network
172     ) : PacketReader(handler, RCV_BUFFER_SIZE) {
173         private var sockFd: FileDescriptor? = null
174         private var futureReply: CompletableFuture<ByteArray>? = null
175 
createFdnull176         override fun createFd(): FileDescriptor {
177             // sockFd is closed by calling super.stop()
178             val sock = Os.socket(AF_INET6, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_ICMPV6)
179             // APF runs only on WiFi, so make sure the socket is bound to the right network.
180             network.bindSocket(sock)
181             sockFd = sock
182             return sock
183         }
184 
handlePacketnull185         override fun handlePacket(recvbuf: ByteArray, length: Int) {
186             // If zero-length or Type is not echo reply: ignore.
187             if (length == 0 || recvbuf[0] != 0x81.toByte()) {
188                 return
189             }
190             // Only copy the ping data and complete the future.
191             val result = recvbuf.sliceArray(8..<length)
192             Log.i(TAG, "Received ping reply: ${result.toHexString()}")
193             futureReply!!.complete(recvbuf.sliceArray(8..<length))
194         }
195 
sendPingnull196         fun sendPing(data: ByteArray, payloadSize: Int) {
197             require(data.size == payloadSize)
198 
199             // rfc4443#section-4.1: Echo Request Message
200             //   0                   1                   2                   3
201             //   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
202             //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
203             //  |     Type      |     Code      |          Checksum             |
204             //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
205             //  |           Identifier          |        Sequence Number        |
206             //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
207             //  |     Data ...
208             //  +-+-+-+-+-
209             val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
210             val packet = icmp6Header + data
211             Log.i(TAG, "Sent ping: ${packet.toHexString()}")
212             futureReply = CompletableFuture<ByteArray>()
213             Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
214         }
215 
expectPingReplynull216         fun expectPingReply(): ByteArray {
217             return futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
218         }
219 
expectPingDroppednull220         fun expectPingDropped() {
221             assertFailsWith(TimeoutException::class) {
222                 futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
223             }
224         }
225 
startnull226         override fun start(): Boolean {
227             // Ignore the fact start() could return false or throw an exception.
228             handler.post({ super.start() })
229             handler.waitForIdle(TIMEOUT_MS)
230             return true
231         }
232 
stopnull233         override fun stop() {
234             handler.post({ super.stop() })
235             handler.waitForIdle(TIMEOUT_MS)
236         }
237     }
238 
239     @get:Rule val ignoreRule = DevSdkIgnoreRule()
240     @get:Rule val expect = Expect.create()
241 
<lambda>null242     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
<lambda>null243     private val pm by lazy { context.packageManager }
244     private lateinit var network: Network
245     private lateinit var ifname: String
246     private lateinit var networkCallback: TestableNetworkCallback
247     private lateinit var caps: ApfCapabilities
<lambda>null248     private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
249     private val handler = Handler(handlerThread.looper)
250     private lateinit var packetReader: Icmp6PacketReader
251 
getApfCapabilitiesnull252     fun getApfCapabilities(): ApfCapabilities {
253         val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
254         if (caps.isEmpty()) {
255             return ApfCapabilities(0, 0, 0)
256         }
257         val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
258         return ApfCapabilities(version, maxLen, packetFormat)
259     }
260 
261     @Before
setUpnull262     fun setUp() {
263         assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
264 
265         networkCallback = TestableNetworkCallback()
266         cm.requestNetwork(
267                 NetworkRequest.Builder()
268                         .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
269                         .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
270                         .build(),
271                 networkCallback
272         )
273         network = networkCallback.expect<Available>().network
274         networkCallback.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
275             ifname = assertNotNull(it.lp.interfaceName)
276             true
277         }
278         // It's possible the device does not support APF, in which case this command will not be
279         // successful. Ignore the error as testApfCapabilities() already asserts APF support on the
280         // respective VSR releases and all other tests are based on the capabilities indicated.
281         runShellCommand("cmd network_stack apf $ifname pause")
282         caps = getApfCapabilities()
283 
284         packetReader = Icmp6PacketReader(handler, network)
285         packetReader.start()
286     }
287 
288     @After
tearDownnull289     fun tearDown() {
290         if (::packetReader.isInitialized) {
291             packetReader.stop()
292         }
293         handlerThread.quitSafely()
294         handlerThread.join()
295 
296         if (::ifname.isInitialized) {
297             runShellCommand("cmd network_stack apf $ifname resume")
298         }
299         if (::networkCallback.isInitialized) {
300             cm.unregisterNetworkCallback(networkCallback)
301         }
302     }
303 
304     @VsrTest(
305         requirements = ["VSR-5.3.12-001", "VSR-5.3.12-003", "VSR-5.3.12-004", "VSR-5.3.12-009",
306             "VSR-5.3.12-012"]
307     )
308     @Test
testApfCapabilitiesnull309     fun testApfCapabilities() {
310         // APF became mandatory in Android 14 VSR.
311         assume().that(getVsrApiLevel()).isAtLeast(34)
312 
313         // ApfFilter does not support anything but ARPHRD_ETHER.
314         assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
315 
316         // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
317         // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
318         //   the getApfPacketFilterCapabilities HAL method.
319         // - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
320         //   the getApfPacketFilterCapabilities HAL method.
321         // TODO: check whether above text should be changed "34 or higher"
322         assertThat(caps.apfVersionSupported).isAtLeast(4)
323         assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
324 
325         if (caps.apfVersionSupported > 4) {
326             assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
327             assertThat(caps.apfVersionSupported).isEqualTo(6000) // v6.0000
328         }
329 
330         // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
331         // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
332         // - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
333         //   the getApfPacketFilterCapabilities HAL method.
334         if (getVsrApiLevel() >= 202404) {
335             assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
336         }
337     }
338 
339     // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
assumeApfVersionSupportAtLeastnull340     fun assumeApfVersionSupportAtLeast(version: Int) {
341         assume().that(caps.apfVersionSupported).isAtLeast(version)
342     }
343 
installProgramnull344     fun installProgram(bytes: ByteArray) {
345         val prog = bytes.toHexString()
346         val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
347         // runShellCommandOrThrow only throws on S+.
348         assertThat(result).isEqualTo("success")
349     }
350 
readProgramnull351     fun readProgram(): ByteArray {
352         val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
353         // runShellCommandOrThrow only throws on S+.
354         assertThat(progHexString).isNotEmpty()
355         return HexDump.hexStringToByteArray(progHexString)
356     }
357 
358     @VsrTest(
359             requirements = ["VSR-5.3.12-007", "VSR-5.3.12-008", "VSR-5.3.12-010", "VSR-5.3.12-011"]
360     )
361     @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
362     // APF integration is mostly broken before V, only run the full read / write test on V+.
363     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
364     // Increase timeout for test to 15 minutes to accommodate device with large APF RAM.
365     @Test(timeout = 15 * 60 * 1000)
testReadWriteProgramnull366     fun testReadWriteProgram() {
367         assumeApfVersionSupportAtLeast(4)
368 
369         val minReadWriteSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
370             2
371         } else {
372             8
373         }
374 
375         // The minReadWriteSize is 2 bytes. The first byte always stays PASS.
376         val program = ByteArray(caps.maximumApfProgramSize)
377         for (i in caps.maximumApfProgramSize downTo minReadWriteSize) {
378             // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
379             // Note that only the very first instruction (PASS) is valid APF bytecode.
380             Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
381             installProgram(program.sliceArray(0..<i))
382 
383             // Compare entire memory region.
384             val readResult = readProgram()
385             val errMsg = """
386                 read/write $i byte prog failed.
387                 In APFv4, the APF memory region MUST NOT be modified or cleared except by APF
388                 instructions executed by the interpreter or by Android OS calls to the HAL. If this
389                 requirement cannot be met, the firmware cannot declare that it supports APFv4 and
390                 it should declare that it only supports APFv3(if counter is partially supported) or
391                 APFv2.
392             """.trimIndent()
393             assertWithMessage(errMsg).that(readResult).isEqualTo(program)
394         }
395     }
396 
addPassIfNotIcmpv6EchoReplynull397     fun ApfV4Generator.addPassIfNotIcmpv6EchoReply() {
398         // If not IPv6 -> PASS
399         addLoad16(R0, ETH_ETHERTYPE_OFFSET)
400         addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
401 
402         // If not ICMPv6 -> PASS
403         addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
404         addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
405 
406         // If not echo reply -> PASS
407         addLoad8(R0, ICMP6_TYPE_OFFSET)
408         addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
409     }
410 
411     // APF integration is mostly broken before V
412     @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
413     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
414     @Test
testDropPingReplynull415     fun testDropPingReply() {
416         // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
417         // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
418         // should be turned on.
419         assume().that(getVsrApiLevel()).isAtLeast(34)
420         assumeApfVersionSupportAtLeast(4)
421 
422         // clear any active APF filter
423         clearApfMemory()
424         readProgram() // wait for install completion
425 
426         // Assert that initial ping does not get filtered.
427         val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
428             68
429         } else {
430             4
431         }
432         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
433         packetReader.sendPing(data, payloadSize)
434         assertThat(packetReader.expectPingReply()).isEqualTo(data)
435 
436         // Generate an APF program that drops the next ping
437         val gen = ApfV4Generator(4)
438 
439         // If not ICMPv6 Echo Reply -> PASS
440         gen.addPassIfNotIcmpv6EchoReply()
441 
442         // if not data matches -> PASS
443         gen.addLoadImmediate(R0, ICMP6_TYPE_OFFSET + PING_HEADER_LENGTH)
444         gen.addJumpIfBytesAtR0NotEqual(data, BaseApfGenerator.PASS_LABEL)
445 
446         // else DROP
447         gen.addJump(BaseApfGenerator.DROP_LABEL)
448 
449         val program = gen.generate()
450         installProgram(program)
451         readProgram() // wait for install completion
452 
453         packetReader.sendPing(data, payloadSize)
454         packetReader.expectPingDropped()
455     }
456 
clearApfMemorynull457     fun clearApfMemory() = installProgram(ByteArray(caps.maximumApfProgramSize))
458 
459     // APF integration is mostly broken before V
460     @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
461     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
462     @Test
463     fun testPrefilledMemorySlotsV4() {
464         // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
465         // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
466         // should be turned on.
467         assume().that(getVsrApiLevel()).isAtLeast(34)
468         // Test v4 memory slots on both v4 and v6 interpreters.
469         assumeApfVersionSupportAtLeast(4)
470         clearApfMemory()
471         val gen = ApfV4Generator(4)
472 
473         // If not ICMPv6 Echo Reply -> PASS
474         gen.addPassIfNotIcmpv6EchoReply()
475 
476         // Store all prefilled memory slots in counter region [500, 520)
477         val counterRegion = 500
478         gen.addLoadImmediate(R1, counterRegion)
479         gen.addLoadFromMemory(R0, MemorySlot.PROGRAM_SIZE)
480         gen.addStoreData(R0, 0)
481         gen.addLoadFromMemory(R0, MemorySlot.RAM_LEN)
482         gen.addStoreData(R0, 4)
483         gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE)
484         gen.addStoreData(R0, 8)
485         gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
486         gen.addStoreData(R0, 12)
487         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
488         gen.addStoreData(R0, 16)
489 
490         val program = gen.generate()
491         assertThat(program.size).isLessThan(counterRegion)
492         installProgram(program)
493         readProgram() // wait for install completion
494 
495         // Trigger the program by sending a ping and waiting on the reply.
496         val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
497             68
498         } else {
499             4
500         }
501         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
502         packetReader.sendPing(data, payloadSize)
503         packetReader.expectPingReply()
504 
505         val readResult = readProgram()
506         val buffer = ByteBuffer.wrap(readResult, counterRegion, 20 /* length */)
507         expect.withMessage("PROGRAM_SIZE").that(buffer.getInt()).isEqualTo(program.size)
508         expect.withMessage("RAM_LEN").that(buffer.getInt()).isEqualTo(caps.maximumApfProgramSize)
509         expect.withMessage("IPV4_HEADER_SIZE").that(buffer.getInt()).isEqualTo(0)
510         // Ping packet payload + ICMPv6 header (8)  + IPv6 header (40) + ethernet header (14)
511         expect.withMessage("PACKET_SIZE").that(buffer.getInt()).isEqualTo(payloadSize + 8 + 40 + 14)
512         expect.withMessage("FILTER_AGE_SECONDS").that(buffer.getInt()).isLessThan(5)
513     }
514 
515     // APF integration is mostly broken before V
516     @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
517     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
518     @Test
testFilterAgeIncreasesBetweenPacketsnull519     fun testFilterAgeIncreasesBetweenPackets() {
520         // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
521         // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
522         // should be turned on.
523         assume().that(getVsrApiLevel()).isAtLeast(34)
524         assumeApfVersionSupportAtLeast(4)
525         clearApfMemory()
526         val gen = ApfV4Generator(4)
527 
528         // If not ICMPv6 Echo Reply -> PASS
529         gen.addPassIfNotIcmpv6EchoReply()
530 
531         // Store all prefilled memory slots in counter region [500, 520)
532         val counterRegion = 500
533         gen.addLoadImmediate(R1, counterRegion)
534         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
535         gen.addStoreData(R0, 0)
536 
537         installProgram(gen.generate())
538         readProgram() // wait for install completion
539 
540         val payloadSize = 56
541         val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
542         packetReader.sendPing(data, payloadSize)
543         packetReader.expectPingReply()
544 
545         var buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
546         val filterAgeSecondsOrig = buffer.getInt()
547 
548         Thread.sleep(5100)
549 
550         packetReader.sendPing(data, payloadSize)
551         packetReader.expectPingReply()
552 
553         buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
554         val filterAgeSeconds = buffer.getInt()
555         // Assert that filter age has increased, but not too much.
556         val timeDiff = filterAgeSeconds - filterAgeSecondsOrig
557         assertThat(timeDiff).isAnyOf(5, 6)
558     }
559 }
560