Android Packet Filter

Android Packet Filter (APF) lets the framework control hardware packet filtering logic at runtime. This lets the system save power by dropping packets in hardware, while allowing the Android framework to change filtering rules at runtime based on network conditions.

APF overview

APF consists of two main components:

  • The APF interpreter runs on networking hardware (typically, the Wi-Fi chipset). The APF interpreter runs APF bytecode on packets received by the hardware and decides whether to accept, drop, or reply to them.
  • The APF program generation code runs on the main CPU. The code creates and updates APF programs according to network and device state.

Wi-Fi HAL methods allow the Android framework to install the APF program bytecode and to read the current counters. The Network Stack Mainline module can update the APF program bytecode at any time while APF is running.

There are several APF filters implemented. For example, APF includes filters to drop disallowed ethertypes, filter IPv6 router advertisement (RA) packets, filter multicast and broadcast traffic if the multicast lock isn't held, drop DHCP packets for other hosts, and drop unsolicited address resolution protocol (ARP) and neighbor discovery (ND) packets. If the firmware supports APFv6, ApfFilter also generates rules to reply to common packet types that would otherwise require the CPU to wake up in order to respond, such as ARP queries and NS queries. The full list of filters is defined in ApfFilter.

Because the APF program generation code is part of the Network Stack module, you can use monthly [Mainline updates to add new filters and update the filtering logic.

APF revision

The following list describes the revision history of APF:

  • APFv6: Introduced in Android 15, this version supports packet filtering, includes counters for debugging and metrics, and supports packet transmission.
  • APFv4: Introduced in Android 10, this version supports packet filtering and includes counters for debugging and metrics.
  • APFv2: Introduced in Android 7, this version supports packet filtering.

APF integration

The APF APIs between the APF interpreter and the hardware are defined in apf_interpreter.h (APFv4, APFv6). The Wi-Fi firmware code calls accept_packet() in APFv4 or apf_run() in APFv6 to determine if the packet should be dropped (zero return value) or passed to the app processor (nonzero return value). If a packet needs to be transmitted, apf_run() also returns zero because its packet doesn't need to be passed to the app processor. If the firmware supports APFv6, it must implement the apf_allocate_buffer() and apf_transmit_buffer() APIs. The APF interpreter calls these two APIs during packet transmission logic. APF instructions are variable length. Each instruction is at least 1 byte in length. The APF instruction codes are defined in apf.h for APFv4 and are inlined directly within apf_interpreter.c for APFv6.

APF relies on dedicated memory. The memory is used for both the APF program itself and for data storage, and the memory must not be cleared or written by the chipset except through the APF HAL methods. The APF bytecode uses the data storage to store counters for accepted and dropped packets. The data region can be read from the Android framework. APF instructions are memory efficient, but maximizing their power-saving and functionality potential demands complex, dynamic filtering rules. This complexity necessitates a dedicated portion of on-chipset memory. The minimum memory requirement for APFv4 is 1024 bytes, while APFv6 requires 2048 bytes. However, we strongly recommend allocating 4096 bytes for APFv6 to ensure optimal performance. The APF interpreter must be compiled into firmware. Both APFv4 and APFv6 interpreters are optimized for code size. Under arm32 architecture, the compiled APFv4 interpreter is around 1.8 KB, while the more complex APFv6 interpreter, with added features (for example, native checksum support and native DNS decompression code), is approximately 4 KB.

APF filters can work alongside other chipset vendor-specific filters within the firmware. Chipset vendors can choose to run their filtering logic either before or after the APF filtering process. If a packet is dropped before reaching the APF filter, the APF filter doesn't process the packet.

To ensure correct APF filter functionality, when APF is turned on, the firmware must provide the APF filter with access to the entire packet, not just the header, when APF is enabled.

APF program samples

ApfTest and ApfFilterTest contain sample test programs that illustrate how each APF filter works. To study the actual generated program, modify the test case to print the program as a hex string.

The testdata folder contains sample APFv4 programs for APF RA filters. The samples folder contains Python utilities that generate APFv6 offload programs. For more details, see the documentation in the Python utility files.

Debug APF

To check if APF is enabled on the device, display the current program, show the current counters, and run the adb shell dumpsys network_stack command. The following is an example of this command:

adb shell dumpsys network_stack
......
IpClient.wlan0 APF dump:
    Capabilities: ApfCapabilities{version: 4, maxSize: 4096, format: 1}
......
    Last program:
      6bfcb03a01b8120c6b9494026506006b907c025e88a27c025988a47c025488b87c024f88cd7c024a88e17c024588e384004408066a0e6bdca4022b000600010800060412147a1e016bd884021f00021a1c6b8c7c021c0000686bd4a402080006ffffffffffff6a266bbca402010004c0a801eb6bf87401f6120c84005f08000a17821f1112149c00181fffab0d2a108211446a3239a20506c2fc393057dd6bf47401cb0a1e52f06bac7c01c600e06bb41a1e7e000001b9ffffffff6bb07e000001aec0a801ff6be868a4019a0006ffffffffffff6bb874019b6bf07401907c001386dd686bd0a4017d0006ffffffffffff6bc874017e0a147a0e3a6b980a267c017000ff6be07401650a366ba87c016200858219886a26a2050fff02000000000000000000000000006ba4740146aa0e84013700e6aa0f8c0130006068a4011b000f33330000000184c9b26aed4c86dd606a12a2f02600b03afffe8000000000000086c9b2fffe6aed4cff02000000000000000000000000000186006a3aa2e9024000123c92e4606a3ea2d70800000000000000006a56a2ce04030440c01a5a92c9601a5e92c4606a62a2bb04000000006a66a2a6102401fa00049c048400000000000000006a76a29d04030440c01a7a9298601a7e9293606c0082a28904000000006c0086a27310fdfd9ed67950000400000000000000006c0096a2690418033c001a9a9264606c009ea24e102401fa00049c048000000000000000006c00aea24404180330001ab2923f606c00b6a22910fdfd9ed67950000000000000000000006c00c6a21f04190300001aca921a606c00cea20410fdfd9ed67950000400000000000000016bc472086be4b03a01b87206b03a01b87201
    APF packet counters:
      TOTAL_PACKETS: 469
      PASSED_DHCP: 4
      PASSED_IPV4: 65
      PASSED_IPV6_NON_ICMP: 64
      PASSED_IPV4_UNICAST: 64
      PASSED_IPV6_ICMP: 223
      PASSED_IPV6_UNICAST_NON_ICMP: 6
      PASSED_ARP_UNICAST_REPLY: 4
      PASSED_NON_IP_UNICAST: 1
      DROPPED_RA: 4
      DROPPED_IPV4_BROADCAST_ADDR: 7
      DROPPED_IPV4_BROADCAST_NET: 27

The output for this example adb shell dumpsys network_stack command includes the following:

  • ApfCapabilities{version: 4, maxSize: 4096, format: 1}: This means the Wi-Fi chips support APF (version 4).
  • Last program: This section is the latest installed APF program binary in hex string format.
  • APF packet counters: This section shows how many packets are passed or dropped by APF and the specific reasons.

To decode and disassemble the code into human-readable assembler language, use the apf_disassembler tool. To compile the executable binary, run the m apf_disassembler command. The following is an example of how to use the apf_disassembler tool:

echo "6bfcb03a01b8120c6b949401e906006b907c01e288a27c01dd88a47c01d888b87c01d388cd7c01ce88e17c01c988e384004008066a0e6bdca401af000600010800060412147a1e016bd88401a300021a1c6b8c7c01a00000686bd4a4018c0006ffffffffffff1a266bc07c018900006bf874017e120c84005408000a17821f1112149c00181fffab0d2a108211446a3239a205065a56483ac3146bf47401530a1e52f06bac7c014e00e06bb41a1e7e00000141ffffffff6be868a4012d0006ffffffffffff6bb874012e6bf07401237c001386dd686bd0a401100006ffffffffffff6bc87401110a147a0d3a6b980a267c010300ff6be072f90a366ba87af8858218886a26a2040fff02000000000000000000000000006ba472ddaa0e82d0aeaa0f8c00c9025868a2b60f5a56483ac3140c8126f3895186dd606a12a28b2600783afffe8000000000000002005efffe00026fff02000000000000000000000000000186006a3aa284024000123c94007d02586a3ea2700800000000000000006a56a26704190500001a5a94006002586a5ea23b2020014860486000000000000000006464200148604860000000000000000000646a7ea23204030440c01a8294002b02581a8694002402586c008aa21a04000000006c008ea204102a0079e10abcf60500000000000000006bc472086be4b03a01b87206b03a01b87201" | out/host/linux-x86/bin/apf_disassembler
       0: li    r1, -4
       2: lddw  r0, [r1+0]
       3: add   r0, 1
       5: stdw  r0, [r1+0]
       6: ldh   r0, [12]
       8: li    r1, -108
      10: jlt   r0, 0x600, 504
      15: li    r1, -112
      17: jeq   r0, 0x88a2, 504
      22: jeq   r0, 0x88a4, 504
      27: jeq   r0, 0x88b8, 504
      32: jeq   r0, 0x88cd, 504
      37: jeq   r0, 0x88e1, 504
      42: jeq   r0, 0x88e3, 504
      47: jne   r0, 0x806, 116
......

To check APF results offline, use the apf_run tool. To compile the executable binary, run the m apf_run command. The apf_run tool supports both APFv4 and APFv6 interpreters.

The following is the manual for the apf_run command. By default, the apf_run command runs in the APFv4 interpreter. Passing the --v6 argument to apf_run lets it run against the APFv6 interpreter. All other arguments can be used for both APFv4 and APFv6.

apf_run --help
Usage: apf_run --program <program> --pcap <file>|--packet <packet> [--data <content>] [--age <number>] [--trace]
  --program    APF program, in hex.
  --pcap       Pcap file to run through program.
  --packet     Packet to run through program.
  --data       Data memory contents, in hex.
  --age        Age of program in seconds (default: 0).
  --trace      Enable APF interpreter debug tracing
  --v6         Use APF v6
  -c, --cnt    Print the APF counters
  -h, --help   Show this message.

Here is an example to pass one packet to APF to check if the packet can be dropped or passed.

To provide the hex binary string presentation of the raw packet, use the --packet option. To provide the hex binary string of the data region, which is used to store the APF counter, use the --data option. Because each counter is 4 bytes long, the data regions must be long enough to make sure no buffer overflow occurs.

out/host/linux-x86/bin/apf_run --program 6bfcb03a01b8120c6b9494010c06006b907c010588a27c010088a47c00fb88b87c00f688cd7c00f188e17c00ec88e384003908066a0e6bdca2d40600010800060412147a18016bd882ca021a1c6b8c7ac900686bd4a2b706ffffffffffff6a266bbca2b204c0a814656bf872a8120c84005808000a17821e1112149c00171fffab0d2a108210446a3239a204064651dbcc88ff6bf4727e0a1e52f06bac7a7be06bb41a1e7e0000006effffffff6bb07e00000063c0a814ff6be868a25106ffffffffffff6bb872536bf072497c001086dd686bd0a23806ffffffffffff6bc8723a0a147a0b3a6b980a267a2eff6be072240a366ba87a23858218886a26a2040fff02000000000000000000000000006ba472086be4b03a01b87206b03a01b87201 --packet 5ebcd79a8f0dc244efaab81408060001080006040002c244efaab814c0a8ca1e5ebcd79a8f0d --data 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Packet passed
Data: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000001

To check APF results against the pcap file taken by tcpdump, use the apf_run command as follows:

out/host/linux-x86/bin/apf_run --program 6bfcb03a01b8120c6b989401df06006b947c01d888a27c01d388a47c01ce88b87c01c988cd7c01c488e17c01bf88e384004408066a0e6bdca401a5000600010800060412147a1e016bd884019900021a1c6b907c01960000686bd4a401820006ffffffffffff6a266bc0a4017b0004c0a82b056bf874017084005f08000a17821f1112149c00181fffab0d2a108211446a3239a20506fabe589435936bf47401470a1e52f06bb07c014200e06bb81a1e7e00000135ffffffff6bb47e0000012ac0a82bff6be868a401160006ffffffffffff6bbc7401176bf074010c7c001086dd686bd0a2fb06ffffffffffff6bcc72fd0a147a0b3a6b9c0a267af1ff6be072e70a366bac7ae6858218886a26a2040fff02000000000000000000000000006ba872cbaa0e82be8eaa0f8c00b7025868a2a40ffabe5894359352a9874d08aa86dd606a12a2792600583afffe80000000000000f7d4e8ccd81ddb43fe80000000000000f8be58fffe94359386006a3aa272024108123c94006b02586a3ea25e0800000000000000006a56a25504030440c01a5a94004e02581a5e94004702586a62a23e04000000006a66a229102409891f9a26ae6d00000000000000006a76a22004190300001a7a94001902586a7ea204102409891f9a26ae6dba98e781ca9ef9ba6bc872086be4b03a01b87206b03a01b87201 --pcap apf.pcap --data 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
37 packets dropped
1733 packets passed
Data: 00000000000000000000000000000000000000000200000005000000000000000000000002000000000000001b000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000689000000000000003c00000000000000000000000000000000000006ea

To test APFv6 transmit capabilities, use the apf_run command as follows:

$ apf_run --program 75001001020304050608060001080006040002AA300E3CAA0FBA06AA09BA07AA08BA086A01BA09120C84006F08066A0EA30206000108000604032B12147A27017A020203301A1C820200032D68A30206FFFFFFFFFFFF020E1A267E000000020A000001032C020B1A267E000000020A000001032CAB24003CCA0606CB0306CB090ACB0306C60A000001CA0606CA1C04AA
0A3A12AA1AAA25FFFF032F020D120C84001708000A1782100612149C00091FFFAB0D2A10820207032A02117C000E86DD68A30206FFFFFFFFFFFF021603190A1482020002187A023A02120A36820285031F8216886A26A2020FFF020000000000000000000000000003200214 --packet FFFFFFFFFFFF112233445566080600010800060400011122334455660A0000020000000000000A0000
01 --data 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 --age 0 --v6 --trace
      R0       R1       PC  Instruction
-------------------------------------------------
       0        0        0: data        16, 01020304050608060001080006040002
       0        0       19: debugbuf    size=3644
       0        0       23: ldm         r0, m[15]
       0        0       25: stdw        counter=6, r0
       0        0       27: ldm         r0, m[9]
       0        0       29: stdw        counter=7, r0
       0        0       31: ldm         r0, m[8]
 134d811        0       33: stdw        counter=8, r0
 134d811        0       35: li          r0, 1
       1        0       37: stdw        counter=9, r0
       1        0       39: ldh         r0, [12]
     806        0       41: jne         r0, 0x806, 157
     806        0       46: li          r0, 14
       e        0       48: jbseq       r0, 0x6, 59, 000108000604
       e        0       59: ldh         r0, [20]
       1        0       61: jeq         r0, 0x1, 103
       1        0      103: ldw         r0, [38]
 a000001        0      105: jeq         r0, 0xa000001, 116
 a000001        0      116: allocate    60
 a000001        0      120: pktcopy     src=6, len=6
 a000001        0      123: datacopy    src=3, len=6
 a000001        0      126: datacopy    src=9, len=10
 a000001        0      129: datacopy    src=3, len=6
 a000001        0      132: write       0x0a000001
 a000001        0      137: pktcopy     src=6, len=6
 a000001        0      140: pktcopy     src=28, len=4
 a000001        0      143: ldm         r0, m[10]
      2a        0      145: add         r0, 18
      3c        0      147: stm         r0, m[10]
      3c        0      149: transmit    ip_ofs=255
      3c        0      153: drop        counter=47
Packet dropped
Data: 00000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000100000011d8340100000000000000000000000000000000000000000100000078563412
transmitted packet: 112233445566010203040506080600010800060400020102030405060a0000011122334455660a000002000000000000000000000000000000000000

When you use the --trace parameter, the apf_run tool provides a detailed output of each step in the interpreter's execution, which is helpful for debugging. In this example, we input an ARP query packet into the APF program. The output shows that the ARP query is dropped, but a reply packet is generated. The details of this generated packet are shown in the transmitted packet section.

Common integration issues

This section highlights several common problems encountered during APF integration:

  • Unexpected data region clearing: The APF memory must be entirely dedicated to APF; only the interpreter code or framework code (through HAL API) are allowed to modify the APF memory region.
  • Installation issues with APF programs of X bytes (X <= maxLen): Firmware must support reading or writing any program length up to maxLen without failure, crashes, or truncation. Writes must not alter any bytes between X and maxLen.
  • APF implementation in driver code: APF should be implemented only within firmware, not driver code. Otherwise, there are no power saving benefits because the CPU needs to wake up to process the packet.
  • Incorrect filter_age or filter_age_16384th values: The filter_age (APFv4) and filter_age_16384th (APFv6) values must be correctly passed to the accept_packet() and apf_run() functions. For details on calculating filter_age_16384th, refer to the documentation in apf_interpreter.h.
  • APF not enabled when required: APF must be enabled when the screen is off and either the Wi-Fi link is idle or traffic is below 10 Mbps.
  • Truncated packets passed to accept_packet() or apf_run(): All unicast, broadcast, and multicast packets passed to accept_packet() or apf_run() must be complete. Passing truncated packets into APF isn't valid.

APF tests

Starting in Android 15, Android provides both single-device and multi-device CTS test cases for APF filter and APF interpreter integration to ensure correct APF functionality. Here's a breakdown of the purpose of each test case:

  • ApfFilter and apf_interpreter integration test: Verifies that ApfFilter generates correct bytecode, and apf_interpreter executes the code correctly to produce expected results.
  • APF single-device CTS: Uses a single device to test APF functionality on the Wi-Fi chipset. Confirms that:
    • APF turns on when the screen is on and Wi-Fi traffic is below 10 Mbps.
    • APF capabilities are declared correctly.
    • Read and write operations on the APF memory region succeed, and the memory region isn't unexpectedly modified.
    • Arguments are passed correctly to accept_packet() or apf_run().
    • Firmware integrated with APFv4/APFv6 can drop packets.
    • Firmware integrated with APFv6 can reply to packets.
  • APF multi-device CTS: Employs two devices (one sender, one receiver) to test the filtering behavior of APF. Various types of packets are generated on the sender side, and the test confirms whether they are correctly dropped, passed, or replied to based on the rules configured in ApfFilter.

Additional integration test instructions

Additionally, we strongly recommend that chipset vendors incorporate APF testing into their own firmware Wi-Fi integration test suites.

Integrating APF testing into firmware Wi-Fi integration test suites is crucial for verifying proper APF functionality in complex Wi-Fi connection scenarios such as make-before-break or roaming Wi-Fi connection scenarios. Detailed instructions on how to perform integration tests can be found in the following section.

Prerequisites

When performing integration tests, do the following:

  • APF must be enabled throughout all integration test cases (for example, roaming, make-before-break).
  • At the start of each test, clear the APF memory.
  • Install or reinstall APF programs every 5 minutes during the test.

Test scenarios

APF must be active throughout integration tests. There are two APF programs provided in this document that can be installed during testing. The programs are in hex string format, and the tester must convert the hex string to binary and install them to the firmware so the programs can be executed by apf_interpreter. During the integration test, the tester should send packets that are expected to trigger the filtering logic in program 1 and program 2.

APF program 1

When the device screen is on, install APF program 1. This program can drop harmless packets that don't impact device functionality. These packets are used to test whether the APF is correctly filtering network traffic.

The APF program 1 logic is as follows:

  1. Drop and increment counter:
    1. EtherType values: 0x88A2, 0x88A4, 0x88B8, 0x88CD, 0x88E1, 0x88E3
    2. IPv4 DHCP discover or request packets
    3. RS packets
  2. Pass and increment counter: All other packets.

APF program 1 byte codes are as follows:

6BF0B03A01B86BF8AA0FB86BF4AA09B8120C6BEC7C005D88A27C005888A47C005388B87C004E88CD7C004988E17C004488E3120C84002008001A1A821B001A1E8600000010FFFFFFFF0A17820B11AB0D2A108204436BE8721D120C84000E86DD0A1482093A0A368204856BE072086BDCB03A01B87206B03A01B87201
APF program 2

When the device screen is off, install APF program 2. This program filters out all the packets that APF program 1 filters, as well as ping request packets. To verify that APF program 2 is correctly installed, send ping packets to the device under test.

The APF program 2 logic is as follows:

  1. Drop and increment counter:
    1. EtherType values: 0x88A2, 0x88A4, 0x88B8, 0x88CD, 0x88E1, 0x88E3
    2. IPv4 DHCP discover or request packets
    3. RS packets
  2. Drop and increment counter: ICMP ping request packets
  3. Pass and increment counter: All other packets

APF program 2 bytes codes are as follows:

6BF0B03A01B86BF8AA0FB86BF4AA09B8120C6BEC7C007488A27C006F88A47C006A88B87C006588CD7C006088E17C005B88E3120C84002008001A1A821B001A1E8600000010FFFFFFFF0A17820B11AB0D2A108204436BE87234120C84000E86DD0A1482093A0A368204856BE0721F120C84001008000A17820B01AB0D220E8204086BE472086BDCB03A01B87206B03A01B87201
Data verification

To verify that the APF program is executed and packets are passed or dropped correctly, do the following:

  • Fetch and verify the APF data region every 5 minutes.
  • Don't clear the counter.
  • Generate test packets to trigger each filter rule.
  • Verify counter increments using the following memory locations:

    Counter name Memory location
    DROPPED_ETHERTYPE_DENYLISTED [ApfRamSize - 20, ApfRamSize - 16]
    DROPPED_DHCP_REQUEST_DISCOVERY [ApfRamSize - 24, ApfRamSize - 20]
    DROPPED_ICMP4_ECHO_REQUEST [ApfRamSize - 28, ApfRamSize - 24]
    DROPPED_RS [ApfRamSize - 32, ApfRamSize - 28]
    PASSED_PACKET [ApfRamSize - 36, ApfRamSize - 32]

Pseudocode for APF program 1 and APF program 2

The following pseudocode explains the logic of APF program 1 and APF program 2 in detail:

// ethertype filter
If the ethertype in [0x88A2, 0x88A4, 0x88B8, 0x88CD, 0x88E1, 0x88E3]:
    drop packet and increase counter: DROPPED_ETHERTYPE_DENYLISTED

// dhcp discover/request filter
if ethertype != ETH_P_IP:
    skip the filter
if ipv4_src_addr != 0.0.0.0:
    skip the filter
if ipv4_dst_addr != 255.255.255.255
    skip the filter
if not UDP packet:
    skip the filter
if UDP src port is not dhcp request port:
    skip the filter
else:
    drop the packet and increase the counter: DROPPED_DHCP_REQUEST_DISCOVERY

// Router Solicitation filter:
if ethertype != ETH_P_IPV6:
    skip the filter
if not ICMP6 packet:
    skip the filter
if ICMP6 type is not a Router Solicitation:
    skip the filter
else:
    drop the packet and increase the counter: DROPPED_RS

// IPv4 ping filter (only included in Program 2)
if ethertype != ETH_P_IP:
    skip the filter
if it ipv4 protocol is not ICMP:
    skip the filter
if port is not a ping request port
    skip the filter
else:
    drop the packet and increase the counter: DROPPED_ICMP4_ECHO_REQUEST

pass the packet and increase: PASSED_PACKET