Register IpClient as OffloadEngine when APF supports mDNS offload

Updated IpClient to register itself as an OffloadEngine instance when
the APF indicates support for mDNS offload functionality.

The offloadEngine is registered when IpClient starts running and
unregistered when it stops. However, the offloadEngine is also
registered if the IpClient's ApfCapabilities are updated. This can
happen when a WiFi STA starts in secondary mode and then switches
to primary mode. If WiFi STA starts in secondary mode, then
ApfCapabilities is null so there is no ApfFilter created when IpClient
entered the RunningState.

Test: TH
Change-Id: I4a0c73a1ebdcd306f981236c0298b48414a563c4
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
index 6dd4fad..a688ba3 100644
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -15,6 +15,8 @@
  */
 package android.net.apf;
 
+import android.annotation.ChecksSdkIntAtLeast;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
 import android.net.NattKeepalivePacketDataParcelable;
@@ -22,6 +24,8 @@
 
 import com.android.internal.util.IndentingPrintWriter;
 
+import java.util.List;
+
 /**
  * The interface for AndroidPacketFilter
  */
@@ -113,4 +117,18 @@
     default boolean supportNdOffload() {
         return false;
     }
+
+    /**
+     * Return if the ApfFilter should use mDNS offload.
+     */
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */, codename =
+            "VanillaIceCream")
+    default boolean shouldUseMdnsOffload() {
+        return false;
+    }
+
+    /**
+     * Update the mDNS offload rules.
+     */
+    default void updateMdnsOffloadReplyRules(@NonNull List<MdnsOffloadRule> rules) { }
 }
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index c3dc9ac..7790ec4 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -215,6 +215,7 @@
         public boolean hasClatInterface;
         public boolean shouldHandleArpOffload;
         public boolean shouldHandleNdOffload;
+        public boolean shouldHandleMdnsOffload;
     }
 
     // Thread to listen for RAs.
@@ -305,6 +306,7 @@
     private final int mAcceptRaMinLft;
     private final boolean mShouldHandleArpOffload;
     private final boolean mShouldHandleNdOffload;
+    private final boolean mShouldHandleMdnsOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
@@ -406,6 +408,7 @@
         mAcceptRaMinLft = config.acceptRaMinLft;
         mShouldHandleArpOffload = config.shouldHandleArpOffload;
         mShouldHandleNdOffload = config.shouldHandleNdOffload;
+        mShouldHandleMdnsOffload = config.shouldHandleMdnsOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
@@ -2683,6 +2686,11 @@
         return shouldUseApfV6Generator() && mShouldHandleNdOffload;
     }
 
+    @Override
+    public boolean shouldUseMdnsOffload() {
+        return shouldUseApfV6Generator() && mShouldHandleMdnsOffload;
+    }
+
     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 9bc13bd..3333c29 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -31,6 +31,7 @@
 import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_RUNNING_TO_STOPPING;
 import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_STOPPING_TO_STOPPED;
 import static android.net.ip.IpClient.IpClientCommands.CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF;
+import static android.net.ip.IpClient.IpClientCommands.CMD_REMOVE_OFFLOAD_SERVICE;
 import static android.net.ip.IpClient.IpClientCommands.CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY;
 import static android.net.ip.IpClient.IpClientCommands.CMD_SET_MULTICAST_FILTER;
 import static android.net.ip.IpClient.IpClientCommands.CMD_START;
@@ -41,6 +42,7 @@
 import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_HTTP_PROXY;
 import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_L2INFORMATION;
 import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_L2KEY_CLUSTER;
+import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_OFFLOAD_SERVICE;
 import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_TCP_BUFFER_SIZES;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_DHCPACTION_TIMEOUT;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_IPV6_AUTOCONF_TIMEOUT;
@@ -52,6 +54,8 @@
 import static android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor.INetlinkMessageProcessor;
 import static android.net.ip.IpReachabilityMonitor.INVALID_REACHABILITY_LOSS_TYPE;
 import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
