Update APF multicast address when receiving IGMP report monitor callback

Test: atest NetworkStackTests
Change-Id: I56ef1a3275b8bed54439f1be50d48eb470351cec
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index d844dc1..456f43c 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -101,6 +101,7 @@
 import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
 import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
 import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
@@ -143,6 +144,7 @@
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.BaseApfGenerator.IllegalInstructionException;
+import android.net.ip.IgmpReportMonitor;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
 import android.net.nsd.NsdManager;
 import android.net.nsd.OffloadEngine;
@@ -217,6 +219,7 @@
         public boolean shouldHandleArpOffload;
         public boolean shouldHandleNdOffload;
         public boolean shouldHandleMdnsOffload;
+        public boolean shouldHandleIgmpOffload;
     }
 
 
@@ -284,11 +287,14 @@
     private final boolean mShouldHandleArpOffload;
     private final boolean mShouldHandleNdOffload;
     private final boolean mShouldHandleMdnsOffload;
+    private final boolean mShouldHandleIgmpOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
     private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
     private final NsdManager mNsdManager;
+    private final IgmpReportMonitor mIgmpReportMonitor;
+
     @VisibleForTesting
     final List<OffloadServiceInfo> mOffloadServiceInfos = new ArrayList<>();
     private OffloadEngine mOffloadEngine;
@@ -442,6 +448,7 @@
         mShouldHandleArpOffload = config.shouldHandleArpOffload;
         mShouldHandleNdOffload = config.shouldHandleNdOffload;
         mShouldHandleMdnsOffload = config.shouldHandleMdnsOffload;
+        mShouldHandleIgmpOffload = config.shouldHandleIgmpOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
@@ -476,6 +483,17 @@
             Log.wtf(TAG, "Failed to start RaPacketReader");
         }
 
+        mIgmpReportMonitor = new IgmpReportMonitor(
+            mHandler,
+            mInterfaceParams,
+            this::updateIPv4MulticastAddrs,
+            mDependencies.createEgressIgmpReportsReaderSocket(ifParams.index)
+        );
+
+        if (shouldEnableIgmpOffload()) {
+            mIgmpReportMonitor.start();
+        }
+
         // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
         mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
 
@@ -483,6 +501,9 @@
         if (shouldEnableMdnsOffload()) {
             registerOffloadEngine();
         }
+
+        mIPv4MulticastAddresses =
+                new ArraySet<>(mDependencies.getIPv4MulticastAddresses(mInterfaceParams.name));
     }
 
     /**
@@ -514,6 +535,24 @@
         }
 
         /**
+         * Create a socket to read egress IGMPv2/v3 reports.
+         */
+        @Nullable
+        public FileDescriptor createEgressIgmpReportsReaderSocket(int ifIndex) {
+            FileDescriptor socket;
+            try {
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+                NetworkStackUtils.attachEgressIgmpReportFilter(socket);
+                Os.bind(socket, makePacketSocketAddress(ETH_P_ALL, ifIndex));
+            } catch (SocketException | ErrnoException e) {
+                Log.wtf(TAG, "Error starting filter", e);
+                return null;
+            }
+
+            return socket;
+        }
+
+        /**
          * Get elapsedRealtime.
          */
         public long elapsedRealtime() {
@@ -2604,6 +2643,10 @@
         if (shouldEnableMdnsOffload()) {
             unregisterOffloadEngine();
         }
+
+        if (shouldEnableIgmpOffload()) {
+            mIgmpReportMonitor.stop();
+        }
     }
 
     public void setMulticastFilter(boolean isEnabled) {
@@ -2710,6 +2753,10 @@
         return shouldUseApfV6Generator() && mShouldHandleMdnsOffload;
     }
 
