Implement IGMPv2 general query reply generation in APF

This change adds logic to APF to generate IGMPv2 report packets in
response to IGMPv2 general queries.

To optimize bytecode efficiency, the implementation reuses previously
constructed IGMPv3 packets from the data region whenever possible.
Notably, the code can generate multiple IGMPv2 reports for a single
IGMPv2 general query if the device has joined multiple IPv4 multicast
addresses.

Bug: 379840541
Test: TH
Change-Id: I0c0029b464698f848f0ee8f073ee31fdee543fd5
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index acc5064..e7a48ab 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -62,6 +62,7 @@
 import static android.net.apf.ApfConstants.ICMP6_ROUTE_INFO_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET;
+import static android.net.apf.ApfConstants.IGMPV2_REPORT_FROM_IPV4_OPTION_TO_IGMP_CHECKSUM;
 import static android.net.apf.ApfConstants.IGMPV3_MODE_IS_EXCLUDE;
 import static android.net.apf.ApfConstants.IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET;
 import static android.net.apf.ApfConstants.IGMP_MAX_RESP_TIME_OFFSET;
@@ -1919,7 +1920,7 @@
         //     if the max_res_code == 0, then it is IGMPv1:
         //       pass
         //     else it is IGMPv2:
-        //       pass
+        //       transmit IGMPv2 reports (one report per group) and drop
         //
         // if filtering multicast (i.e. multicast lock not held):
         //   if it's DHCP destined to our MAC:
@@ -2497,6 +2498,47 @@
     }
 
     /**
+     * Generate transmit code to send IGMPv2 report in response to general query packets.
+     */
+    private void generateIgmpV2ReportTransmit(ApfV6GeneratorBase<?> gen,
+            byte[] igmpPktFromEthSrcToIpTos, byte[] igmpPktFromIpIdToSrc)
+            throws IllegalInstructionException {
+        final int ipv4TotalLen =
+                IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN + IPV4_IGMP_MIN_SIZE;
+
+        // Reuse IGMPv3 packet chunks when creating the IGMPv2 report listed below:
+        //   - from Ethernet source to IPv4 Tos: 10 bytes
+        //   - from IPv4 identification to source address: 12 bytes
+        //   - multicast group addresses: 4 bytes * number of addresses
+        for (Inet4Address mcastAddr: mIPv4McastAddrsExcludeAllHost) {
+            final MacAddress mcastEther =
+                    NetworkStackUtils.ipv4MulticastToEthernetMulticast(mcastAddr);
+            gen.addAllocate(ETHER_HEADER_LEN + ipv4TotalLen)
+                    .addDataCopy(mcastEther.toByteArray())
+                    .addDataCopy(igmpPktFromEthSrcToIpTos)
+                    .addWriteU16(ipv4TotalLen)
+                    .addDataCopy(igmpPktFromIpIdToSrc)
+                    .addDataCopy(mcastAddr.getAddress())
+                    .addDataCopy(IGMPV2_REPORT_FROM_IPV4_OPTION_TO_IGMP_CHECKSUM)
+                    .addDataCopy(mcastAddr.getAddress())
+                    .addTransmitL4(
+                            // ip_ofs
+                            ETHER_HEADER_LEN,
+                            // csum_ofs
+                            IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET,
+                            // csum_start
+                            ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN,
+                            // partial_sum
+                            0,
+                            // udp
+                            false
+                    );
+        }
+
+        gen.addCountAndDrop(Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED);
+    }
+
+    /**
      * Generates filter code to handle IGMP packets.
      * <p>
      * On entry, this filter know it is processing an IPv4 packet. It will then process all IGMP
@@ -2583,8 +2625,8 @@
         v6Gen.addLoad8Indexed(R0, IGMP_MAX_RESP_TIME_OFFSET)
                 .addCountAndPassIfR0Equals(0, PASSED_IPV4); // IGMPv1
 
-        // TODO: add IGMPv2 general query offload
-        v6Gen.addCountAndPass(PASSED_IPV4); // IGMPv2
+        // Drop and transmit IGMPv2 reports
+        generateIgmpV2ReportTransmit(v6Gen, igmpPktFromEthSrcToIpTos, igmpPktFromIpIdToSrc);
 
         v6Gen.defineLabel(skipIgmpFilter);
     }
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
index 3aee731..7dd6c82 100644
--- a/tests/unit/src/android/net/apf/ApfFilterTest.kt
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -31,6 +31,7 @@
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_GARP_REPLY
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_INVALID
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_REPORT
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_ADDR
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_NET
@@ -745,7 +746,7 @@
 
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
-    fun testIgmpV2GeneralQueryPassed() {
+    fun testIgmpV2GeneralQueryReplied() {
         val apfFilter = getIgmpApfFilter()
         val program = consumeInstalledProgram(apfController, installCnt = 3)
         // Using scapy to generate IGMPv2 general query packet without router alert option:
@@ -761,13 +762,120 @@
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(pkt),
-            PASSED_IPV4
+            DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
         )
+
+        val igmpv2ReportPkts = setOf(
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:01
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb15
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.1
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafd
+            //         gaddr     = 239.0.0.1
+            """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001600fafd
+            ef000001
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:02
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb14
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.2
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafc
+            //         gaddr     = 239.0.0.2
+            """
+            01005e000002020304050607080046c00020000040000102eb140a000001ef000002940400001600fafc
+            ef000002
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:03
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb13
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.3
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafb
+            //         gaddr     = 239.0.0.3
+            """
+            01005e000003020304050607080046c00020000040000102eb130a000001ef000003940400001600fafb
+            ef000003
+            """.replace("\\s+".toRegex(), "").trim().uppercase()
+        )
+
+        val transmitPackets = ApfJniUtils.getAllTransmittedPackets()
+            .map { HexDump.toHexString(it).uppercase() }.toSet()
+        assertEquals(igmpv2ReportPkts, transmitPackets)
     }
 
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
-    fun testIgmpV2GeneralQueryWithRouterAlertOptionPassed() {
+    fun testIgmpV2GeneralQueryWithRouterAlertOptionReplied() {
         val apfFilter = getIgmpApfFilter()
         val program = consumeInstalledProgram(apfController, installCnt = 3)
         // Using scapy to generate IGMPv2 general query packet with router alert option:
@@ -784,8 +892,116 @@
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(pkt),
-            PASSED_IPV4
+            DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
         )
+
+        val igmpv2ReportPkts = setOf(
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:01
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb15
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.1
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafd
+            //         gaddr     = 239.0.0.1
+            """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001600fafd
+            ef000001
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:02
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb14
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.2
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafc
+            //         gaddr     = 239.0.0.2
+            """
+            01005e000002020304050607080046c00020000040000102eb140a000001ef000002940400001600fafc
+            ef000002
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:03
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb13
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.3
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafb
+            //         gaddr     = 239.0.0.3
+            """
+            01005e000003020304050607080046c00020000040000102eb130a000001ef000003940400001600fafb
+            ef000003
+            """.replace("\\s+".toRegex(), "").trim().uppercase()
+        )
+
+        val transmitPackets = ApfJniUtils.getAllTransmittedPackets()
+            .map { HexDump.toHexString(it).uppercase() }.toSet()
+        assertEquals(igmpv2ReportPkts, transmitPackets)
     }
 
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)