+import static android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK;
+import static android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_HEURISTIC_TRIGGERED;
@@ -109,7 +113,9 @@
 import android.net.apf.AndroidPacketFilter;
 import android.net.apf.ApfCapabilities;
 import android.net.apf.ApfFilter;
+import android.net.apf.ApfMdnsUtils;
 import android.net.apf.LegacyApfFilter;
+import android.net.apf.MdnsOffloadRule;
 import android.net.dhcp.DhcpClient;
 import android.net.dhcp.DhcpPacket;
 import android.net.dhcp6.Dhcp6Client;
@@ -118,6 +124,9 @@
 import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable;
 import android.net.networkstack.aidl.ip.ReachabilityLossReason;
+import android.net.nsd.NsdManager;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
 import android.net.shared.InitialConfiguration;
 import android.net.shared.Layer2Information;
 import android.net.shared.ProvisioningConfiguration;
@@ -146,6 +155,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
@@ -181,6 +191,7 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -595,6 +606,8 @@
         static final int CMD_UPDATE_APF_CAPABILITIES = 19;
         static final int EVENT_IPV6_AUTOCONF_TIMEOUT = 20;
         static final int CMD_UPDATE_APF_DATA_SNAPSHOT = 21;
+        static final int CMD_UPDATE_OFFLOAD_SERVICE = 22;
+        static final int CMD_REMOVE_OFFLOAD_SERVICE = 23;
         // Internal commands to use instead of trying to call transitionTo() inside
         // a given State's enter() method. Calling transitionTo() from enter/exit
         // encounters a Log.wtf() that can cause trouble on eng builds.
@@ -770,6 +783,7 @@
     private final boolean mPopulateLinkAddressLifetime;
     private final boolean mApfShouldHandleArpOffload;
     private final boolean mApfShouldHandleNdOffload;
+    private final boolean mApfShouldHandleMdnsOffload;
 
     private InterfaceParams mInterfaceParams;
 
@@ -785,6 +799,10 @@
     private String mTcpBufferSizes;
     private ProxyInfo mHttpProxy;
     private AndroidPacketFilter mApfFilter;
+    private final NsdManager mNsdManager;
+    @Nullable
+    private OffloadEngine mOffloadEngine;
+    private final List<OffloadServiceInfo> mOffloadServiceInfos = new ArrayList<>();
     private String mL2Key; // The L2 key for this network, for writing into the memory store
     private String mCluster; // The cluster for this network, for writing into the memory store
     private int mCreatorUid; // Uid of app creating the wifi configuration
@@ -998,6 +1016,7 @@
         // InterfaceController.Dependencies class.
         mNetd = deps.getNetd(mContext);
         mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog);
+        mNsdManager = context.getSystemService(NsdManager.class);
 
         mDhcp6PrefixDelegationEnabled = mDependencies.isFeatureEnabled(mContext,
                 IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION);
@@ -1020,6 +1039,8 @@
                 mContext, APF_HANDLE_ARP_OFFLOAD);
         mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
                 mContext, APF_HANDLE_ND_OFFLOAD);
+        // TODO: turn on APF mDNS offload.
+        mApfShouldHandleMdnsOffload = false;
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
 
@@ -2645,6 +2666,7 @@
         }
         apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
         apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
+        apfConfig.shouldHandleMdnsOffload = mApfShouldHandleMdnsOffload;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
         return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
@@ -2663,6 +2685,9 @@
             return false;
         }
         if (mApfFilter != null) {
+            if (SdkLevel.isAtLeastV() && mOffloadEngine != null) {
+                unregisterOffloadEngine();
+            }
             mApfFilter.shutdown();
         }
         mCurrentApfCapabilities = apfCapabilities;