+    private boolean shouldEnableIgmpOffload() {
+        return shouldUseApfV6Generator() && mShouldHandleIgmpOffload;
+    }
+
     private boolean shouldUseApfV6Generator() {
         return SdkLevel.isAtLeastV() && ApfV6Generator.supportsVersion(mApfVersionSupported);
     }
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index a99a198..d23d30c 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -808,6 +808,7 @@
     private final boolean mApfShouldHandleArpOffload;
     private final boolean mApfShouldHandleNdOffload;
     private final boolean mApfShouldHandleMdnsOffload;
+    private final boolean mApfShouldHandleIgmpOffload;
     private final boolean mIgnoreNudFailureEnabled;
     private final boolean mDhcp6PdPreferredFlagEnabled;
 
@@ -1071,6 +1072,8 @@
                 mContext, APF_HANDLE_ND_OFFLOAD);
         // TODO: turn on APF mDNS offload.
         mApfShouldHandleMdnsOffload = false;
+        // TODO: turn on APF IGMP offload.
+        mApfShouldHandleIgmpOffload = false;
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
         mIgnoreNudFailureEnabled = mDependencies.isFeatureEnabled(mContext,
@@ -2764,6 +2767,7 @@
         apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
         apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
         apfConfig.shouldHandleMdnsOffload = mApfShouldHandleMdnsOffload;
+        apfConfig.shouldHandleIgmpOffload = mApfShouldHandleIgmpOffload;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
         return mDependencies.maybeCreateApfFilter(getHandler(), mContext, apfConfig,
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
index 01c1260..3fa8e8d 100644
--- a/tests/unit/src/android/net/apf/ApfFilterTest.kt
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -133,6 +133,7 @@
 class ApfFilterTest {
     companion object {
         private const val THREAD_QUIT_MAX_RETRY_COUNT = 3
+        private const val NO_CALLBACK_TIMEOUT_MS: Long = 500
         private const val TAG = "ApfFilterTest"
     }
 
@@ -206,6 +207,7 @@
     }
     private val handler by lazy { Handler(handlerThread.looper) }
     private var writerSocket = FileDescriptor()
+    private var igmpWriteSocket = FileDescriptor()
 
     @Before
     fun setUp() {
@@ -227,6 +229,9 @@
         val readSocket = FileDescriptor()
         Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writerSocket, readSocket)
         doReturn(readSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        val igmpReadSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, igmpWriteSocket, igmpReadSocket)
+        doReturn(igmpReadSocket).`when`(dependencies).createEgressIgmpReportsReaderSocket(anyInt())
         doReturn(nsdManager).`when`(context).getSystemService(NsdManager::class.java)
     }
 
@@ -253,6 +258,7 @@
     @After
     fun tearDown() {
         IoUtils.closeQuietly(writerSocket)
+        IoUtils.closeQuietly(igmpWriteSocket)
         shutdownApfFilters()
         handler.waitForIdle(TIMEOUT_MS)
         Mockito.framework().clearInlineMocks()
@@ -2124,6 +2130,35 @@
         verify(ipClientCallback, never()).installPacketFilter(any())
     }
 
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testApfProgramUpdateWithMulticastAddressChange() {
+        val mcastAddrs = mutableListOf(
+            InetAddress.getByName("224.0.0.1") as Inet4Address
+        )
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val apfConfig = getDefaultConfig()
+        apfConfig.shouldHandleIgmpOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val addr = InetAddress.getByName("239.0.0.1") as Inet4Address
+        mcastAddrs.add(addr)
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val testPacket = HexDump.hexStringToByteArray("000000")
+        Os.write(igmpWriteSocket, testPacket, 0, testPacket.size)
+        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+
+        Os.write(igmpWriteSocket, testPacket, 0, testPacket.size)
+        Thread.sleep(NO_CALLBACK_TIMEOUT_MS)
+        verify(ipClientCallback, never()).installPacketFilter(any())
+
+        mcastAddrs.remove(addr)
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        Os.write(igmpWriteSocket, testPacket, 0, testPacket.size)
+        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+    }
+
     @Test
     fun testApfFilterInitializationCleanUpTheApfMemoryRegion() {
         val apfFilter = getApfFilter()