@@ -3128,6 +3153,37 @@
         return mConfiguration.mIPv4ProvisioningMode != PROV_IPV4_DISABLED;
     }
 
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private OffloadEngine createMdnsOffloadEngine() {
+        return new OffloadEngine() {
+            @Override
+            public void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info) {
+                sendMessage(CMD_UPDATE_OFFLOAD_SERVICE, info);
+            }
+
+            @Override
+            public void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info) {
+                sendMessage(CMD_REMOVE_OFFLOAD_SERVICE, info);
+            }
+        };
+    }
+
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private void registerOffloadEngine() {
+        mOffloadEngine = createMdnsOffloadEngine();
+        mNsdManager.registerOffloadEngine(mInterfaceName,
+                OFFLOAD_TYPE_REPLY,
+                OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK,
+                Runnable::run, mOffloadEngine);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private void unregisterOffloadEngine() {
+        mNsdManager.unregisterOffloadEngine(mOffloadEngine);
+        mOffloadServiceInfos.clear();
+        mOffloadEngine = null;
+    }
+
     class RunningState extends State {
         private ConnectivityPacketTracker mPacketTracker;
         private boolean mDhcpActionInFlight;
@@ -3142,9 +3198,14 @@
             // at the beginning.
             mHasSeenClatInterface = false;
             mApfFilter = maybeCreateApfFilter(mCurrentApfCapabilities);
-            // If Apf supports ND offload, then turn off the vendor ND offload feature.
-            if (mApfFilter != null && mApfFilter.supportNdOffload()) {
-                mCallback.setNeighborDiscoveryOffload(false);
+            if (mApfFilter != null) {
+                // If Apf supports ND offload, then turn off the vendor ND offload feature.
+                if (mApfFilter.supportNdOffload()) {
+                    mCallback.setNeighborDiscoveryOffload(false);
+                }
+                if (mApfFilter.shouldUseMdnsOffload()) {
+                    registerOffloadEngine();
+                }
             }
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
             // rest of this IP configuration startup.
@@ -3206,6 +3267,9 @@
             }
 
             if (mApfFilter != null) {
+                if (SdkLevel.isAtLeastV() && mOffloadEngine != null) {
+                    unregisterOffloadEngine();
+                }
                 mApfFilter.shutdown();
                 mApfFilter = null;
             }
@@ -3353,6 +3417,18 @@
             }
         }
 
+        private void updateApfMdnsOffloadReplyRules() {
+            if (SdkLevel.isAtLeastV() && mApfFilter != null) {
+                try {
+                    final List<MdnsOffloadRule> rules =
+                            ApfMdnsUtils.extractOffloadReplyRule(mOffloadServiceInfos);
+                    mApfFilter.updateMdnsOffloadReplyRules(rules);
+                } catch (IOException e) {
+                    Log.wtf(TAG, "Failed to extract MdnsOffloadRule", e);
+                }
+            }
+        }
+
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
@@ -3452,7 +3528,19 @@
                     }
                     break;
                 }
-
+                case CMD_UPDATE_OFFLOAD_SERVICE: {
+                    final OffloadServiceInfo info = (OffloadServiceInfo) msg.obj;
+                    mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+                    mOffloadServiceInfos.add(info);
+                    updateApfMdnsOffloadReplyRules();
+                    break;
+                }
+                case CMD_REMOVE_OFFLOAD_SERVICE: {
+                    final OffloadServiceInfo info = (OffloadServiceInfo) msg.obj;
+                    mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+                    updateApfMdnsOffloadReplyRules();
+                    break;
+                }
                 case EVENT_DHCPACTION_TIMEOUT:
                     stopDhcpAction();
                     break;
@@ -3603,9 +3691,15 @@
                     final ApfCapabilities apfCapabilities = (ApfCapabilities) msg.obj;
                     if (handleUpdateApfCapabilities(apfCapabilities)) {
                         mApfFilter = maybeCreateApfFilter(apfCapabilities);
-                        // If Apf supports ND offload, then turn off the vendor ND offload feature.
-                        if (mApfFilter != null && mApfFilter.supportNdOffload()) {
-                            mCallback.setNeighborDiscoveryOffload(false);
+                        if (mApfFilter != null) {
+                            // If Apf supports ND offload, then turn off the vendor ND offload
+                            // feature.
+                            if (mApfFilter.supportNdOffload()) {
+                                mCallback.setNeighborDiscoveryOffload(false);
+                            }
+                            if (mApfFilter.shouldUseMdnsOffload()) {
+                                registerOffloadEngine();
+                            }
                         }
                     }
                     break;
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 71f7ebf..213ecd9 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -26,6 +26,11 @@
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.TYPE_A;
+import static com.android.net.module.util.NetworkStackConstants.TYPE_AAAA;
+import static com.android.net.module.util.NetworkStackConstants.TYPE_PTR;
+import static com.android.net.module.util.NetworkStackConstants.TYPE_SRV;
+import static com.android.net.module.util.NetworkStackConstants.TYPE_TXT;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTPROT_KERNEL;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELROUTE;
@@ -44,6 +49,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -78,10 +84,14 @@
 import android.net.apf.AndroidPacketFilter;
 import android.net.apf.ApfCapabilities;
 import android.net.apf.ApfFilter.ApfConfiguration;
+import android.net.apf.MdnsOffloadRule;
 import android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor;
 import android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor.INetlinkMessageProcessor;
 import android.net.ipmemorystore.NetworkAttributes;
 import android.net.metrics.IpConnectivityLog;
+import android.net.nsd.NsdManager;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
 import android.net.shared.InitialConfiguration;
 import android.net.shared.Layer2Information;
 import android.net.shared.ProvisioningConfiguration;
@@ -133,7 +143,6 @@
 import java.util.Random;
 import java.util.Set;
 
-
 /**
  * Tests for IpClient.
  */
@@ -191,6 +200,7 @@
     @Mock private PrintWriter mWriter;
     @Mock private IpClientNetlinkMonitor mNetlinkMonitor;
     @Mock private AndroidPacketFilter mApfFilter;
+    @Mock private NsdManager mNsdManager;
 
     private InterfaceParams mIfParams;
     private INetlinkMessageProcessor mNetlinkMessageProcessor;
@@ -216,7 +226,7 @@
         when(mDependencies.makeIpClientNetlinkMonitor(
                 any(), any(), any(), anyInt(), any())).thenReturn(mNetlinkMonitor);
         when(mNetlinkMonitor.start()).thenReturn(true);
-
+        doReturn(mNsdManager).when(mContext).getSystemService(eq(NsdManager.class));
         mIfParams = null;
     }
 
@@ -1051,6 +1061,92 @@
         clearInvocations(mCb);
     }
 
+    private static final String TEST_SERVICE_NAME = "NsdChat";
+    private static final String TEST_SERVICE_TYPE = "_http._tcp.local";
+    private static final String TEST_HOST_NAME = "Android.local";
+    private static final byte[] TEST_RAW_PACKET = new byte[]{1, 2, 3, 4, 5};
+    private static final byte[] ENCODED_FULL_TEST_SERVICE_NAME =
+            new byte[]{7, 'N', 'S', 'D', 'C', 'H', 'A', 'T', 5, '_', 'H', 'T', 'T', 'P', 4, '_',
+                    'T', 'C', 'P', 5, 'L', 'O', 'C', 'A', 'L', 0, 0};
+    private static final byte[] ENCODED_TEST_SERVICE_TYPE =
+            new byte[]{5, '_', 'H', 'T', 'T', 'P', 4, '_', 'T', 'C', 'P', 5, 'L', 'O', 'C', 'A',
+                    'L', 0, 0};
+    private static final byte[] ENCODED_TEST_HOST_NAME =
+            new byte[]{7, 'A', 'N', 'D', 'R', 'O', 'I', 'D', 5, 'L', 'O', 'C', 'A', 'L', 0, 0};
+
+    private static final List<MdnsOffloadRule> EXPECTED_RULES = List.of(new MdnsOffloadRule(
+            List.of(new MdnsOffloadRule.Matcher(ENCODED_TEST_SERVICE_TYPE, TYPE_PTR),
+                    new MdnsOffloadRule.Matcher(ENCODED_FULL_TEST_SERVICE_NAME, TYPE_SRV),
+                    new MdnsOffloadRule.Matcher(ENCODED_FULL_TEST_SERVICE_NAME, TYPE_TXT),
+                    new MdnsOffloadRule.Matcher(ENCODED_TEST_HOST_NAME, TYPE_A),
+                    new MdnsOffloadRule.Matcher(ENCODED_TEST_HOST_NAME, TYPE_AAAA)),
+            TEST_RAW_PACKET));
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testRegisterOffloadEngineWhenApfEnabledAtStart() throws Exception {
+        doReturn(mApfFilter).when(mDependencies).maybeCreateApfFilter(any(), any(), any(), any(),
+                any(), anyBoolean());
+        when(mApfFilter.shouldUseMdnsOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ProvisioningConfiguration config1 = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withApfCapabilities(new ApfCapabilities(
+                        APF_VERSION_6, 4096 /* maxProgramSize */, ARPHRD_ETHER)).build();
+        ipc.startProvisioning(config1);
+        final ArgumentCaptor<OffloadEngine> captor = ArgumentCaptor.forClass(OffloadEngine.class);
+        verify(mNsdManager, timeout(TEST_TIMEOUT_MS)).registerOffloadEngine(eq(TEST_IFNAME),
+                anyLong(), anyLong(), any(), captor.capture());
+        final OffloadEngine offloadEngine = captor.getValue();
+
+        final OffloadServiceInfo info = new OffloadServiceInfo(
+                new OffloadServiceInfo.Key(TEST_SERVICE_NAME, TEST_SERVICE_TYPE),
+                Collections.emptyList(), TEST_HOST_NAME, TEST_RAW_PACKET, 10, 1 /* offloadType */);
+        offloadEngine.onOffloadServiceUpdated(info);
+
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS)).updateMdnsOffloadReplyRules(EXPECTED_RULES);
+        offloadEngine.onOffloadServiceRemoved(info);
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS)).updateMdnsOffloadReplyRules(
+                Collections.emptyList());
+        verifyShutdown(ipc);
+        verify(mNsdManager).unregisterOffloadEngine(any());
+    }
+
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testRegisterOffloadEngineWhenApfCapabilitiesUpdated() throws Exception {
+        doReturn(mApfFilter).when(mDependencies).maybeCreateApfFilter(any(), any(), any(), any(),
+                any(), anyBoolean());
+        when(mApfFilter.shouldUseMdnsOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        final ProvisioningConfiguration config2 = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .build();
+        ipc.startProvisioning(config2);
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        verify(mNsdManager, never()).registerOffloadEngine(any(), anyLong(), anyLong(), any(),
+                any());
+        ipc.updateApfCapabilities(
+                new ApfCapabilities(APF_VERSION_6, 4096 /* maxProgramSize */, ARPHRD_ETHER));
+        final ArgumentCaptor<OffloadEngine> captor = ArgumentCaptor.forClass(OffloadEngine.class);
+        verify(mNsdManager, timeout(TEST_TIMEOUT_MS)).registerOffloadEngine(eq(TEST_IFNAME),
+                anyLong(), anyLong(), any(), captor.capture());
+        final OffloadEngine offloadEngine = captor.getValue();
+        final OffloadServiceInfo info = new OffloadServiceInfo(
+                new OffloadServiceInfo.Key(TEST_SERVICE_NAME, TEST_SERVICE_TYPE),
+                Collections.emptyList(), TEST_HOST_NAME, TEST_RAW_PACKET, 10, 1 /* offloadType */);
+        offloadEngine.onOffloadServiceUpdated(info);
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS)).updateMdnsOffloadReplyRules(EXPECTED_RULES);
+        offloadEngine.onOffloadServiceRemoved(info);
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS)).updateMdnsOffloadReplyRules(
+                Collections.emptyList());
+        verifyShutdown(ipc);
+        verify(mNsdManager).unregisterOffloadEngine(any());
+    }
+
     @Test
     public void testLinkPropertiesUpdate_callSetLinkPropertiesOnApfFilter() throws Exception {
         when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))