Make change and version bump to aml_net_331910000 for mainline module file: AndroidManifest.xml
Snap for 10066519 from 9f31270334e28a3bdc0a577e5e3541417f5ae5fd to mainline-networking-release

Change-Id: Ibd01e0844b949b6694ed999dc915ed333a62a350
diff --git a/Android.bp b/Android.bp
index 8c7253e..e10a30e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -452,6 +452,7 @@
     required: [
         "privapp_whitelist_com.android.networkstack",
     ],
+    updatable: true,
     lint: { strict_updatability_linting: true },
 }
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c75e832..ad39242 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -19,8 +19,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.android.networkstack"
   android:sharedUserId="android.uid.networkstack"
-  android:versionCode="331810000"
-  android:versionName="aml_net_331810000"
+  android:versionCode="331910000"
+  android:versionName="aml_net_331910000"
   coreApp="true"
 >
     <!--- Defines the MAINLINE_NETWORK_STACK permission used by the networkstack process. -->
@@ -43,10 +43,6 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <!-- Signature permission defined in NetworkStackStub -->
     <uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK" />
-    <!-- Used by creating test network -->
-    <uses-permission android:name="android.permission.MANAGE_TEST_NETWORKS" />
-    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <application
         android:extractNativeLibs="false"
         android:persistent="true"
diff --git a/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileBuilderShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index 46d6e13..0000000
--- a/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.apishim.api29;
-
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-
-/**
- * Implementation of Ikev2VpnProfileBuilderShim for API 29.
- *
- * @param <T> type of builder, typically Ikev2VpnProfile.Builder. This is necessary because at
- *            compile time, shims for older releases will not have access to this class as it
- *            debuted in SDK30. So the user of the shim has to pass it in.
- */
-// NOTE: The trick with the formal parameter only works because when this shim was introduced,
-// the stable API already contained the class that the caller needs to pass in; this won't
-// work for a class added in the latest API level.
-@RequiresApi(Build.VERSION_CODES.Q)
-public class Ikev2VpnProfileBuilderShimImpl<T> implements Ikev2VpnProfileBuilderShim<T> {
-}
diff --git a/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index f84999b..0000000
--- a/apishim/29/com/android/networkstack/apishim/api29/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.apishim.api29;
-
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-/**
- * Implementation of Ikev2VpnProfileShim for API 29.
- * @param <T> type of profile, typically Ikev2VpnProfile
- */
-public class Ikev2VpnProfileShimImpl<T> implements Ikev2VpnProfileShim<T> {
-}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java
index 9a24e63..2597840 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java
@@ -37,8 +37,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.R)
 public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api29.Ikev2VpnProfileBuilderShimImpl<
-        Ikev2VpnProfile.Builder> {
+        implements Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> {
     protected final Ikev2VpnProfile.Builder mBuilder;
 
     protected Ikev2VpnProfileBuilderShimImpl(@NonNull Ikev2VpnProfile.Builder builder) {
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java
index 389bc44..54a2a35 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java
@@ -25,11 +25,8 @@
  * Implementation of Ikev2VpnProfileShim for API 30.
  */
 // TODO : when API29 is no longer supported, remove the type argument
-public class Ikev2VpnProfileShimImpl extends
-        com.android.networkstack.apishim.api29.Ikev2VpnProfileShimImpl<Ikev2VpnProfile> {
-    protected Ikev2VpnProfile mProfile;
-
-    protected Ikev2VpnProfileShimImpl() {}
+public class Ikev2VpnProfileShimImpl implements Ikev2VpnProfileShim<Ikev2VpnProfile> {
+    protected final Ikev2VpnProfile mProfile;
 
     protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
         mProfile = profile;
diff --git a/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileBuilderShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index 36363ef..0000000
--- a/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.apishim.api31;
-
-import android.net.Ikev2VpnProfile;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-/**
- * Implementation of Ikev2VpnProfileBuilderShim for API 31.
- */
-@RequiresApi(Build.VERSION_CODES.S)
-public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl {
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull Ikev2VpnProfile.Builder builder) {
-        super(builder);
-    }
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr,
-            @NonNull String identity) {
-        super(serverAddr, identity);
-    }
-}
diff --git a/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileShimImpl.java b/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index 8e0c078..0000000
--- a/apishim/31/com/android/networkstack/apishim/api31/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.networkstack.apishim.api31;
-
-/**
- * Implementation of Ikev2VpnProfileShim for API 31.
- */
-public class Ikev2VpnProfileShimImpl extends
-        com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl {
-}
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java
index 64c7dad..308407b 100644
--- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java
+++ b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java
@@ -32,7 +32,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api31.Ikev2VpnProfileBuilderShimImpl {
+        extends com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl {
     protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) {
         super(new Ikev2VpnProfile.Builder(params));
     }
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java
index 6821326..8e82218 100644
--- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java
+++ b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java
@@ -16,9 +16,14 @@
 
 package com.android.networkstack.apishim.api33;
 
+import android.net.Ikev2VpnProfile;
+
 /**
  * A shim for Ikev2VpnProfile
  */
 public class Ikev2VpnProfileShimImpl
         extends com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl {
+    protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
+        super(profile);
+    }
 }
diff --git a/apishim/34/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java b/apishim/34/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java
index cd4a8ca..23e5ffc 100644
--- a/apishim/34/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java
+++ b/apishim/34/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java
@@ -29,7 +29,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 public class Ikev2VpnProfileShimImpl
-        extends com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl {
+        extends com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl {
     protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
         super(profile);
     }
@@ -41,7 +41,7 @@
         if (SdkLevel.isAtLeastU()) {
             return new Ikev2VpnProfileShimImpl(profile);
         } else {
-            return com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl
+            return com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl
                     .newInstance(profile);
         }
     }
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index 89de36b..035ddc7 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -128,30 +128,40 @@
         },
     },
     imports: ["ipmemorystore-aidl-interfaces-V10"],
-    versions: [
-        "1",
-        "2",
-        "3",
-        "4",
-        "5",
-        "6",
-        "7",
-        "8",
-        "9",
-        "10",
-        "11",
-        "12",
-        "13",
-        "14",
-        "15",
-        "16",
-    ],
     // TODO: have tethering depend on networkstack-client and set visibility to private
     visibility: [
         "//system/tools/aidl/build",
         "//frameworks/base/packages/Tethering",
         "//packages/modules/Connectivity/Tethering",
     ],
+    versions_with_info: [
+        // Remove old networkstack aidl interface version info that is no longer used.
+        {
+            version: "13",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
+        {
+            version: "14",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
+        {
+            version: "15",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
+        {
+            version: "16",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
+        {
+            version: "17",
+            imports: ["ipmemorystore-aidl-interfaces-V10"],
+        },
+
+    ],
+
+    // "frozen: true" is removed manually after each freeze, this property cannot be unrecognized
+    // in some downstream branches and TH will get the build error, see b/262507066 for details.
+
 }
 
 java_library {
@@ -160,7 +170,7 @@
     min_sdk_version: "29",
     static_libs: [
         "ipmemorystore-aidl-interfaces-V10-java",
-        "networkstack-aidl-interfaces-V16-java",
+        "networkstack-aidl-interfaces-V17-java",
     ],
     visibility: ["//packages/modules/NetworkStack:__subpackages__"],
     apex_available: [
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/.hash
new file mode 100644
index 0000000..a9db7c1
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/.hash
@@ -0,0 +1 @@
+e29ecca6f4e40189d76cf0a1ac6cbc7bccd1db8a
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..771deda
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..31f2194
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..fb13c0c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitor.aidl
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkConnectedParcel(in android.net.networkstack.aidl.NetworkMonitorParameters params);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 0x01;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 0x04;
+  const int NETWORK_VALIDATION_PROBE_DNS = 0x04;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 0x08;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 0x10;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 0x20;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 0x40;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..36eda8e
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  oneway void showProvisioningNotification(String action, String packageName) = 3;
+  oneway void hideProvisioningNotification() = 4;
+  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..8120ffc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..0b6b778
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..6103774
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6a597e6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..83796ee
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..4b3fff5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..18cf954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..4d6d5a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..1457caf
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..fba524b
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,63 @@
+/*
+**
+** Copyright (C) 2019 The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ProvisioningConfigurationParcelable {
+  /**
+   * @deprecated use ipv4ProvisioningMode instead.
+   */
+  boolean enableIPv4;
+  /**
+   * @deprecated use ipv6ProvisioningMode instead.
+   */
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+  @nullable List<android.net.networkstack.aidl.dhcp.DhcpOption> options;
+  int ipv4ProvisioningMode;
+  int ipv6ProvisioningMode;
+  boolean uniqueEui64AddressesOnly;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..94fc27f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..0e1c21c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3cd8860
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..7997936
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,49 @@
+/**
+ *
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+  int leasesSubnetPrefixLength = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..9312f47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..1109f35
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..ab8577c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..3ba1d0b
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClient.aidl
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+  const int PROV_IPV4_DISABLED = 0x00;
+  const int PROV_IPV4_STATIC = 0x01;
+  const int PROV_IPV4_DHCP = 0x02;
+  const int PROV_IPV6_DISABLED = 0x00;
+  const int PROV_IPV6_SLAAC = 0x01;
+  const int PROV_IPV6_LINKLOCAL = 0x02;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9d36419
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2019, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+  oneway void onReachabilityFailure(in android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable lossInfo);
+  oneway void setMaxDtimMultiplier(int multiplier);
+  const int DTIM_MULTIPLIER_RESET = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/NetworkMonitorParameters.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
new file mode 100644
index 0000000..2ab9db0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
@@ -0,0 +1,41 @@
+/**
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl;
+@JavaDerive(equals=true, toString=true)
+parcelable NetworkMonitorParameters {
+  android.net.NetworkAgentConfig networkAgentConfig;
+  android.net.NetworkCapabilities networkCapabilities;
+  android.net.LinkProperties linkProperties;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..eea3e0d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+  byte type;
+  @nullable byte[] value;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
new file mode 100644
index 0000000..bb88434
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.ip;
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ReachabilityLossInfoParcelable {
+  String message;
+  android.net.networkstack.aidl.ip.ReachabilityLossReason reason;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
new file mode 100644
index 0000000..f9bb3c4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/17/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.ip;
+@Backing(type="int")
+enum ReachabilityLossReason {
+  ROAM,
+  CONFIRM,
+  ORGANIC,
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
index fc6a70e..fb13c0c 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitor.aidl
@@ -49,12 +49,12 @@
   const int NETWORK_TEST_RESULT_VALID = 0;
   const int NETWORK_TEST_RESULT_INVALID = 1;
   const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
-  const int NETWORK_VALIDATION_RESULT_VALID = 1;
-  const int NETWORK_VALIDATION_RESULT_PARTIAL = 2;
-  const int NETWORK_VALIDATION_RESULT_SKIPPED = 4;
-  const int NETWORK_VALIDATION_PROBE_DNS = 4;
-  const int NETWORK_VALIDATION_PROBE_HTTP = 8;
-  const int NETWORK_VALIDATION_PROBE_HTTPS = 16;
-  const int NETWORK_VALIDATION_PROBE_FALLBACK = 32;
-  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 64;
+  const int NETWORK_VALIDATION_RESULT_VALID = 0x01;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 0x04;
+  const int NETWORK_VALIDATION_PROBE_DNS = 0x04;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 0x08;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 0x10;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 0x20;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 0x40;
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
index a97511e..3ba1d0b 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClient.aidl
@@ -49,10 +49,10 @@
   oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
   oneway void notifyPreconnectionComplete(boolean success);
   oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
-  const int PROV_IPV4_DISABLED = 0;
-  const int PROV_IPV4_STATIC = 1;
-  const int PROV_IPV4_DHCP = 2;
-  const int PROV_IPV6_DISABLED = 0;
-  const int PROV_IPV6_SLAAC = 1;
-  const int PROV_IPV6_LINKLOCAL = 2;
+  const int PROV_IPV4_DISABLED = 0x00;
+  const int PROV_IPV4_STATIC = 0x01;
+  const int PROV_IPV4_DHCP = 0x02;
+  const int PROV_IPV6_DISABLED = 0x00;
+  const int PROV_IPV6_SLAAC = 0x01;
+  const int PROV_IPV6_LINKLOCAL = 0x02;
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
index 24bbf64..9d36419 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/ip/IIpClientCallbacks.aidl
@@ -49,4 +49,6 @@
   oneway void setNeighborDiscoveryOffload(boolean enable);
   oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
   oneway void onReachabilityFailure(in android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable lossInfo);
+  oneway void setMaxDtimMultiplier(int multiplier);
+  const int DTIM_MULTIPLIER_RESET = 0;
 }
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
index 70a7db2..f9bb3c4 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
@@ -34,7 +34,7 @@
 package android.net.networkstack.aidl.ip;
 @Backing(type="int")
 enum ReachabilityLossReason {
-  ROAM = 0,
-  CONFIRM = 1,
-  ORGANIC = 2,
+  ROAM,
+  CONFIRM,
+  ORGANIC,
 }
diff --git a/common/networkstackclient/src/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/src/android/net/ip/IIpClientCallbacks.aidl
index 932672e..5bb0cf3 100644
--- a/common/networkstackclient/src/android/net/ip/IIpClientCallbacks.aidl
+++ b/common/networkstackclient/src/android/net/ip/IIpClientCallbacks.aidl
@@ -72,4 +72,20 @@
     // Called when the internal IpReachabilityMonitor (if enabled) has detected the loss of a
     // critical number of required neighbors or DHCP roaming fails.
     void onReachabilityFailure(in ReachabilityLossInfoParcelable lossInfo);
+
+    // Reset the DTIM multiplier to the default hardware driver value.
+    const int DTIM_MULTIPLIER_RESET = 0;
+
+    // Set maximum acceptable DTIM multiplier to hardware driver. Any multiplier larger than the
+    // maximum value must not be accepted, it will cause packet loss higher than what the system
+    // can accept, which will cause unexpected behavior for apps, and may interrupt the network
+    // connection.
+    //
+    // DTIM multiplier controls how often the device should wake up to receive multicast/broadcast
+    // packets. Typically the wake up interval is decided by multiplier * AP's DTIM period if
+    // multiplier is non-zero. For example, when hardware driver sets the DTIM multiplier to 2, it
+    // means device wakes up once every 2 DTIM periods, 50% of multicast packets will be dropped.
+    // Setting DTIM multiplier to DTIM_MULTIPLIER_RESET(0) applies the hardware driver default
+    // value.
+    void setMaxDtimMultiplier(int multiplier);
 }
diff --git a/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java b/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java
index 7cf46f5..e46d4c9 100644
--- a/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java
+++ b/common/networkstackclient/src/android/net/ip/IpClientCallbacks.java
@@ -146,4 +146,11 @@
         // onReachabilityLost method.
         onReachabilityLost(lossInfo.message);
     }
+
+    /**
+     * Set maximum acceptable DTIM multiplier to hardware driver.
+     *
+     * @param multiplier an integer maximum DTIM multiplier value to set.
+     */
+    public void setMaxDtimMultiplier(int multiplier) {}
 }
diff --git a/common/networkstackclient/src/android/net/ip/IpClientUtil.java b/common/networkstackclient/src/android/net/ip/IpClientUtil.java
index 7636abd..b953d35 100644
--- a/common/networkstackclient/src/android/net/ip/IpClientUtil.java
+++ b/common/networkstackclient/src/android/net/ip/IpClientUtil.java
@@ -198,6 +198,12 @@
             mCb.onReachabilityFailure(lossInfo);
         }
 
+        // Set maximum acceptable DTIM multiplier to hardware driver.
+        @Override
+        public void setMaxDtimMultiplier(int multiplier) {
+            mCb.setMaxDtimMultiplier(multiplier);
+        }
+
         @Override
         public int getInterfaceVersion() {
             return this.VERSION;
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp
index e63483f..5d84f57 100644
--- a/jni/network_stack_utils_jni.cpp
+++ b/jni/network_stack_utils_jni.cpp
@@ -99,17 +99,15 @@
     }
 }
 
-static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jclass clazz, jobject javaFd, jboolean dropMF) {
-    const __u32 frag_mask = static_cast<__u32>((dropMF ? IP_MF : 0) | IP_OFFMASK);
-
-    sock_filter filter_code[] = {
+static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jclass clazz, jobject javaFd) {
+    static sock_filter filter_code[] = {
         // Check the protocol is UDP.
         BPF_STMT(BPF_LD  | BPF_B    | BPF_ABS, kIPv4Protocol),
         BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   IPPROTO_UDP, 0, 6),
 
         // Check this is not a fragment.
         BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   frag_mask, 4, 0),
+        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_MF | IP_OFFMASK, 4, 0),
 
         // Get the IP header length.
         BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
@@ -253,7 +251,7 @@
 static const JNINativeMethod gNetworkStackUtilsMethods[] = {
     /* name, signature, funcPtr */
     { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_addArpEntry },
-    { "attachDhcpFilter", "(Ljava/io/FileDescriptor;Z)V", (void*) network_stack_utils_attachDhcpFilter },
+    { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachDhcpFilter },
     { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachRaFilter },
     { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachControlPacketFilter },
 };
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
index db51186..ee713c5 100644
--- a/src/android/net/apf/ApfGenerator.java
+++ b/src/android/net/apf/ApfGenerator.java
@@ -282,10 +282,6 @@
             }
             // Calculate distance from end of this instruction to instruction.offset.
             final int targetLabelOffset = targetLabelInstruction.offset - (offset + size());
-            if (targetLabelOffset < 0) {
-                throw new IllegalInstructionException("backward branches disallowed; label: " +
-                        mTargetLabel);
-            }
             return targetLabelOffset;
         }
 
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java
new file mode 100644
index 0000000..6b0bdc4
--- /dev/null
+++ b/src/android/net/apf/DnsUtils.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.apf;
+
+import static android.net.apf.ApfGenerator.Register.R0;
+import static android.net.apf.ApfGenerator.Register.R1;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility class that generates generating APF filters for DNS packets.
+ */
+public class DnsUtils {
+
+    /** Length of the DNS header. */
+    private static final int DNS_HEADER_LEN = 12;
+    /** Offset of the qdcount field within the DNS header. */
+    private static final int DNS_QDCOUNT_OFFSET = 4;
+
+    // Static labels
+    private static final String LABEL_START_MATCH = "start_match";
+    private static final String LABEL_PARSE_DNS_LABEL = "parse_dns_label";
+    private static final String LABEL_FIND_NEXT_DNS_QUESTION = "find_next_dns_question";
+
+    // Length of the pointers used by compressed names.
+    private static final int LABEL_SIZE = Byte.BYTES;
+    private static final int POINTER_SIZE = Short.BYTES;
+    private static final int QUESTION_HEADER_SIZE = Short.BYTES + Short.BYTES;
+    private static final int LABEL_AND_QUESTION_HEADER_SIZE = LABEL_SIZE + QUESTION_HEADER_SIZE;
+    private static final int POINTER_AND_QUESTION_HEADER_SIZE = POINTER_SIZE + QUESTION_HEADER_SIZE;
+
+    /** Memory slot that stores the offset within the packet of the DNS header. */
+    private static final int SLOT_DNS_HEADER_OFFSET = 1;
+    /** Memory slot that stores the current parsing offset. */
+    private static final int SLOT_CURRENT_PARSE_OFFSET = 2;
+    /**
+     * Memory slot that stores the offset after the current question, if the code is currently
+     * parsing a pointer, or 0 if it is not.
+     */
+    private static final int SLOT_AFTER_POINTER_OFFSET = 3;
+    /**
+     * Contains qdcount remaining, as a negative number. For example, will be -1 when starting to
+     * parse a DNS packet with one question in it. It's stored as a negative number because adding 1
+     * is much easier than subtracting 1 (which can't be done just by adding -1, because that just
+     * adds 254).
+     */
+    private static final int SLOT_NEGATIVE_QDCOUNT_REMAINING = 6;
+    /** Memory slot used by the jump table. */
+    private static final int SLOT_RETURN_VALUE_INDEX = 10;
+
+    /**
+     * APF function: parse_dns_label
+     *
+     * Parses a label potentially containing a pointer, and calculates the label length and the
+     * offset of the label data.
+     *
+     * Inputs:
+     * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header
+     * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset
+     * - m[SLOT_AFTER_POINTER_OFFSET]: offset after the question (e.g., offset of the next question,
+     *        or offset of the answer section) if a pointer is being chased, 0 otherwise
+     * - m[SLOT_RETURN_VALUE_INDEX]: index into return jump table
+     *
+     * Outputs:
+     * - R1: label length
+     * - m[SLOT_CURRENT_PARSE_OFFSET]: offset of label text
+     */
+    private static void genParseDnsLabel(ApfGenerator gen, JumpTable jumpTable) throws Exception {
+        final String labelParseDnsLabelReal = "parse_dns_label_real";
+        final String labelPointerOffsetStored = "pointer_offset_stored";
+
+        /**
+         * :parse_dns_label
+         * // Load parsing offset.
+         * LDM R1, 2                        // R1 = parsing offset. (All indexed loads use R1.)
+         */
+        gen.defineLabel(LABEL_PARSE_DNS_LABEL);
+        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
+
+
+        /**
+         * // Check that we’re in the DNS packet, i.e., that R1 >= m[SLOT_DNS_HEADER_OFFSET].
+         * LDM R0, 1                        // R0 = DNS header offset
+         * JGT R0, R1, DROP                 // Bad pointer. Drop.
+         */
+        gen.addLoadFromMemory(R0, SLOT_DNS_HEADER_OFFSET);
+        gen.addJumpIfR0GreaterThanR1(ApfGenerator.DROP_LABEL);
+
+        /**
+         * // Now parse the label.
+         * LDBX R0, [R1+0]                  // R0 = label length, R1 = parsing offset
+         * AND R0, 0xc0                     // Is this a pointer?
+         *
+         * JEQ R0, 0, :parse_dns_label_real
+         */
+        gen.addLoad8Indexed(R0, 0);
+        gen.addAnd(0xc0);
+        gen.addJumpIfR0Equals(0, labelParseDnsLabelReal);
+
+
+        /**
+         * // If we’re not already chasing a pointer, store offset after pointer into
+         * // m[SLOT_AFTER_POINTER_OFFSET].
+         * LDM R0, 3                        // R0 = previous offset after pointer
+         * JNE 0, :pointer_offset_stored
+         * MOV R0, R1                       // R0 = R1
+         * ADD R0, 6                        // R0 = offset after pointer and record
+         * STM R0, 3                        // Store offset after pointer
+         */
+        gen.addLoadFromMemory(R0, SLOT_AFTER_POINTER_OFFSET);
+        gen.addJumpIfR0NotEquals(0, labelPointerOffsetStored);
+        gen.addMove(R0);
+        gen.addAdd(POINTER_AND_QUESTION_HEADER_SIZE);
+        gen.addStoreToMemory(R0, SLOT_AFTER_POINTER_OFFSET);
+
+        /**
+         * :pointer_offset_stored
+         * LDHX R0, [R1+0]                  // R0 = 2-byte pointer value
+         * AND R0, 0x3ff                    // R0 = pointer destination offset (from DNS header)
+         * LDM R1, 1                        // R1 = offset in packet of DNS header
+         * ADD R0, R1                       // R0 = pointer destination offset
+         * LDM R1, 2                        // R1 = current parsing offset
+         * JEQ R0, R1, DROP                 // Drop if pointer points here...
+         * JGT R0, R1, DROP                 // ... or after here (must point backwards)
+         * STM R0, 2                        // Set next parsing offset to pointer destination
+         */
+        gen.defineLabel(labelPointerOffsetStored);
+        gen.addLoad16Indexed(R0, 0);
+        gen.addAnd(0x3ff);
+        gen.addLoadFromMemory(R1, SLOT_DNS_HEADER_OFFSET);
+        gen.addAddR1();
+        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
+        gen.addJumpIfR0EqualsR1(ApfGenerator.DROP_LABEL);
+        gen.addJumpIfR0GreaterThanR1(ApfGenerator.DROP_LABEL);
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+
+        /** // Pointer chased. Parse starting from the pointer destination (which may also be a
+         * pointer).
+         * JMP :parse_dns_label
+         */
+        gen.addJump(LABEL_PARSE_DNS_LABEL);
+
+        /**
+         * :parse_real_label
+         * // This is where the real (non-pointer) label starts.
+         * // Load label length into R1, and return to caller.
+         * // m[SLOT_CURRENT_PARSE_OFFSET] already contains label offset.
+         * LDHX R1 [R1+0]                   // R1 = label length
+         */
+        gen.defineLabel(labelParseDnsLabelReal);
+        gen.addLoad8Indexed(R1, 0);
+
+        /** // Return
+         * LDM R0, 10
+         * JMP :jump_table
+         */
+        gen.addLoadFromMemory(R0, SLOT_RETURN_VALUE_INDEX);
+        gen.addJump(jumpTable.getStartLabel());
+    }
+
+    /**
+     * APF function: find_next_dns_question
+     *
+     * Finds the next question in the question section, or drops the packet if there is none.
+     *
+     * Inputs:
+     * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset
+     * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, or 0 if not chasing a
+     *           pointer
+     * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: qdcount remaining, as a negative number. This is
+     *           because adding 1 is much easier than subtracting 1 (which can't be done just by
+     *           adding -1, because that just adds 254)
+     * - m[SLOT_RETURN_VALUE_INDEX]: index into return jump table
+     *
+     * Outputs:
+     * None
+     */
+    private static void genFindNextDnsQuestion(ApfGenerator gen, JumpTable jumpTable)
+            throws Exception {
+        final String labelFindNextDnsQuestionFollow = "find_next_dns_question_follow";
+        final String labelFindNextDnsQuestionLabel = "find_next_dns_question_label";
+        final String labelFindNextDnsQuestionLoop = "find_next_dns_question_loop";
+        final String labelFindNextDnsQuestionNoPointer = "find_next_dns_question_no_pointer";
+        final String labelFindNextDnsQuestionReturn = "find_next_dns_question_return";
+
+        // Function entry point.
+        gen.defineLabel(LABEL_FIND_NEXT_DNS_QUESTION);
+
+        // Are we chasing a pointer?
+        gen.addLoadFromMemory(R0, SLOT_AFTER_POINTER_OFFSET);
+        gen.addJumpIfR0Equals(0, labelFindNextDnsQuestionFollow);
+
+        // If so, offset after the pointer and question is stored in m[SLOT_AFTER_POINTER_OFFSET].
+        // Move parsing offset there, clear m[SLOT_AFTER_POINTER_OFFSET], and return.
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+        gen.addLoadImmediate(R0, 0);
+        gen.addStoreToMemory(R0, SLOT_AFTER_POINTER_OFFSET);
+        gen.addJump(labelFindNextDnsQuestionReturn);
+
+        // We weren't chasing a pointer. Loop, following the label chain, until we reach a
+        // zero-length label or a pointer. At the beginning of the loop, the current parsing offset
+        // is m[SLOT_CURRENT_PARSE_OFFSET]. Move it to R1 and keep it in R1 throughout the loop.
+        gen.defineLabel(labelFindNextDnsQuestionFollow);
+        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
+
+        // Load label length.
+        gen.defineLabel(labelFindNextDnsQuestionLoop);
+        gen.addLoad8Indexed(R0, 0);
+        // Is it a pointer?
+        gen.addAnd(0xc0);
+        gen.addJumpIfR0Equals(0, labelFindNextDnsQuestionNoPointer);
+        // It's a pointer. Skip the pointer and question, and return.
+        gen.addLoadImmediate(R0, POINTER_AND_QUESTION_HEADER_SIZE);
+        gen.addAddR1();
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+        gen.addJump(labelFindNextDnsQuestionReturn);
+
+        // R1 still contains parsing offset.
+        gen.defineLabel(labelFindNextDnsQuestionNoPointer);
+        gen.addLoad8Indexed(R0, 0);
+
+        // Zero-length label? We're done.
+        // Skip the label (1 byte) and query (2 bytes qtype, 2 bytes qclass) and return.
+        gen.addJumpIfR0NotEquals(0, labelFindNextDnsQuestionLabel);
+        gen.addLoadImmediate(R0, LABEL_AND_QUESTION_HEADER_SIZE);
+        gen.addAddR1();
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+        gen.addJump(labelFindNextDnsQuestionReturn);
+
+        // Non-zero length label. Consume it and continue.
+        gen.defineLabel(labelFindNextDnsQuestionLabel);
+        gen.addAdd(1);
+        gen.addAddR1();
+        gen.addMove(R1);
+        gen.addJump(labelFindNextDnsQuestionLoop);
+
+        gen.defineLabel(labelFindNextDnsQuestionReturn);
+
+        // Is this the last question? If so, drop.
+        gen.addLoadFromMemory(R0, SLOT_NEGATIVE_QDCOUNT_REMAINING);
+        gen.addAdd(1);
+        gen.addStoreToMemory(R0, SLOT_NEGATIVE_QDCOUNT_REMAINING);
+        gen.addJumpIfR0Equals(0, ApfGenerator.DROP_LABEL);
+
+        // If not, return.
+        gen.addJump(jumpTable.getStartLabel());
+    }
+
+    /** @return jump label that points to the start of a DNS label's parsing code. */
+    private static String getStartMatchLabel(int labelIndex) {
+        return "dns_parse_" + labelIndex;
+    }
+
+    /** @return jump label used while parsing the specified DNS label. */
+    private static String getPostMatchJumpTargetForLabel(int labelIndex) {
+        return "dns_parsed_" + labelIndex;
+    }
+
+    /** @return jump label used when the match for the specified DNS label fails. */
+    private static String getNoMatchLabel(int labelIndex) {
+        return "dns_nomatch_" + labelIndex;
+    }
+
+    private static void addMatchLabel(@NonNull ApfGenerator gen, @NonNull JumpTable jumpTable,
+            int labelIndex, @NonNull String label, @NonNull String nextLabel) throws Exception {
+        final String parsedLabel = getPostMatchJumpTargetForLabel(labelIndex);
+        final String noMatchLabel = getNoMatchLabel(labelIndex);
+        gen.defineLabel(getStartMatchLabel(labelIndex));
+
+        // Store return address.
+        gen.addLoadImmediate(R0, jumpTable.getIndex(parsedLabel));
+        gen.addStoreToMemory(R0, SLOT_RETURN_VALUE_INDEX);
+
+        // Call the parse_label function.
+        gen.addJump(LABEL_PARSE_DNS_LABEL);
+
+        gen.defineLabel(parsedLabel);
+
+        // If label length is 0, this is the end of the name and the match failed.
+        gen.addSwap(); // Move label length from R1 to R0
+        gen.addJumpIfR0Equals(0, noMatchLabel);
+
+        // Label parsed, check it matches what we're looking for.
+        gen.addJumpIfR0NotEquals(label.length(), noMatchLabel);
+        gen.addLoadFromMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+        gen.addAdd(1);
+        gen.addJumpIfBytesNotEqual(R0, label.getBytes(), noMatchLabel);
+
+        // Prep offset of next label.
+        gen.addAdd(label.length());
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+
+        // Match, go to next label.
+        gen.addJump(nextLabel);
+
+        // Match failed. Go to next name, and restart from the first match.
+        gen.defineLabel(noMatchLabel);
+        gen.addLoadImmediate(R1, jumpTable.getIndex(LABEL_START_MATCH));
+        gen.addStoreToMemory(R1, SLOT_RETURN_VALUE_INDEX);
+        gen.addJump(LABEL_FIND_NEXT_DNS_QUESTION);
+    }
+
+    /**
+     * Generates a filter that accepts DNS packet that ask for the specified name.
+     *
+     * The filter supports compressed DNS names and scanning through multiple questions in the same
+     * packet, e.g., as used by MDNS. However, it currently only supports one DNS name.
+     *
+     * Limitations:
+     * <ul>
+     * <li>Filter size is just under 300 bytes for a typical question.
+     * <li>Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter
+     *   instruction limit. This limit causes the APF interpreter to accept the packet once it has
+     *   executed a number of instructions equal to the program length in bytes.
+     *   A program that consists *only* of this filter will be able to execute just under 300
+     *   instructions, and will be able to correctly drop packets with two questions but not three
+     *   questions. In a real APF setup, there will be other code (e.g., RA filtering) which counts
+     *   against the limit, so the filter should be able to parse packets with more questions.
+     * <li>Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is
+     *   likely impossible to overcome without interpreter changes.
+     * </ul>
+     *
+     * TODO:
+     * <ul>
+     * <li>Add unit tests for the parse_dns_label and find_next_dns_question functions.
+     * <li>Support accepting more than one name.
+     * <li>For devices where power saving is a priority (e.g., flat panel TVs), add support for
+     *   dropping packets with more than X queries, to ensure the filter will drop the packet rather
+     *   than hit the instruction limit.
+     * </ul>
+     */
+    public static void generateFilter(ApfGenerator gen, String[] labels) throws Exception {
+        final int etherPlusUdpLen = ETHER_HEADER_LEN + UDP_HEADER_LEN;
+
+        final String labelJumpTable = "jump_table";
+
+        // Initialize parsing
+        /**
+         * - R1: length of IP header.
+         * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header
+         * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset (start of question section)
+         * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, must be 0 when
+         *                                 starting a new name
+         * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: negative qdcount
+         */
+        // Move IP header length to R0 and use it to find the DNS header offset.
+        // TODO: this uses R1 for consistency with ApfFilter#generateMdnsFilterLocked. Evaluate
+        // using R0 instead.
+        gen.addMove(R0);
+        gen.addAdd(etherPlusUdpLen);
+        gen.addStoreToMemory(R0, SLOT_DNS_HEADER_OFFSET);
+
+        gen.addAdd(DNS_QDCOUNT_OFFSET);
+        gen.addMove(R1);
+        gen.addLoad16Indexed(R1, 0);
+        gen.addNeg(R1);
+        gen.addStoreToMemory(R1, SLOT_NEGATIVE_QDCOUNT_REMAINING);
+
+        gen.addAdd(DNS_HEADER_LEN - DNS_QDCOUNT_OFFSET);
+        gen.addStoreToMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
+
+        gen.addLoadImmediate(R0, 0);
+        gen.addStoreToMemory(R0, SLOT_AFTER_POINTER_OFFSET);
+
+        gen.addJump(LABEL_START_MATCH);
+
+        // Create JumpTable but
+        final JumpTable table = new JumpTable(labelJumpTable, SLOT_RETURN_VALUE_INDEX);
+
+        // Generate bytecode for parse_label function.
+        genParseDnsLabel(gen, table);
+        genFindNextDnsQuestion(gen, table);
+
+        // Populate jump table. Should be before the code that calls to it (i.e., the addMatchLabel
+        // calls below) because otherwise all the jumps are backwards, and backwards jumps are more
+        // expensive (5 bytes of bytecode)
+        for (int i = 0; i < labels.length; i++) {
+            table.addLabel(getPostMatchJumpTargetForLabel(i));
+        }
+        table.addLabel(LABEL_START_MATCH);
+        table.generate(gen);
+
+        // Add match statements for name.
+        gen.defineLabel(LABEL_START_MATCH);
+        for (int i = 0; i < labels.length; i++) {
+            final String nextLabel = (i == labels.length - 1)
+                    ? ApfGenerator.PASS_LABEL
+                    : getStartMatchLabel(i + 1);
+            addMatchLabel(gen, table, i, labels[i], nextLabel);
+        }
+        gen.addJump(ApfGenerator.DROP_LABEL);
+    }
+
+    private DnsUtils() {
+    }
+}
diff --git a/src/android/net/apf/JumpTable.java b/src/android/net/apf/JumpTable.java
new file mode 100644
index 0000000..b449697
--- /dev/null
+++ b/src/android/net/apf/JumpTable.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.apf;
+
+import static android.net.apf.ApfGenerator.Register.R0;
+
+import androidx.annotation.NonNull;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+/**
+ * A table that stores program labels to jump to.
+ *
+ * This is needed to implement subroutines because APF jump targets must be known at compile
+ * time and cannot be computed dynamically.
+ *
+ * At compile time, any code that calls a subroutine must:
+ *
+ * <ul>
+ * <li>Define a label (via {@link ApfGenerator#defineLabel}) immediately after the code that invokes
+ *     the subroutine.
+ * <li>Add the label to the jump table using {@link #addLabel}.
+ * <li>Generate the jump table in the program.
+ * </ul>
+ *
+ * <p>At runtime, before invoking the subroutine, the APF code must store the index of the return
+ * label (obtained via {@link #getIndex}) into the jump table's return address memory slot, and then
+ * jump to the subroutine. To return to the caller, the subroutine must jump to the label returned
+ * by {@link #getStartLabel}, and the jump table will then jump to the return label.
+ *
+ * <p>Implementation details:
+ * <ul>
+ * <li>The jumps are added to the program in the same order as the labels were added.
+ * <li>Using the jump table will overwrite the value of register R0.
+ * <li>If, before calling a subroutine, the APF code stores a nonexistent return label index, then
+ *     the jump table will pass the packet. This cannot happen if the code correctly obtains the
+ *     label using {@link #getIndex}, as that would throw an exception when generating the program.
+ * </ul>
+ *
+ * For example:
+ * <pre>
+ *     JumpTable t = new JumpTable("my_jump_table", 7);
+ *     t.addLabel("jump_1");
+ *     ...
+ *     t.addLabel("after_parsing");
+ *     ...
+ *     t.addLabel("after_subroutine");
+ *     t.generate(gen);
+ *</pre>
+ * generates the following APF code:
+ * <pre>
+ *     :my_jump_table
+ *     ldm r0, 7
+ *     jeq r0, 0, jump_1
+ *     jeq r0, 1, after_parsing
+ *     jeq r0, 2, after_subroutine
+ *     jmp DROP
+ * </pre>
+ */
+public class JumpTable {
+    /** Maps jump indices to jump labels. LinkedHashMap guarantees iteration in insertion order. */
+    private final Map<String, Integer> mJumpLabels = new LinkedHashMap<>();
+    /** Label to jump to to execute this jump table. */
+    private final String mStartLabel;
+    /** Memory slot that contains the return value index. */
+    private final int mReturnAddressMemorySlot;
+
+    private int mIndex = 0;
+
+    public JumpTable(@NonNull String startLabel, int returnAddressMemorySlot) {
+        Objects.requireNonNull(startLabel);
+        mStartLabel = startLabel;
+        if (returnAddressMemorySlot < 0
+                || returnAddressMemorySlot >= ApfGenerator.FIRST_PREFILLED_MEMORY_SLOT) {
+            throw new IllegalArgumentException("Invalid memory slot " + returnAddressMemorySlot);
+        }
+        mReturnAddressMemorySlot = returnAddressMemorySlot;
+    }
+
+    /** Returns the label to jump to to start executing the table. */
+    @NonNull
+    public String getStartLabel() {
+        return mStartLabel;
+    }
+
+    /**
+     * Adds a jump label to this table. Passing a label that was already added is not an error.
+     *
+     * @param label the label to add
+     */
+    public void addLabel(@NonNull String label) {
+        Objects.requireNonNull(label);
+        if (mJumpLabels.putIfAbsent(label, mIndex) == null) mIndex++;
+    }
+
+    /**
+     * Gets the index of a previously-added label.
+     * @return the label's index.
+     * @throws NoSuchElementException if the label was never added.
+     */
+    public int getIndex(@NonNull String label) {
+        final Integer index = mJumpLabels.get(label);
+        if (index == null) throw new NoSuchElementException("Unknown label " + label);
+        return index;
+    }
+
+    /** Generates APF code for this jump table */
+    public void generate(@NonNull ApfGenerator gen)
+            throws ApfGenerator.IllegalInstructionException {
+        gen.defineLabel(mStartLabel);
+        gen.addLoadFromMemory(R0, mReturnAddressMemorySlot);
+        for (Map.Entry<String, Integer> e : mJumpLabels.entrySet()) {
+            gen.addJumpIfR0Equals(e.getValue(), e.getKey());
+        }
+        // Cannot happen unless the program is malformed (i.e., the APF code loads an invalid return
+        // label index before jumping to the subroutine.
+        gen.addJump(ApfGenerator.PASS_LABEL);
+    }
+}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 8f59839..7ffd500 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -52,7 +52,6 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_ANNOUNCE_NUM;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_PROBE_NUM;
 import static com.android.net.module.util.SocketUtils.closeSocketQuietly;
-import static com.android.networkstack.util.NetworkStackUtils.DHCP_DISABLE_DROP_MF;
 import static com.android.networkstack.util.NetworkStackUtils.DHCP_INIT_REBOOT_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION;
@@ -693,11 +692,9 @@
 
         @Override
         protected FileDescriptor createFd() {
-            boolean dropMF = !DeviceConfigUtils.getDeviceConfigPropertyBoolean(
-                    NAMESPACE_CONNECTIVITY, DHCP_DISABLE_DROP_MF, false);
             try {
                 mPacketSock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
-                NetworkStackUtils.attachDhcpFilter(mPacketSock, dropMF);
+                NetworkStackUtils.attachDhcpFilter(mPacketSock);
                 final SocketAddress addr = makePacketSocketAddress(ETH_P_IP, mIface.index);
                 Os.bind(mPacketSock, addr);
             } catch (SocketException | ErrnoException e) {
diff --git a/src/android/net/dhcp6/Dhcp6AdvertisePacket.java b/src/android/net/dhcp6/Dhcp6AdvertisePacket.java
new file mode 100644
index 0000000..3eb5503
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6AdvertisePacket.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 ADVERTISE packet class, a server sends an Advertise message to indicate that it's
+ * available for DHCP service, in response to a Solicit message received from a client.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6AdvertisePacket extends Dhcp6Packet {
+    /**
+     * Generates an advertise packet with the specified parameters.
+     */
+    Dhcp6AdvertisePacket(int transId, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
+        super(transId, (short) 0 /* secs */, clientDuid, serverDuid, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Advertise message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_ADVERTISE << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid);
+        addTlv(packet, DHCP6_SERVER_IDENTIFIER, mServerDuid);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
new file mode 100644
index 0000000..3fa7cb0
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+
+import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS;
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_PORT;
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+
+import android.content.Context;
+import android.net.ip.IpClient;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * A DHCPv6 client.
+ *
+ * So far only support IA_PD (prefix delegation), not for IA_NA/IA_TA yet.
+ *
+ * @hide
+ */
+public class Dhcp6Client extends StateMachine {
+    private static final String TAG = Dhcp6Client.class.getSimpleName();
+    private static final boolean DBG = true;
+
+    // Internal messages.
+    // Dhcp6Client shares the same handler with IpClient, define the base command range for
+    // both public and private messages used in Dhcp6Client, to avoid commands overlap.
+    private static final int PRIVATE_BASE         = IpClient.DHCP6CLIENT_CMD_BASE + 100;
+    private static final int CMD_RECEIVED_PACKET  = PRIVATE_BASE + 1;
+
+    @NonNull private final Context mContext;
+
+    // State variables.
+    @NonNull private final StateMachine mController;
+    @NonNull private final String mIfaceName;
+
+    private Dhcp6Client(@NonNull final Context context, @NonNull final StateMachine controller,
+            @NonNull final InterfaceParams iface) {
+        super(TAG, controller.getHandler());
+
+        mContext = context;
+        mController = controller;
+        mIfaceName = iface.name;
+
+        // TODO: add state machine initialization.
+    }
+
+    private class Dhcp6PacketHandler extends PacketReader {
+        private FileDescriptor mUdpSock;
+
+        Dhcp6PacketHandler(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            try {
+                final Dhcp6Packet packet = Dhcp6Packet.decodePacket(recvbuf, length);
+                if (DBG) Log.d(TAG, "Received packet: " + packet);
+                sendMessage(CMD_RECEIVED_PACKET, packet);
+            } catch (Dhcp6Packet.ParseException e) {
+                Log.e(TAG, "Can't parse DHCPv6 packet: " + e.getMessage());
+            }
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            try {
+                mUdpSock = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
+                SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
+                Os.bind(mUdpSock, IPV6_ADDR_ANY, DHCP6_CLIENT_PORT);
+            } catch (SocketException | ErrnoException e) {
+                Log.e(TAG, "Error creating udp socket", e);
+                closeFd(mUdpSock);
+                mUdpSock = null;
+                return null;
+            }
+            return mUdpSock;
+        }
+
+        @Override
+        protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
+            try {
+                return Os.read(fd, packetBuffer, 0, packetBuffer.length);
+            } catch (IOException | ErrnoException e) {
+                Log.e(TAG, "Fail to read packet");
+                throw e;
+            }
+        }
+
+        public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException {
+            int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */,
+                    buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS,
+                    DHCP6_SERVER_PORT);
+            return ret;
+        }
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
new file mode 100644
index 0000000..6d77d4e
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_OPTION_LEN;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.IaPrefixOption;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Defines basic data and operations needed to build and use packets for the
+ * DHCPv6 protocol. Subclasses create the specific packets used at each
+ * stage of the negotiation.
+ *
+ * @hide
+ */
+public class Dhcp6Packet {
+
+    /**
+     * DHCPv6 Message Type.
+     */
+    public static final byte DHCP6_MESSAGE_TYPE_SOLICIT = 1;
+    public static final byte DHCP6_MESSAGE_TYPE_ADVERTISE = 2;
+    public static final byte DHCP6_MESSAGE_TYPE_REQUEST = 3;
+    public static final byte DHCP6_MESSAGE_TYPE_CONFIRM = 4;
+    public static final byte DHCP6_MESSAGE_TYPE_RENEW = 5;
+    public static final byte DHCP6_MESSAGE_TYPE_REBIND = 6;
+    public static final byte DHCP6_MESSAGE_TYPE_REPLY = 7;
+    public static final byte DHCP6_MESSAGE_TYPE_RELEASE = 8;
+    public static final byte DHCP6_MESSAGE_TYPE_DECLINE = 9;
+    public static final byte DHCP6_MESSAGE_TYPE_RECONFIGURE = 10;
+    public static final byte DHCP6_MESSAGE_TYPE_INFORMATION_REQUEST = 11;
+    public static final byte DHCP6_MESSAGE_TYPE_RELAY_FORW = 12;
+    public static final byte DHCP6_MESSAGE_TYPE_RELAY_REPL = 13;
+
+    /**
+     * DHCPv6 Optional Type: Client Identifier.
+     * DHCPv6 message from client must have this option.
+     */
+    public static final byte DHCP6_CLIENT_IDENTIFIER = 1;
+    @NonNull
+    protected final byte[] mClientDuid;
+
+    /**
+     * DHCPv6 Optional Type: Server Identifier.
+     */
+    public static final byte DHCP6_SERVER_IDENTIFIER = 2;
+    protected final byte[] mServerDuid;
+
+    /**
+     * DHCPv6 Optional Type: Elapsed time.
+     */
+    public static final byte DHCP6_ELAPSED_TIME = 8;
+    protected final short mSecs;
+
+    /**
+     * DHCPv6 Optional Type: Status Code.
+     */
+    public static final byte DHCP6_STATUS_CODE = 13;
+    protected short mStatusCode;
+    protected String mStatusMsg;
+
+    public static final short STATUS_SUCCESS           = 0;
+    public static final short STATUS_UNSPEC_FAIL       = 1;
+    public static final short STATUS_NO_ADDR_AVAI      = 2;
+    public static final short STATUS_NO_BINDING        = 3;
+    public static final short STATUS_PREFIX_NOT_ONLINK = 4;
+    public static final short STATUS_USE_MULTICAST     = 5;
+    public static final short STATUS_NO_PREFIX_AVAI    = 6;
+
+    /**
+     * DHCPv6 Optional Type: IA_PD.
+     */
+    public static final byte DHCP6_IA_PD = 25;
+    @NonNull
+    protected final byte[] mIaPd;
+    @NonNull
+    protected PrefixDelegation mPrefixDelegation;
+
+    /**
+     * The transaction identifier used in this particular DHCPv6 negotiation
+     */
+    protected final int mTransId;
+
+    /**
+     * The unique identifier for IA_NA, IA_TA, IA_PD used in this particular DHCPv6 negotiation
+     */
+    protected int mIaId;
+
+    Dhcp6Packet(int transId, short secs, @NonNull final byte[] clientDuid, final byte[] serverDuid,
+            @NonNull final byte[] iapd) {
+        mTransId = transId;
+        mSecs = secs;
+        mClientDuid = clientDuid;
+        mServerDuid = serverDuid;
+        mIaPd = iapd;
+    }
+
+    /**
+     * Returns the transaction ID.
+     */
+    public int getTransactionId() {
+        return mTransId;
+    }
+
+    /**
+     * Returns IA_ID associated to IA_PD.
+     */
+    public int getIaId() {
+        return mIaId;
+    }
+
+    /**
+     * Returns the client's DUID.
+     */
+    @NonNull
+    public byte[] getClientDuid() {
+        return mClientDuid;
+    }
+
+    /**
+     * Returns the server's DUID.
+     */
+    public byte[] getServerDuid() {
+        return mServerDuid;
+    }
+
+    /**
+     * A class to take DHCPv6 IA_PD option allocated from server.
+     * https://www.rfc-editor.org/rfc/rfc8415.html#section-21.21
+     */
+    public static class PrefixDelegation {
+        public int iaid;
+        public int t1;
+        public int t2;
+        public final IaPrefixOption ipo;
+
+        PrefixDelegation(int iaid, int t1, int t2, final IaPrefixOption ipo) {
+            this.iaid = iaid;
+            this.t1 = t1;
+            this.t2 = t2;
+            this.ipo = ipo;
+        }
+
+        @Override
+        public String toString() {
+            return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
+                    + ", prefix " + ipo;
+        }
+    }
+
+    /**
+     * DHCPv6 packet parsing exception.
+     */
+    public static class ParseException extends Exception {
+        ParseException(String msg) {
+            super(msg);
+        }
+    }
+
+    private static void skipOption(@NonNull final ByteBuffer packet, int optionLen)
+            throws BufferUnderflowException {
+        for (int i = 0; i < optionLen; i++) {
+            packet.get();
+        }
+    }
+
+    /**
+     * Reads a string of specified length from the buffer.
+     *
+     * TODO: move to a common place which can be shared with DhcpClient.
+     */
+    private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount,
+            boolean isNullOk) {
+        final byte[] bytes = new byte[byteCount];
+        buf.get(bytes);
+        return readAsciiString(bytes, isNullOk);
+    }
+
+    private static String readAsciiString(@NonNull final byte[] payload, boolean isNullOk) {
+        final byte[] bytes = payload;
+        int length = bytes.length;
+        if (!isNullOk) {
+            // Stop at the first null byte. This is because some DHCP options (e.g., the domain
+            // name) are passed to netd via FrameworkListener, which refuses arguments containing
+            // null bytes. We don't do this by default because vendorInfo is an opaque string which
+            // could in theory contain null bytes.
+            for (length = 0; length < bytes.length; length++) {
+                if (bytes[length] == 0) {
+                    break;
+                }
+            }
+        }
+        return new String(bytes, 0, length, StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * Creates a concrete Dhcp6Packet from the supplied ByteBuffer.
+     *
+     * The buffer only starts with a UDP encapsulation (i.e. DHCPv6 message). A subset of the
+     * optional parameters are parsed and are stored in object fields. Client/Server message
+     * format:
+     *
+     *  0                   1                   2                   3
+     *  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
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |    msg-type   |               transaction-id                  |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                                                               |
+     * .                            options                            .
+     * .                 (variable number and length)                  .
+     * |                                                               |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     */
+    @VisibleForTesting
+    static Dhcp6Packet decodePacket(@NonNull final ByteBuffer packet) throws ParseException {
+        short secs = 0;
+        byte[] iapd = null;
+        byte[] serverDuid = null;
+        byte[] clientDuid = null;
+        short statusCode = STATUS_SUCCESS;
+        String statusMsg = null;
+
+        packet.order(ByteOrder.BIG_ENDIAN);
+
+        // DHCPv6 message contents.
+        final int msgTypeAndTransId = packet.getInt();
+        final byte messageType = (byte) (msgTypeAndTransId >> 24);
+        final int transId = msgTypeAndTransId & 0x0FFF;
+
+        /**
+         * Parse DHCPv6 options, option format:
+         *
+         * 0                   1                   2                   3
+         * 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
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         * |          option-code          |           option-len          |
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         * |                          option-data                          |
+         * |                      (option-len octets)                      |
+         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         */
+        while (packet.hasRemaining()) {
+            try {
+                final short optionType = packet.getShort();
+                final int optionLen = packet.getShort() & 0xFFFF;
+                int expectedLen = 0;
+
+                switch(optionType) {
+                    case DHCP6_SERVER_IDENTIFIER:
+                        expectedLen = optionLen;
+                        final byte[] sduid = new byte[expectedLen];
+                        packet.get(sduid, 0 /* offset */, expectedLen);
+                        serverDuid = sduid;
+                        break;
+                    case DHCP6_CLIENT_IDENTIFIER:
+                        expectedLen = optionLen;
+                        final byte[] cduid = new byte[expectedLen];
+                        packet.get(cduid, 0 /* offset */, expectedLen);
+                        clientDuid = cduid;
+                        break;
+                    case DHCP6_IA_PD:
+                        expectedLen = optionLen;
+                        final byte[] bytes = new byte[expectedLen];
+                        packet.get(bytes, 0 /* offset */, expectedLen);
+                        iapd = bytes;
+                        break;
+                    case DHCP6_ELAPSED_TIME:
+                        expectedLen = 2;
+                        secs = packet.getShort();
+                        break;
+                    case DHCP6_STATUS_CODE:
+                        expectedLen = optionLen;
+                        statusCode = packet.getShort();
+                        statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
+                        break;
+                    default:
+                        expectedLen = optionLen;
+                        // BufferUnderflowException will be thrown if option is truncated.
+                        skipOption(packet, optionLen);
+                        break;
+                }
+                if (expectedLen != optionLen) {
+                    throw new ParseException(
+                            "Invalid length " + optionLen + " for option " + optionType
+                                    + ", expected " + expectedLen);
+                }
+            } catch (BufferUnderflowException e) {
+                throw new ParseException(e.getMessage());
+            }
+        }
+
+        Dhcp6Packet newPacket;
+
+        switch(messageType) {
+            case DHCP6_MESSAGE_TYPE_SOLICIT:
+                newPacket = new Dhcp6SolicitPacket(transId, secs, clientDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_ADVERTISE:
+                newPacket = new Dhcp6AdvertisePacket(transId, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REQUEST:
+                newPacket = new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REPLY:
+                newPacket = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_RENEW:
+                newPacket = new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REBIND:
+                newPacket = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd);
+                break;
+            default:
+                throw new ParseException("Unimplemented DHCP6 message type %d" + messageType);
+        }
+
+        if (iapd != null) {
+            final ByteBuffer buffer = ByteBuffer.wrap(iapd);
+            final int iaid = buffer.getInt();
+            final int t1 = buffer.getInt();
+            final int t2 = buffer.getInt();
+            final IaPrefixOption ipo = Struct.parse(IaPrefixOption.class, buffer);
+            newPacket.mPrefixDelegation = new PrefixDelegation(iaid, t1, t2, ipo);
+            newPacket.mIaId = iaid;
+        }
+        newPacket.mStatusCode = statusCode;
+        newPacket.mStatusMsg = statusMsg;
+
+        return newPacket;
+    }
+
+    /**
+     * Parse a packet from an array of bytes, stopping at the given length.
+     */
+    public static Dhcp6Packet decodePacket(@NonNull final byte[] packet, int length)
+            throws ParseException {
+        final ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN);
+        return decodePacket(buffer);
+    }
+
+    /**
+     * Adds an optional parameter containing an array of bytes.
+     */
+    protected static void addTlv(ByteBuffer buf, short type, @NonNull byte[] payload) {
+        if (payload.length > DHCP_MAX_OPTION_LEN) {
+            throw new IllegalArgumentException("DHCP option too long: "
+                    + payload.length + " vs. " + DHCP_MAX_OPTION_LEN);
+        }
+        buf.putShort(type);
+        buf.putShort((short) payload.length);
+        buf.put(payload);
+    }
+
+    /**
+     * Adds an optional parameter containing a short integer.
+     */
+    protected static void addTlv(ByteBuffer buf, short type, short value) {
+        buf.putShort(type);
+        buf.putShort((short) 2);
+        buf.putShort(value);
+    }
+
+    /**
+     * Builds a DHCPv6 SOLICIT packet from the required specified parameters.
+     */
+    public static ByteBuffer buildSolicitPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid) {
+        final Dhcp6SolicitPacket pkt = new Dhcp6SolicitPacket(transId, secs, clientDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 ADVERTISE packet from the required specified parameters.
+     */
+    public static ByteBuffer buildAdvertisePacket(int transId, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
+        final Dhcp6AdvertisePacket pkt =
+                new Dhcp6AdvertisePacket(transId, clientDuid, serverDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 REPLY packet from the required specified parameters.
+     */
+    public static ByteBuffer buildReplyPacket(int transId, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
+        final Dhcp6ReplyPacket pkt = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 REQUEST packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRequestPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
+        final Dhcp6RequestPacket pkt =
+                new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 RENEW packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRenewPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
+        final Dhcp6RenewPacket pkt =
+                new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 REBIND packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRebindPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid) {
+        final Dhcp6RebindPacket pkt = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd);
+        return pkt.buildPacket();
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6RebindPacket.java b/src/android/net/dhcp6/Dhcp6RebindPacket.java
new file mode 100644
index 0000000..10c10b9
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6RebindPacket.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 REBIND packet class, a client sends a Rebind message to any available server to extend
+ * the lifetimes on the leases assigned to the client and to update other configuration parameters.
+ * This message is sent after a client receives no response to a Renew message.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6RebindPacket extends Dhcp6Packet {
+    /**
+     * Generates a rebind packet with the specified parameters.
+     */
+    Dhcp6RebindPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] iapd) {
+        super(transId, secs, clientDuid, null /* serverDuid */, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Rebind message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_REBIND << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid());
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6RenewPacket.java b/src/android/net/dhcp6/Dhcp6RenewPacket.java
new file mode 100644
index 0000000..8715b59
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6RenewPacket.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 RENEW packet class, a client sends an Renew message to the server that originally
+ * provided the client's leases and configuration parameters to extend the lifetimes on the
+ * leases assigned to the client and to update other configuration parameters.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6RenewPacket extends Dhcp6Packet {
+    /**
+     * Generates a renew packet with the specified parameters.
+     */
+    Dhcp6RenewPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
+        super(transId, secs, clientDuid, serverDuid, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Renew message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_RENEW << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_SERVER_IDENTIFIER, getServerDuid());
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid());
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6ReplyPacket.java b/src/android/net/dhcp6/Dhcp6ReplyPacket.java
new file mode 100644
index 0000000..4b37478
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6ReplyPacket.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 REPLY packet class, a server sends an Reply message containing assigned leases
+ * and configuration parameters in response to a Solicit, Request, Renew or Rebind messages
+ * received from a client.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6ReplyPacket extends Dhcp6Packet {
+    /**
+     * Generates a reply packet with the specified parameters.
+     */
+    Dhcp6ReplyPacket(int transId, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
+        super(transId, (short) 0 /* secs */, clientDuid, serverDuid, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Reply message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_REPLY << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid);
+        addTlv(packet, DHCP6_SERVER_IDENTIFIER, mServerDuid);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6RequestPacket.java b/src/android/net/dhcp6/Dhcp6RequestPacket.java
new file mode 100644
index 0000000..f2f398d
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6RequestPacket.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 REQUEST packet class, a client sends a Request message to request configuration
+ * parameters, including addresses and/or delegated prefixes from a specific server.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6RequestPacket extends Dhcp6Packet {
+    /**
+     * Generates a request packet with the specified parameters.
+     */
+    Dhcp6RequestPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
+        super(transId, secs, clientDuid, serverDuid, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Request message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_REQUEST << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_SERVER_IDENTIFIER, getServerDuid());
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid());
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6SolicitPacket.java b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
new file mode 100644
index 0000000..909ff2c
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 SOLICIT packet class, a client sends a Solicit message to locate DHCPv6 servers.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6SolicitPacket extends Dhcp6Packet {
+    /**
+     * Generates a solicit packet with the specified parameters.
+     */
+    Dhcp6SolicitPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            final byte[] iapd) {
+        super(transId, secs, clientDuid, null /* serverDuid */, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Solicit message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_SOLICIT << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index fa03e08..5737d87 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -21,6 +21,7 @@
 import static android.net.ip.IIpClient.PROV_IPV4_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_LINKLOCAL;
+import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
 import static android.net.ip.IpReachabilityMonitor.INVALID_REACHABILITY_LOSS_TYPE;
 import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
@@ -104,6 +105,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SharedLog;
@@ -432,6 +434,18 @@
         }
 
         /**
+         * Set maximum acceptable DTIM multiplier to hardware driver.
+         */
+        public void setMaxDtimMultiplier(int multiplier) {
+            log("setMaxDtimMultiplier(" + multiplier + ")");
+            try {
+                mCallback.setMaxDtimMultiplier(multiplier);
+            } catch (RemoteException e) {
+                log("Failed to call setMaxDtimMultiplier", e);
+            }
+        }
+
+        /**
          * Get the version of the IIpClientCallbacks AIDL interface.
          */
         public int getInterfaceVersion() {
@@ -471,6 +485,7 @@
     private static final int CMD_UPDATE_L2KEY_CLUSTER = 15;
     private static final int CMD_COMPLETE_PRECONNECTION = 16;
     private static final int CMD_UPDATE_L2INFORMATION = 17;
+    private static final int CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY = 18;
 
     private static final int ARG_LINKPROP_CHANGED_LINKSTATE_DOWN = 0;
     private static final int ARG_LINKPROP_CHANGED_LINKSTATE_UP = 1;
@@ -485,6 +500,9 @@
     // IpClient shares a handler with DhcpClient: commands must not overlap
     public static final int DHCPCLIENT_CMD_BASE = 1000;
 
+    // IpClient shares a handler with Dhcp6Client: commands must not overlap
+    public static final int DHCP6CLIENT_CMD_BASE = 2000;
+
     // Settings and default values.
     private static final int MAX_LOG_RECORDS = 500;
     private static final int MAX_PACKET_RECORDS = 100;
@@ -494,6 +512,47 @@
     private static final int DEFAULT_MIN_RDNSS_LIFETIME =
             ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q) ? 120 : 0;
 
+    // Used to wait for the provisioning to complete eventually and then decide the target
+    // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6
+    // provisioning connection latency metrics, the latency of 95% can go up to 16s, so pick
+    // ProvisioningConfiguration.DEFAULT_TIMEOUT_MS value for this delay.
+    @VisibleForTesting
+    static final String CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS =
+            "ipclient_initial_provisioning_dtim_delay";
+    private static final int DEFAULT_INITIAL_PROVISIONING_DTIM_DELAY_MS = 18000;
+
+    @VisibleForTesting
+    static final String CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER =
+            "ipclient_multicast_lock_max_dtim_multiplier";
+    @VisibleForTesting
+    static final int DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER = 1;
+
+    @VisibleForTesting
+    static final String CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER =
+            "ipclient_ipv6_only_max_dtim_multiplier";
+    @VisibleForTesting
+    static final int DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER = 2;
+
+    @VisibleForTesting
+    static final String CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER =
+            "ipclient_ipv4_only_max_dtim_multiplier";
+    @VisibleForTesting
+    static final int DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER = 9;
+
+    @VisibleForTesting
+    static final String CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER =
+            "ipclient_dual_stack_max_dtim_multiplier";
+    // The default value for dual-stack networks is the min of maximum DTIM multiplier to use for
+    // IPv4-only and IPv6-only networks.
+    @VisibleForTesting
+    static final int DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER = 2;
+
+    @VisibleForTesting
+    static final String CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER =
+            "ipclient_before_ipv6_prov_max_dtim_multiplier";
+    @VisibleForTesting
+    static final int DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER = 1;
+
     private static final boolean NO_CALLBACKS = false;
     private static final boolean SEND_CALLBACKS = true;
 
@@ -583,9 +642,11 @@
     private String mCluster; // The cluster for this network, for writing into the memory store
     private boolean mMulticastFiltering;
     private long mStartTimeMillis;
+    private long mInitialProvisioningEndTimeMillis;
     private MacAddress mCurrentBssid;
     private boolean mHasDisabledIpv6OrAcceptRaOnProvLoss;
     private Integer mDadTransmits = null;
+    private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
 
     /**
      * Reading the snapshot is an asynchronous operation initiated by invoking
@@ -1805,6 +1866,10 @@
         maybeSaveNetworkToIpMemoryStore();
         if (sendCallbacks) {
             dispatchCallback(delta, newLp);
+            // We cannot do this along with onProvisioningSuccess callback, because the network
+            // can become dual-stack after a success IPv6 provisioning, and the multiplier also
+            // needs to be updated upon the loss of IPv4 and/or IPv6 provisioning.
+            updateMaxDtimMultiplier();
         }
         return (delta != PROV_CHANGE_LOST_PROVISIONING);
     }
@@ -2196,6 +2261,12 @@
             maybeRestoreInterfaceMtu();
             // Reset number of dad_transmits to default value if changed.
             maybeRestoreDadTransmits();
+            // Reset DTIM multiplier to default value if changed.
+            if (mMaxDtimMultiplier != DTIM_MULTIPLIER_RESET) {
+                mCallback.setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
+                mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
+                mInitialProvisioningEndTimeMillis = 0;
+            }
         }
 
         @Override
@@ -2374,11 +2445,19 @@
         public void enter() {
             mIpProvisioningMetrics.reset();
             mStartTimeMillis = SystemClock.elapsedRealtime();
+            final int delay = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS,
+                    DEFAULT_INITIAL_PROVISIONING_DTIM_DELAY_MS);
+            mInitialProvisioningEndTimeMillis = mStartTimeMillis + delay;
+
             if (mConfiguration.mProvisioningTimeoutMs > 0) {
                 final long alarmTime = SystemClock.elapsedRealtime()
                         + mConfiguration.mProvisioningTimeoutMs;
                 mProvisioningTimeoutAlarm.schedule(alarmTime);
             }
+            // Send a delay message to wait for IP provisioning to complete eventually and set the
+            // specific DTIM multiplier by checking the target network type.
+            sendMessageDelayed(CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY, delay);
         }
 
         @Override
@@ -2615,6 +2694,7 @@
                     } else {
                         mCallback.setFallbackMulticastFilter(mMulticastFiltering);
                     }
+                    updateMaxDtimMultiplier();
                     break;
                 }
 
@@ -2733,6 +2813,10 @@
                     mDhcpClient = null;
                     break;
 
+                case CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY:
+                    updateMaxDtimMultiplier();
+                    break;
+
                 default:
                     return NOT_HANDLED;
             }
@@ -2742,6 +2826,71 @@
         }
     }
 
+    /**
+     * Set the maximum DTIM multiplier to hardware driver per network condition. Any multiplier
+     * larger than the maximum value must not be accepted, it will cause packet loss higher than
+     * what the system can accept, which will cause unexpected behavior for apps, and may interrupt
+     * the network connection.
+     *
+     * When Wifi STA is in the power saving mode and the system is suspended, the wakeup interval
+     * will be set to:
+     *    1) multiplier * AP's DTIM period if multiplier > 0.
+     *    2) the driver default value if multiplier <= 0.
+     * Some implementations may apply an additional cap to wakeup interval in the case of 1).
+     */
+    private void updateMaxDtimMultiplier() {
+        int multiplier = deriveDtimMultiplier();
+        if (mMaxDtimMultiplier == multiplier) return;
+
+        mMaxDtimMultiplier = multiplier;
+        log("set max DTIM multiplier to " + multiplier);
+        mCallback.setMaxDtimMultiplier(multiplier);
+    }
+
+    private int deriveDtimMultiplier() {
+        final boolean hasIpv4Addr = mLinkProperties.hasIpv4Address();
+        // For a host in the network that has only ULA and link-local but no GUA, consider
+        // that it also has IPv6 connectivity. LinkProperties#isIpv6Provisioned only returns
+        // true when it has a GUA, so we cannot use it for IPv6-only network case.
+        final boolean hasIpv6Addr = CollectionUtils.any(mLinkProperties.getLinkAddresses(),
+                la -> {
+                    final InetAddress address = la.getAddress();
+                    return (address instanceof Inet6Address) && !address.isLinkLocalAddress();
+                });
+
+        final int multiplier;
+        if (!mMulticastFiltering) {
+            multiplier = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER,
+                    DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
+        } else if (!hasIpv6Addr
+                && (SystemClock.elapsedRealtime() < mInitialProvisioningEndTimeMillis)) {
+            // IPv6 provisioning may or may not complete soon in the future, we don't know when
+            // it will complete, however, setting multiplier to a high value will cause higher
+            // RA packet loss, that increases the overall IPv6 provisioning latency. So just set
+            // multiplier to 1 before device gains the IPv6 provisioning, make sure device won't
+            // miss any RA packet later.
+            multiplier = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER,
+                    DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+        } else if (hasIpv6Addr && !hasIpv4Addr) {
+            multiplier = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
+                    DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+        } else if (hasIpv4Addr && !hasIpv6Addr) {
+            multiplier = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
+                    DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+        } else if (hasIpv6Addr && hasIpv4Addr) {
+            multiplier = mDependencies.getDeviceConfigPropertyInt(
+                    CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER,
+                    DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
+        } else {
+            multiplier = DTIM_MULTIPLIER_RESET;
+        }
+        return multiplier;
+    }
+
     private static class MessageHandlingLogger {
         public String processedInState;
         public String receivedInState;
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 0771bff..f1fb3da 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -46,6 +46,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.HexDump;
+import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.NetlinkMonitor;
@@ -539,7 +540,8 @@
             if (!mNetlinkEventParsingEnabled) return;
             final String[] addresses = new String[opt.servers.length];
             for (int i = 0; i < opt.servers.length; i++) {
-                addresses[i] = opt.servers[i].getHostAddress();
+                final Inet6Address addr = opt.servers[i];
+                addresses[i] = InetAddressUtils.withScopeId(addr, mIfindex).getHostAddress();
             }
             updateInterfaceDnsServerInfo(opt.header.lifetime, addresses);
         }
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index b7cb9ce..ff6d65e 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -259,7 +259,7 @@
         mCm = context.getSystemService(ConnectivityManager.class);
         mDependencies = dependencies;
         mMulticastResolicitEnabled = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_MCAST_RESOLICIT_VERSION, false /* defaultEnabled */);
+                IP_REACHABILITY_MCAST_RESOLICIT_VERSION, true /* defaultEnabled */);
         mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureEnabled(context,
                 IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION,
                 false /* defaultEnabled */);
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 5c072de..147c33a 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -17,6 +17,7 @@
 package com.android.networkstack.util;
 
 import android.content.Context;
+import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.MacAddress;
 import android.system.ErrnoException;
@@ -26,6 +27,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HexDump;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -165,11 +167,6 @@
     public static final String DHCP_RAPID_COMMIT_ENABLED = "dhcp_rapid_commit_enabled";
 
     /**
-     * Disable dropping DHCP packets with IPv4 MF flag set.
-     */
-    public static final String DHCP_DISABLE_DROP_MF = "dhcp_disable_drop_mf";
-
-    /**
      * Minimum module version at which to enable the DHCP INIT-REBOOT state.
      */
     public static final String DHCP_INIT_REBOOT_VERSION = "dhcp_init_reboot_version";
@@ -326,10 +323,50 @@
     }
 
     /**
+     * Convert 48bits MAC address to 64bits link-layer address(EUI64).
+     *     1. insert the 0xFFFE in the middle of mac address
+     *     2. flip the 7th bit(universal/local) of the first byte.
+     */
+    public static byte[] macAddressToEui64(@NonNull final MacAddress hwAddr) {
+        final byte[] eui64 = new byte[8];
+        final byte[] mac48 = hwAddr.toByteArray();
+        System.arraycopy(mac48 /* src */, 0 /* srcPos */, eui64 /* dest */, 0 /* destPos */,
+                3 /* length */);
+        eui64[3] = (byte) 0xFF;
+        eui64[4] = (byte) 0xFE;
+        System.arraycopy(mac48 /* src */, 3 /* srcPos */, eui64 /* dest */, 5 /* destPos */,
+                3 /* length */);
+        eui64[0] = (byte) (eui64[0] ^ 0x02); // flip 7th bit
+        return eui64;
+    }
+
+    /**
+     * Generate an IPv6 address based on the given prefix(/64) and stable interface
+     * identifier(EUI64).
+     */
+    public static Inet6Address createInet6AddressFromEui64(@NonNull final IpPrefix prefix,
+            @NonNull final byte[] eui64) {
+        if (prefix.getPrefixLength() != 64) {
+            Log.e(TAG, "Invalid IPv6 prefix length " + prefix.getPrefixLength());
+            return null;
+        }
+        final byte[] address = new byte[16];
+        System.arraycopy(prefix.getRawAddress() /* src */, 0 /* srcPos */, address /* dest */,
+                0 /* destPos*/, 8 /* length */);
+        System.arraycopy(eui64 /* src */, 0 /* srcPos */, address /* dest */, 8 /* destPos */,
+                eui64.length);
+        try {
+            return (Inet6Address) InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            Log.e(TAG, "Invalid IPv6 address " + HexDump.toHexString(address), e);
+            return null;
+        }
+    }
+
+    /**
      * Attaches a socket filter that accepts DHCP packets to the given socket.
      */
-    public static native void attachDhcpFilter(FileDescriptor fd, boolean dropMF)
-            throws ErrnoException;
+    public static native void attachDhcpFilter(FileDescriptor fd) throws ErrnoException;
 
     /**
      * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket.
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index bfd3735..85c971a 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -23,7 +23,10 @@
 
          05-14 00:41:02.723 18330 18330 E AndroidRuntime: java.lang.IllegalStateException: Signature|privileged permissions not in privapp-permissions whitelist: {com.android.server.networkstack.integrationtests: android.permission.CONNECTIVITY_INTERNAL}
     -->
-
+    <!-- Used by creating test network -->
+    <uses-permission android:name="android.permission.MANAGE_TEST_NETWORKS" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index e36af57..2fb5ac8 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -35,6 +35,7 @@
 import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
 import static android.net.dhcp.DhcpPacket.MIN_V6ONLY_WAIT_MS;
 import static android.net.dhcp.DhcpResultsParcelableUtil.fromStableParcelable;
+import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
 import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX;
 import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
 import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM;
@@ -86,6 +87,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.contains;
 import static org.mockito.ArgumentMatchers.longThat;
+import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.atLeastOnce;
@@ -566,6 +568,8 @@
 
     protected abstract void setFeatureEnabled(String name, boolean enabled);
 
+    protected abstract void setDeviceConfigProperty(String name, int value);
+
     protected abstract boolean isFeatureEnabled(String name, boolean defaultEnabled);
 
     protected abstract boolean useNetworkStackSignature();
@@ -598,6 +602,21 @@
                 isIPv6OnlyPreferredEnabled);
     }
 
+    private void setDeviceConfigForMaxDtimMultiplier() {
+        setDeviceConfigProperty(IpClient.CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS,
+                500 /* default value */);
+        setDeviceConfigProperty(IpClient.CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER,
+                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
+        setDeviceConfigProperty(IpClient.CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
+                IpClient.DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+        setDeviceConfigProperty(IpClient.CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
+                IpClient.DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+        setDeviceConfigProperty(IpClient.CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER,
+                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
+        setDeviceConfigProperty(IpClient.CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER,
+                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+    }
+
     @Before
     public void setUp() throws Exception {
         // Suffix "[0]" or "[1]" is added to the end of test method name after running with
@@ -640,6 +659,11 @@
         }
 
         mIIpClient = makeIIpClient(mIfaceName, mCb);
+
+        // Enable multicast filtering after creating IpClient instance, make the integration test
+        // more realistic.
+        mIIpClient.setMulticastFilter(true);
+        setDeviceConfigForMaxDtimMultiplier();
     }
 
     protected void setUpMocks() throws Exception {
@@ -660,6 +684,12 @@
         when(mNetworkStackServiceManager.getIpMemoryStoreService())
                 .thenReturn(mIpMemoryStoreService);
         when(mCb.getInterfaceVersion()).thenReturn(IpClient.VERSION_ADDED_REACHABILITY_FAILURE);
+        // This mock is required, otherwise, ignoreIPv6ProvisioningLoss variable is always true,
+        // and IpReachabilityMonitor#avoidingBadLinks() will always return false as well, that
+        // results in the target tested IPv6 off-link DNS server won't be removed from LP and
+        // notifyLost won't be invoked, or the wrong code path when receiving RA with 0 router
+        // liftime.
+        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
 
         mDependencies.setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67);
         mDependencies.setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10);
@@ -699,7 +729,14 @@
         final TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> {
             final TestNetworkManager tnm =
                     inst.getContext().getSystemService(TestNetworkManager.class);
-            return tnm.createTapInterface();
+            try {
+                return tnm.createTapInterface(true /* carrierUp */, true /* bringUp */,
+                        true /* disableIpv6ProvisioningDelay */);
+            } catch (NoSuchMethodError e) {
+                // createTapInterface(boolean, boolean, boolean) has been introduced since T,
+                // use the legancy API if the method is not found on previous platforms.
+                return tnm.createTapInterface();
+            }
         });
         mIfaceName = iface.getInterfaceName();
         mClientMac = getIfaceMacAddr(mIfaceName).toByteArray();
@@ -979,7 +1016,7 @@
 
         startIpClientProvisioning(prov.build());
         if (!isPreconnectionEnabled) {
-            verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+            verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
         }
         verify(mCb, never()).onProvisioningFailure(any());
     }
@@ -1287,7 +1324,7 @@
                 mDependencies.mDhcpClient.sendMessage(DhcpClient.CMD_TIMEOUT);
             }
         }
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
 
         final LinkAddress ipAddress = new LinkAddress(CLIENT_ADDR, PREFIX_LENGTH);
         verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetCfg(ifConfig.capture());
@@ -2009,6 +2046,33 @@
         reset(mCb);
     }
 
+    @Test
+    public void testRaRdnss_Ipv6LinkLocalDns() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
+        // put an IPv6 link-local DNS server
+        final ByteBuffer rdnss = buildRdnssOption(600, ROUTER_LINK_LOCAL.getHostAddress());
+        // put SLLA option to avoid address resolution for "fe80::1"
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+
+        waitForRouterSolicitation();
+        mPacketReader.sendResponse(ra);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        assertEquals(1, lp.getDnsServers().size());
+        assertEquals(ROUTER_LINK_LOCAL, (Inet6Address) lp.getDnsServers().get(0));
+        assertTrue(lp.isIpv6Provisioned());
+    }
+
     private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception {
         inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
                 argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));
@@ -2352,7 +2416,7 @@
         // Force IpClient transition to RunningState from PreconnectionState.
         mIIpClient.notifyPreconnectionComplete(false /* success */);
         HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
     }
 
     @Test
@@ -2852,13 +2916,22 @@
         final InOrder inOrder = inOrder(mCb);
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
 
-        doIpv6OnlyProvisioning(inOrder, ra);
-
-        // Start IPv4 provisioning and wait until entire provisioning completes.
+        // Start IPv4 provisioning first and wait IPv4 provisioning to succeed, and then start
+        // IPv6 provisioning, which is more realistic and avoid the flaky case of both IPv4 and
+        // IPv6 provisioning complete at the same time.
         handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                 true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
+
+        waitForRouterSolicitation();
+        mPacketReader.sendResponse(ra);
+
+        // Wait until we see both success IPv4 and IPv6 provisioning, then there would be 4
+        // addresses in LinkProperties, they are IPv4 address, IPv6 link-local address, stable
+        // privacy address and privacy address.
         verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(x -> {
             if (!x.isIpv4Provisioned() || !x.isIpv6Provisioned()) return false;
+            if (x.getLinkAddresses().size() != 4) return false;
             lpFuture.complete(x);
             return true;
         }));
@@ -2867,13 +2940,14 @@
         assertNotNull(lp);
         assertTrue(lp.getDnsServers().contains(dnsServer));
         assertTrue(lp.getDnsServers().contains(SERVER_ADDR));
+        assertHasAddressThat("link-local address", lp, x -> x.getAddress().isLinkLocalAddress());
+        assertHasAddressThat("privacy address", lp, this::isPrivacyAddress);
+        assertHasAddressThat("stable privacy address", lp, this::isStablePrivacyAddress);
 
         return lp;
     }
 
     private void doDualStackProvisioning(boolean shouldDisableAcceptRa) throws Exception {
-        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
-
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
                 .build();
@@ -2884,7 +2958,12 @@
         // not strictly necessary.
         setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
                 false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
-        mIpc.startProvisioning(config);
+        // Both signature and root tests can use this function to do dual-stack provisioning.
+        if (useNetworkStackSignature()) {
+            mIpc.startProvisioning(config);
+        } else {
+            mIIpClient.startProvisioning(config.toStableParcelable());
+        }
 
         performDualStackProvisioning();
     }
@@ -3063,7 +3142,7 @@
         // Force IpClient transition to RunningState from PreconnectionState.
         mIpc.notifyPreconnectionComplete(true /* success */);
         HandlerUtils.waitForIdle(mDependencies.mDhcpClient.getHandler(), TEST_TIMEOUT_MS);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
 
         // DHCP server SHOULD NOT honor the Rapid-Commit option if the response would
         // contain the IPv6-only Preferred option to the client, instead respond with
@@ -3246,7 +3325,7 @@
                 false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
 
         startIpClientProvisioning(prov.build());
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
         verify(mCb, never()).onProvisioningFailure(any());
 
         return getNextDhcpPacket();
@@ -3798,7 +3877,7 @@
         setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
                 isMulticastResolicitEnabled);
         startIpClientProvisioning(config);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
         doIpv6OnlyProvisioning();
 
         // Simulate the roaming.
@@ -3936,11 +4015,6 @@
             final boolean isIgnoreIncompleteIpv6DnsServerEnabled,
             final boolean isIgnoreIncompleteIpv6DefaultRouterEnabled,
             final boolean expectNeighborLost) throws Exception {
-        // This mock is required, otherwise, IpReachabilityMonitor#avoidingBadLinks() will always
-        // return false, that results in the target tested IPv6 off-link DNS server won't be removed
-        // from LP and notifyLost won't be invoked.
-        when(mCm.shouldAvoidBadWifi()).thenReturn(true);
-
         mNetworkAgentThread =
                 new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
         mNetworkAgentThread.start();
@@ -3957,7 +4031,7 @@
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .build();
         startIpClientProvisioning(config);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(false);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
 
         final List<ByteBuffer> options = new ArrayList<ByteBuffer>();
         options.add(buildPioOption(3600, 1800, "2001:db8:1::/64")); // PIO
@@ -4327,4 +4401,106 @@
         assertFalse(lp.hasGlobalIpv6Address());
         assertEquals(1, lp.getLinkAddresses().size()); // only link-local
     }
+
+    @Test
+    public void testMaxDtimMultiplier_IPv6OnlyNetwork() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+
+        LinkProperties lp = doIpv6OnlyProvisioning();
+        assertNotNull(lp);
+        assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_IPv6LinkLocalOnlyMode() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withIpv6LinkLocalOnly()
+                .build();
+        startIpClientProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
+        inOrder.verify(mCb).setMaxDtimMultiplier(
+                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                DTIM_MULTIPLIER_RESET);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_IPv4OnlyNetwork() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
+                true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
+                TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
+        inOrder.verify(mCb).setMaxDtimMultiplier(
+                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
+    }
+
+    private void runDualStackNetworkDtimMultiplierSetting(final InOrder inOrder) throws Exception {
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+        inOrder.verify(mCb).setMaxDtimMultiplier(
+                IpClient.DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_DualStackNetwork() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        runDualStackNetworkDtimMultiplierSetting(inOrder);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_MulticastLock() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        runDualStackNetworkDtimMultiplierSetting(inOrder);
+
+        // Simulate to hold the multicast lock by disabling the multicast filter.
+        mIIpClient.setMulticastFilter(false);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
+
+        // Simulate to disable the multicast lock again, then check the multiplier should be
+        // changed to 2 (dual-stack setting)
+        mIIpClient.setMulticastFilter(true);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_MulticastLockEnabled_StoppedState() throws Exception {
+        // Simulate to hold the multicast lock by disabling the multicast filter at StoppedState,
+        // verify no callback to be sent, start dual-stack provisioning and verify the multiplier
+        // to be set to 1 (multicast lock setting) later.
+        mIIpClient.setMulticastFilter(false);
+        verify(mCb, after(10).never()).setMaxDtimMultiplier(
+                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
+
+        doDualStackProvisioning(false /* shouldDisableAcceptRa */);
+        verify(mCb, times(1)).setMaxDtimMultiplier(
+                IpClient.DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
+    }
+
+    @Test
+    public void testMaxDtimMultiplier_resetMultiplier() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        runDualStackNetworkDtimMultiplierSetting(inOrder);
+
+        verify(mCb, never()).setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
+
+        // Stop IpClient and verify if the multiplier has been reset.
+        mIIpClient.stop();
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
+    }
 }
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt
index f1fa1c7..0f7ec0c 100644
--- a/tests/integration/root/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -138,7 +138,7 @@
         }
     }
 
-    private val originalFlagValues = ArrayMap<String, String>()
+    private val originalPropertyValues = ArrayMap<String, String>()
 
     /**
      * Wrapper class for IIpClientCallbacks.
@@ -154,11 +154,11 @@
     }
 
     @After
-    fun tearDownFlags() {
+    fun tearDownDeviceConfigProperties() {
         if (testSkipped()) return
         automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
         try {
-            for ((key, value) in originalFlagValues.entries) {
+            for ((key, value) in originalPropertyValues.entries) {
                 if (key == null) continue
                 DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, key,
                         value, false /* makeDefault */)
@@ -195,25 +195,34 @@
         return ipClientCaptor.value
     }
 
-    override fun setFeatureEnabled(feature: String, enabled: Boolean) {
+    private fun setDeviceConfigProperty(name: String, value: String) {
         automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
         try {
-            // Do not use computeIfAbsent as it would overwrite null values (flag originally unset)
-            if (!originalFlagValues.containsKey(feature)) {
-                originalFlagValues[feature] =
-                        DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature)
+            // Do not use computeIfAbsent as it would overwrite null values,
+            // property originally unset.
+            if (!originalPropertyValues.containsKey(name)) {
+                originalPropertyValues[name] =
+                        DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, name)
             }
-            // The feature is enabled if the flag is lower than the package version.
-            // Package versions follow a standard format with 9 digits.
-            // TODO: consider resetting flag values on reboot when set to special values like "1" or
-            // "999999999"
-            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, feature,
-                    if (enabled) "1" else "999999999", false)
+            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, name, value,
+                    false /* makeDefault */)
         } finally {
             automation.dropShellPermissionIdentity()
         }
     }
 
+    override fun setFeatureEnabled(feature: String, enabled: Boolean) {
+        // The feature is enabled if the flag is lower than the package version.
+        // Package versions follow a standard format with 9 digits.
+        // TODO: consider resetting flag values on reboot when set to special values like "1" or
+        // "999999999"
+        setDeviceConfigProperty(feature, if (enabled) "1" else "999999999")
+    }
+
+    override fun setDeviceConfigProperty(name: String, value: Int) {
+        setDeviceConfigProperty(name, value.toString())
+    }
+
     override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
         automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
         try {
diff --git a/tests/integration/signature/android/net/NetworkStatsIntegrationTest.kt b/tests/integration/signature/android/net/NetworkStatsIntegrationTest.kt
index 11ec1c2..f1cbd21 100644
--- a/tests/integration/signature/android/net/NetworkStatsIntegrationTest.kt
+++ b/tests/integration/signature/android/net/NetworkStatsIntegrationTest.kt
@@ -18,8 +18,10 @@
 
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
 import android.app.usage.NetworkStatsManager
 import android.net.NetworkTemplate.MATCH_TEST
+import android.os.Process
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.testutils.PacketBridge
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
@@ -56,10 +58,12 @@
     private val REMOTE_V4ADDR =
         LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
     private val DEFAULT_BUFFER_SIZE = 1000
+    private val CONNECTION_TIMEOUT_MILLIS = 15000
     private val TEST_DOWNLOAD_SIZE = 10000L
     private val TEST_UPLOAD_SIZE = 20000L
     private val HTTP_SERVER_NAME = "test.com"
     private val DNS_SERVER_PORT = 53
+    private val TEST_TAG = 0xF00D
 
     // Set up the packet bridge with two IPv6 address only test networks.
     private val inst = InstrumentationRegistry.getInstrumentation()
@@ -133,6 +137,7 @@
         val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
 
         val (_, rxBytesBeforeTest) = getTotalTxRxBytes(internalInterfaceName)
+        val (_, rxTaggedBytesBeforeTest) = getTaggedTxRxBytes(internalInterfaceName, TEST_TAG)
 
         // Generate the download traffic.
         genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
@@ -140,25 +145,58 @@
         // In practice, for one way 10k download payload, the download usage is about
         // 11222~12880 bytes. And the upload usage is about 1279~1626 bytes, which is majorly
         // contributed by TCP ACK packets.
-        val (txBytesAfterDownload, rxBytesAfterDownload) = getTotalTxRxBytes(internalInterfaceName)
-        assertTrue(
-            rxBytesAfterDownload - rxBytesBeforeTest in
-                    TEST_DOWNLOAD_SIZE..(TEST_DOWNLOAD_SIZE * 1.3).toLong(),
-            "Download size on $internalInterfaceName"
+        val (txBytesAfterDownload, rxBytesAfterDownload) =
+            getTotalTxRxBytes(internalInterfaceName)
+        val (txTaggedBytesAfterDownload, rxTaggedBytesAfterDownload) = getTaggedTxRxBytes(
+            internalInterfaceName,
+            TEST_TAG
+        )
+        assertInRange(
+            "Download size", internalInterfaceName,
+            rxBytesAfterDownload - rxBytesBeforeTest,
+            TEST_DOWNLOAD_SIZE, (TEST_DOWNLOAD_SIZE * 1.3).toLong()
+        )
+        // Increment of tagged data should be zero since no tagged traffic was generated.
+        assertEquals(
+            rxTaggedBytesBeforeTest,
+            rxTaggedBytesAfterDownload,
+            "Tagged download size of uid ${Process.myUid()} on $internalInterfaceName"
         )
 
-        genHttpTraffic(packetBridge.internalNetwork, TEST_UPLOAD_SIZE, downloadSize = 0L)
+        // Generate upload traffic with tag to verify tagged data accounting as well.
+        genHttpTrafficWithTag(
+            packetBridge.internalNetwork,
+            TEST_UPLOAD_SIZE,
+            downloadSize = 0L,
+            TEST_TAG
+        )
 
         // Verify upload data usage accounting.
         val (txBytesAfterUpload, _) = getTotalTxRxBytes(internalInterfaceName)
-        assertTrue(
-            txBytesAfterUpload - txBytesAfterDownload in
-                    TEST_UPLOAD_SIZE..(TEST_UPLOAD_SIZE * 1.3).toLong(),
-            "Upload size on $internalInterfaceName"
+        val (txTaggedBytesAfterUpload, _) = getTaggedTxRxBytes(internalInterfaceName, TEST_TAG)
+        assertInRange(
+            "Upload size", internalInterfaceName,
+            txBytesAfterUpload - txBytesAfterDownload,
+            TEST_UPLOAD_SIZE, (TEST_UPLOAD_SIZE * 1.3).toLong()
+        )
+        assertInRange(
+            "Tagged upload size of uid ${Process.myUid()}",
+            internalInterfaceName,
+            txTaggedBytesAfterUpload - txTaggedBytesAfterDownload,
+            TEST_UPLOAD_SIZE,
+            (TEST_UPLOAD_SIZE * 1.3).toLong()
         )
     }
 
-    private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) {
+    private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+        genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+    private fun genHttpTrafficWithTag(
+        network: Network,
+        uploadSize: Long,
+        downloadSize: Long,
+        tag: Int
+    ) {
         val path = "/test_upload_download"
         val buf = ByteArray(DEFAULT_BUFFER_SIZE)
 
@@ -166,50 +204,84 @@
             TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK,
             content = getRandomString(downloadSize)
         )
-        val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
-        val url = URL(spec)
-        val httpConnection = network.openConnection(url) as HttpURLConnection
-        httpConnection.requestMethod = "POST"
-        httpConnection.doOutput = true
-        // Tell the server that the response should not be compressed. Otherwise, the data usage
-        // accounted will be less than expected.
-        httpConnection.setRequestProperty("Accept-Encoding", "identity")
+        var httpConnection: HttpURLConnection? = null
+        try {
+            TrafficStats.setThreadStatsTag(tag)
+            val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+            val url = URL(spec)
+            httpConnection = network.openConnection(url) as HttpURLConnection
+            httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+            httpConnection.requestMethod = "POST"
+            httpConnection.doOutput = true
+            // Tell the server that the response should not be compressed. Otherwise, the data usage
+            // accounted will be less than expected.
+            httpConnection.setRequestProperty("Accept-Encoding", "identity")
+            // Tell the server that to close connection after this request, this is needed to
+            // prevent from reusing the same socket that has different tagging requirement.
+            httpConnection.setRequestProperty("Connection", "close")
 
-        // Send http body.
-        val outputStream = BufferedOutputStream(httpConnection.outputStream)
-        outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
-        outputStream.close()
-        assertEquals(HTTP_OK, httpConnection.responseCode)
+            // Send http body.
+            val outputStream = BufferedOutputStream(httpConnection.outputStream)
+            outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+            outputStream.close()
+            assertEquals(HTTP_OK, httpConnection.responseCode)
 
-        // Receive response from the server.
-        val inputStream = BufferedInputStream(httpConnection.getInputStream())
-        var total = 0L
-        while (true) {
-            val count = inputStream.read(buf)
-            if (count == -1) break // End-of-Stream
-            total += count
+            // Receive response from the server.
+            val inputStream = BufferedInputStream(httpConnection.getInputStream())
+            var total = 0L
+            while (true) {
+                val count = inputStream.read(buf)
+                if (count == -1) break // End-of-Stream
+                total += count
+            }
+            assertEquals(downloadSize, total)
+        } finally {
+            httpConnection?.inputStream?.close()
+            TrafficStats.clearThreadStatsTag()
         }
-        assertEquals(downloadSize, total)
-        httpConnection.getInputStream().close()
     }
 
     private fun getTotalTxRxBytes(iface: String): Pair<Long, Long> {
+        return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+            nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+        }
+    }
+
+    private fun getTaggedTxRxBytes(iface: String, tag: Int): Pair<Long, Long> {
+        return getNetworkStatsThat(iface, tag) { nsm, template ->
+            nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+        }
+    }
+
+    private fun getNetworkStatsThat(
+        iface: String,
+        tag: Int,
+        queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+    ): Pair<Long, Long> {
         val nsm = context.getSystemService(NetworkStatsManager::class.java)
         nsm.forceUpdate()
         val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
             .setWifiNetworkKeys(setOf(iface)).build()
-        val xtStats = nsm.querySummary(testTemplate, Long.MIN_VALUE, Long.MAX_VALUE)
+        val stats = queryApi.invoke(nsm, testTemplate)
         val recycled = NetworkStats.Bucket()
         var rx = 0L
         var tx = 0L
-        while (xtStats.hasNextBucket()) {
-            xtStats.getNextBucket(recycled)
+        while (stats.hasNextBucket()) {
+            stats.getNextBucket(recycled)
+            if (recycled.uid != Process.myUid() || recycled.tag != tag) continue
             rx += recycled.rxBytes
             tx += recycled.txBytes
         }
         return tx to rx
     }
 
+    /** Verify the given value is in range [lower, upper]  */
+    private fun assertInRange(tag: String, iface: String, value: Long, lower: Long, upper: Long) =
+        assertTrue(
+            value in lower..upper,
+            "$tag on $iface: $value is not within range [$lower, $upper]"
+        )
+
     fun getRandomString(length: Long): String {
         val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
         return (1..length)
diff --git a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
index 26938b6..b494732 100644
--- a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
+++ b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
@@ -52,6 +52,10 @@
         mEnabledFeatures.put(name, enabled)
     }
 
+    override fun setDeviceConfigProperty(name: String, value: Int) {
+        mDependencies.setDeviceConfigProperty(name, value)
+    }
+
     override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes {
         val networkAttributesCaptor = ArgumentCaptor.forClass(NetworkAttributes::class.java)
 
diff --git a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
index 0a80d70..29c5a6c 100644
--- a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
+++ b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
@@ -47,9 +47,9 @@
 import com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN
 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
 import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_FLAGS_OFFSET
 import com.android.net.module.util.NetworkStackConstants.IPV4_FLAG_DF
 import com.android.net.module.util.NetworkStackConstants.IPV4_FLAG_MF
-import com.android.net.module.util.NetworkStackConstants.IPV4_FLAGS_OFFSET
 import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST
 import com.android.net.module.util.structs.PrefixInformationOption
 import com.android.networkstack.util.NetworkStackUtils
@@ -59,10 +59,6 @@
 import com.android.testutils.IPv4UdpFilter
 import com.android.testutils.TapPacketReader
 import com.android.testutils.UDP_HEADER_LENGTH
-import org.junit.After
-import org.junit.Assert.assertArrayEquals
-import org.junit.Before
-import org.junit.Test
 import java.io.FileDescriptor
 import java.net.Inet4Address
 import java.net.Inet6Address
@@ -71,8 +67,13 @@
 import kotlin.reflect.KClass
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Before
+import org.junit.Test
 
 class NetworkStackUtilsIntegrationTest {
     private val inst by lazy { InstrumentationRegistry.getInstrumentation() }
@@ -225,6 +226,41 @@
         assertSolicitedNodeMulticastAddress(addr3, TEST_INET6ADDR_3)
     }
 
+    @Test
+    fun testConvertMacAddressToEui64() {
+        // MAC address with universal/local bit set (the first byte: 0xBA)
+        var expected = byteArrayOf(
+                0xB8.toByte(), 0x98.toByte(), 0x76.toByte(), 0xFF.toByte(),
+                0xFE.toByte(), 0x54.toByte(), 0x32.toByte(), 0x10.toByte())
+        val srcEui64 = NetworkStackUtils.macAddressToEui64(TEST_SRC_MAC)
+        assertArrayEquals(expected, srcEui64)
+
+        // MAC address with universal/local bit unset (the first byte: 0x01).
+        expected = byteArrayOf(
+                0x03.toByte(), 0x23.toByte(), 0x45.toByte(), 0xFF.toByte(),
+                0xFE.toByte(), 0x67.toByte(), 0x89.toByte(), 0x0A.toByte())
+        val targetEui64 = NetworkStackUtils.macAddressToEui64(TEST_TARGET_MAC)
+        assertArrayEquals(expected, targetEui64)
+    }
+
+    @Test
+    fun testGenerateIpv6AddressFromEui64() {
+        val eui64 = NetworkStackUtils.macAddressToEui64(TEST_SRC_MAC)
+        var prefix = IpPrefix("2001:db8:1::/80")
+        // Don't accept the prefix length larger than 64.
+        assertNull(NetworkStackUtils.createInet6AddressFromEui64(prefix, eui64))
+
+        prefix = IpPrefix("2001:db8:1::/48")
+        // Don't accept the prefix length less than 64.
+        assertNull(NetworkStackUtils.createInet6AddressFromEui64(prefix, eui64))
+
+        prefix = IpPrefix("2001:db8:1::/64")
+        // IPv6 address string is formed by combining the IPv6 prefix("2001:db8:1::") and
+        // EUI64 converted from TEST_SRC_MAC, see above test for the output EUI64 example.
+        val expected = parseNumericAddress("2001:db8:1::b898:76ff:fe54:3210") as Inet6Address
+        assertEquals(expected, NetworkStackUtils.createInet6AddressFromEui64(prefix, eui64))
+    }
+
     private fun assertSocketReadErrno(msg: String, fd: FileDescriptor, errno: Int) {
         val received = ByteBuffer.allocate(TEST_MTU)
         try {
@@ -257,11 +293,12 @@
         packet.putShort(checksumOffset, IpUtils.ipChecksum(packet, ETHER_HEADER_LENGTH))
     }
 
-    private fun doTestDhcpResponseWithMfBit(dropMf: Boolean) {
+    @Test
+    fun testDhcpResponseWithMfBitDropped() {
         val ifindex = InterfaceParams.getByName(iface.interfaceName).index
         val packetSock = Os.socket(AF_PACKET, SOCK_RAW or SOCK_NONBLOCK, /*protocol=*/0)
         try {
-            NetworkStackUtils.attachDhcpFilter(packetSock, dropMf)
+            NetworkStackUtils.attachDhcpFilter(packetSock)
             val addr = SocketUtils.makePacketSocketAddress(OsConstants.ETH_P_IP, ifindex)
             Os.bind(packetSock, addr)
             val packet = DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, 42,
@@ -270,15 +307,11 @@
             setMfBit(packet, true)
             reader.sendResponse(packet)
 
-            // Packet with MF bit set is received iff dropMf is false.
-            if (dropMf) {
-                assertSocketReadErrno("Packet with MF bit should have been dropped",
-                    packetSock, OsConstants.EAGAIN)
-            } else {
-                assertNextPacketOnSocket(packetSock, packet)
-            }
+            // Packet with MF bit set is not received.
+            assertSocketReadErrno("Packet with MF bit should have been dropped",
+                packetSock, OsConstants.EAGAIN)
 
-            // Identical packet, except with MF bit cleared, should always be received.
+            // Identical packet, except with MF bit cleared, should be received.
             setMfBit(packet, false)
             reader.sendResponse(packet)
             assertNextPacketOnSocket(packetSock, packet)
@@ -286,16 +319,6 @@
             Os.close(packetSock)
         }
     }
-
-    @Test
-    fun testDhcpResponseWithMfBitDropped() {
-        doTestDhcpResponseWithMfBit(/*dropMf=*/ true)
-    }
-
-    @Test
-    fun testDhcpResponseWithMfBitReceived() {
-        doTestDhcpResponseWithMfBit(/*dropMf=*/ false)
-    }
 }
 
 private fun ByteBuffer.readAsArray(): ByteArray {
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index f9f7e64..285fcb4 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -16,12 +16,15 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfGenerator.Register.R0;
+import static android.net.apf.ApfGenerator.Register.R1;
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_STREAM;
@@ -51,7 +54,6 @@
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.apf.ApfGenerator.IllegalInstructionException;
-import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IIpClientCallbacks;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
 import android.net.metrics.IpConnectivityLog;
@@ -61,8 +63,10 @@
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
+import android.util.Pair;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -99,7 +103,10 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Random;
 
@@ -155,7 +162,7 @@
     private static final byte SIZE8   = (byte)(1 << 1);
     private static final byte SIZE16  = (byte)(2 << 1);
     private static final byte SIZE32  = (byte)(3 << 1);
-    private static final byte R1 = 1;
+    private static final byte R1_REG = 1;
 
     private static ApfConfiguration getDefaultConfig() {
         ApfFilter.ApfConfiguration config = new ApfConfiguration();
@@ -176,16 +183,28 @@
         }
     }
 
+    private static void assertReturnCodesEqual(String msg, int expected, int got) {
+        assertEquals(msg, label(expected), label(got));
+    }
+
     private static void assertReturnCodesEqual(int expected, int got) {
         assertEquals(label(expected), label(got));
     }
 
     private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) {
-        assertReturnCodesEqual(expected, apfSimulate(program, packet, null, filterAge));
+        final String msg = "Unexpected APF verdict. To debug:\n"
+                + "  apf_run --program " + HexDump.toHexString(program)
+                + " --packet " + HexDump.toHexString(packet) + " --trace | less\n  ";
+        assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
+    }
+
+    private void assertVerdict(String msg, int expected, byte[] program, byte[] packet,
+            int filterAge) {
+        assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
     }
 
     private void assertVerdict(int expected, byte[] program, byte[] packet) {
-        assertReturnCodesEqual(expected, apfSimulate(program, packet, null, 0));
+        assertVerdict(expected, program, packet, 0);
     }
 
     private void assertPass(byte[] program, byte[] packet, int filterAge) {
@@ -293,7 +312,7 @@
         gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
+        gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL);
         assertDrop(gen);
 
@@ -307,13 +326,13 @@
         gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
+        gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test load immediate.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
@@ -337,35 +356,35 @@
 
         // Test and.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addAnd(123456789);
         gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test left shift.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addLeftShift(1);
         gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test right shift.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addRightShift(1);
         gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test multiply.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 123456789);
+        gen.addLoadImmediate(R0, 123456789);
         gen.addMul(2);
         gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test divide.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addDiv(2);
         gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL);
         assertDrop(gen);
@@ -378,61 +397,61 @@
 
         // Test add.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addLoadImmediate(R1, 1234567890);
         gen.addAddR1();
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test subtract.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, -1234567890);
+        gen.addLoadImmediate(R1, -1234567890);
         gen.addAddR1();
         gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test or.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addLoadImmediate(R1, 1234567890);
         gen.addOrR1();
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test and.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addLoadImmediate(Register.R1, 123456789);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addLoadImmediate(R1, 123456789);
         gen.addAndR1();
         gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test left shift.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addLoadImmediate(R1, 1);
         gen.addLeftShiftR1();
         gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test right shift.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addLoadImmediate(Register.R1, -1);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addLoadImmediate(R1, -1);
         gen.addLeftShiftR1();
         gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test multiply.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 123456789);
-        gen.addLoadImmediate(Register.R1, 2);
+        gen.addLoadImmediate(R0, 123456789);
+        gen.addLoadImmediate(R1, 2);
         gen.addMulR1();
         gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test divide.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addLoadImmediate(Register.R1, 2);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addLoadImmediate(R1, 2);
         gen.addDivR1();
         gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL);
         assertDrop(gen);
@@ -445,53 +464,53 @@
 
         // Test byte load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoad8(Register.R0, 1);
+        gen.addLoad8(R0, 1);
         gen.addJumpIfR0Equals(45, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test out of bounds load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoad8(Register.R0, 16);
+        gen.addLoad8(R0, 16);
         gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
         assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test half-word load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoad16(Register.R0, 1);
+        gen.addLoad16(R0, 1);
         gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test word load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoad32(Register.R0, 1);
+        gen.addLoad32(R0, 1);
         gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test byte indexed load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1);
-        gen.addLoad8Indexed(Register.R0, 0);
+        gen.addLoadImmediate(R1, 1);
+        gen.addLoad8Indexed(R0, 0);
         gen.addJumpIfR0Equals(45, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test out of bounds indexed load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 8);
-        gen.addLoad8Indexed(Register.R0, 8);
+        gen.addLoadImmediate(R1, 8);
+        gen.addLoad8Indexed(R0, 8);
         gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
         assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test half-word indexed load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1);
-        gen.addLoad16Indexed(Register.R0, 0);
+        gen.addLoadImmediate(R1, 1);
+        gen.addLoad16Indexed(R0, 0);
         gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test word indexed load.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1);
-        gen.addLoad32Indexed(Register.R0, 0);
+        gen.addLoadImmediate(R1, 1);
+        gen.addLoad32Indexed(R0, 0);
         gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
 
@@ -500,7 +519,7 @@
         gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
+        gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL);
         assertDrop(gen);
 
@@ -517,11 +536,11 @@
         gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
+        gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
         assertDrop(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 3);
+        gen.addLoadImmediate(R0, 3);
         gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL);
         assertDrop(gen);
 
@@ -530,8 +549,8 @@
         gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 2);
-        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoadImmediate(R0, 2);
+        gen.addLoadImmediate(R1, 1);
         gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL);
         assertDrop(gen);
 
@@ -540,100 +559,100 @@
         gen.addJumpIfR0LessThanR1(gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1);
+        gen.addLoadImmediate(R1, 1);
         gen.addJumpIfR0LessThanR1(gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if any bits set in register.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 3);
+        gen.addLoadImmediate(R1, 3);
         gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
         assertPass(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 3);
-        gen.addLoadImmediate(Register.R0, 1);
+        gen.addLoadImmediate(R1, 3);
+        gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
         assertDrop(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 3);
-        gen.addLoadImmediate(Register.R0, 3);
+        gen.addLoadImmediate(R1, 3);
+        gen.addLoadImmediate(R0, 3);
         gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test load from memory.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadFromMemory(Register.R0, 0);
+        gen.addLoadFromMemory(R0, 0);
         gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test store to memory.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1234567890);
-        gen.addStoreToMemory(Register.R1, 12);
-        gen.addLoadFromMemory(Register.R0, 12);
+        gen.addLoadImmediate(R1, 1234567890);
+        gen.addStoreToMemory(R1, 12);
+        gen.addLoadFromMemory(R0, 12);
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test filter age pre-filled memory.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
+        gen.addLoadFromMemory(R0, gen.FILTER_AGE_MEMORY_SLOT);
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen, new byte[MIN_PKT_SIZE], 1234567890);
 
         // Test packet size pre-filled memory.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+        gen.addLoadFromMemory(R0, gen.PACKET_SIZE_MEMORY_SLOT);
         gen.addJumpIfR0Equals(MIN_PKT_SIZE, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test IPv4 header size pre-filled memory.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+        gen.addLoadFromMemory(R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
         gen.addJumpIfR0Equals(20, gen.DROP_LABEL);
         assertDrop(gen, new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x45}, 0);
 
         // Test not.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addNot(Register.R0);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addNot(R0);
         gen.addJumpIfR0Equals(~1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test negate.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addNeg(Register.R0);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addNeg(R0);
         gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test move.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1234567890);
-        gen.addMove(Register.R0);
+        gen.addLoadImmediate(R1, 1234567890);
+        gen.addMove(R0);
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
-        gen.addMove(Register.R1);
+        gen.addLoadImmediate(R0, 1234567890);
+        gen.addMove(R1);
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test swap.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R1, 1234567890);
+        gen.addLoadImmediate(R1, 1234567890);
         gen.addSwap();
         gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL);
         assertDrop(gen);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1234567890);
+        gen.addLoadImmediate(R0, 1234567890);
         gen.addSwap();
         gen.addJumpIfR0Equals(0, gen.DROP_LABEL);
         assertDrop(gen);
 
         // Test jump if bytes not equal.
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
-        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addLoadImmediate(R0, 1);
+        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
         program = gen.generate();
         assertEquals(6, program.length);
         assertEquals((13 << 3) | (1 << 1) | 0, program[0]);
@@ -644,21 +663,21 @@
         assertEquals(123, program[5]);
         assertDrop(program, new byte[MIN_PKT_SIZE], 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
-        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addLoadImmediate(R0, 1);
+        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
         byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0};
         assertPass(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
         assertDrop(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
-        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL);
+        gen.addLoadImmediate(R0, 1);
+        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL);
         byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0};
         assertDrop(gen, packet12345, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addLoadImmediate(Register.R0, 1);
-        gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL);
+        gen.addLoadImmediate(R0, 1);
+        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL);
         assertPass(gen, packet12345, 0);
     }
 
@@ -672,13 +691,13 @@
     public void testApfDataOpcodesWantApfV3() throws IllegalInstructionException, Exception {
         ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
         try {
-            gen.addStoreData(Register.R0, 0);
+            gen.addStoreData(R0, 0);
             fail();
         } catch (IllegalInstructionException expected) {
             /* pass */
         }
         try {
-            gen.addLoadData(Register.R0, 0);
+            gen.addLoadData(R0, 0);
             fail();
         } catch (IllegalInstructionException expected) {
             /* pass */
@@ -694,22 +713,22 @@
 
         // 0-byte immediate: li R0, 0
         gen = new ApfGenerator(4);
-        gen.addLoadImmediate(Register.R0, 0);
+        gen.addLoadImmediate(R0, 0);
         assertProgramEquals(new byte[]{LI_OP | SIZE0}, gen.generate());
 
         // 1-byte immediate: li R0, 42
         gen = new ApfGenerator(4);
-        gen.addLoadImmediate(Register.R0, 42);
+        gen.addLoadImmediate(R0, 42);
         assertProgramEquals(new byte[]{LI_OP | SIZE8, 42}, gen.generate());
 
         // 2-byte immediate: li R1, 0x1234
         gen = new ApfGenerator(4);
-        gen.addLoadImmediate(Register.R1, 0x1234);
-        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, 0x12, 0x34}, gen.generate());
+        gen.addLoadImmediate(R1, 0x1234);
+        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1_REG, 0x12, 0x34}, gen.generate());
 
         // 4-byte immediate: li R0, 0x12345678
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 0x12345678);
+        gen.addLoadImmediate(R0, 0x12345678);
         assertProgramEquals(
                 new byte[]{LI_OP | SIZE32, 0x12, 0x34, 0x56, 0x78},
                 gen.generate());
@@ -724,18 +743,18 @@
 
         // 1-byte negative immediate: li R0, -42
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, -42);
+        gen.addLoadImmediate(R0, -42);
         assertProgramEquals(new byte[]{LI_OP | SIZE8, -42}, gen.generate());
 
         // 2-byte negative immediate: li R1, -0x1122
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R1, -0x1122);
-        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE},
+        gen.addLoadImmediate(R1, -0x1122);
+        assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // 4-byte negative immediate: li R0, -0x11223344
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, -0x11223344);
+        gen.addLoadImmediate(R0, -0x11223344);
         assertProgramEquals(
                 new byte[]{LI_OP | SIZE32, (byte)0xEE, (byte)0xDD, (byte)0xCC, (byte)0xBC},
                 gen.generate());
@@ -750,25 +769,26 @@
 
         // Load data with no offset: lddw R0, [0 + r1]
         gen = new ApfGenerator(3);
-        gen.addLoadData(Register.R0, 0);
+        gen.addLoadData(R0, 0);
         assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate());
 
         // Store data with 8bit negative offset: lddw r0, [-42 + r1]
         gen = new ApfGenerator(3);
-        gen.addStoreData(Register.R0, -42);
+        gen.addStoreData(R0, -42);
         assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate());
 
         // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0]
         gen = new ApfGenerator(3);
-        gen.addStoreData(Register.R1, -0x1122);
-        assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE},
+        gen.addStoreData(R1, -0x1122);
+        assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0]
         gen = new ApfGenerator(3);
-        gen.addLoadData(Register.R1, 0xDEADBEEF);
+        gen.addLoadData(R1, 0xDEADBEEF);
         assertProgramEquals(
-                new byte[]{LDDW_OP | SIZE32 | R1, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF},
+                new byte[]{LDDW_OP | SIZE32 | R1_REG,
+                        (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF},
                 gen.generate());
     }
 
@@ -788,9 +808,9 @@
         // Expect value 0x87654321 to be stored starting from address -11 from the end of the
         // data buffer, in big-endian order.
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 0x87654321);
-        gen.addLoadImmediate(Register.R1, -5);
-        gen.addStoreData(Register.R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
+        gen.addLoadImmediate(R0, 0x87654321);
+        gen.addLoadImmediate(R1, -5);
+        gen.addStoreData(R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
         expected_data[5] = (byte)0x87;
         expected_data[6] = (byte)0x65;
         expected_data[7] = (byte)0x43;
@@ -805,8 +825,8 @@
     public void testApfDataRead() throws IllegalInstructionException, Exception {
         // Program that DROPs if address 10 (-6) contains 0x87654321.
         ApfGenerator gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R1, 1000);
-        gen.addLoadData(Register.R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
+        gen.addLoadImmediate(R1, 1000);
+        gen.addLoadData(R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
         gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL);
         byte[] program = gen.generate();
         byte[] packet = new byte[MIN_PKT_SIZE];
@@ -834,10 +854,10 @@
     @Test
     public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception {
         ApfGenerator gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R1, -22);
-        gen.addLoadData(Register.R0, 0);  // Load from address 32 -22 + 0 = 10
+        gen.addLoadImmediate(R1, -22);
+        gen.addLoadData(R0, 0);  // Load from address 32 -22 + 0 = 10
         gen.addAdd(0x78453412);  // 87654321 + 78453412 = FFAA7733
-        gen.addStoreData(Register.R0, 4);  // Write back to address 32 -22 + 4 = 14
+        gen.addStoreData(R0, 4);  // Write back to address 32 -22 + 4 = 14
 
         byte[] packet = new byte[MIN_PKT_SIZE];
         byte[] data = new byte[32];
@@ -861,36 +881,36 @@
 
         // Program that DROPs unconditionally. This is our the baseline.
         ApfGenerator gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 3);
-        gen.addLoadData(Register.R1, 7);
+        gen.addLoadImmediate(R0, 3);
+        gen.addLoadData(R1, 7);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // Same program as before, but this time we're trying to load past the end of the data.
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 20);
-        gen.addLoadData(Register.R1, 15);  // 20 + 15 > 32
+        gen.addLoadImmediate(R0, 20);
+        gen.addLoadData(R1, 15);  // 20 + 15 > 32
         gen.addJump(gen.DROP_LABEL);  // Not reached.
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Subtracting an immediate should work...
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 20);
-        gen.addLoadData(Register.R1, -4);
+        gen.addLoadImmediate(R0, 20);
+        gen.addLoadData(R1, -4);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...and underflowing simply wraps around to the end of the buffer...
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 20);
-        gen.addLoadData(Register.R1, -30);
+        gen.addLoadImmediate(R0, 20);
+        gen.addLoadData(R1, -30);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...but doesn't allow accesses before the start of the buffer
         gen = new ApfGenerator(3);
-        gen.addLoadImmediate(Register.R0, 20);
-        gen.addLoadData(Register.R1, -1000);
+        gen.addLoadImmediate(R0, 20);
+        gen.addLoadData(R1, -1000);
         gen.addJump(gen.DROP_LABEL);  // Not reached.
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
     }
@@ -1155,8 +1175,6 @@
     private static final byte[] IPV4_MDNS_MULTICAST_ADDR = {(byte) 224, 0, 0, (byte) 251};
     private static final byte[] IPV6_MDNS_MULTICAST_ADDR =
             {(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfb};
-    private static final int DNS_HEADER_LEN = 12;
-    private static final int DNS_QDCOUNT_OFFSET = 4;
     private static final int IPV6_UDP_DEST_PORT_OFFSET = IPV6_PAYLOAD_OFFSET + 2;
     private static final int MDNS_UDP_PORT = 5353;
 
@@ -1295,8 +1313,17 @@
         apfFilter.shutdown();
     }
 
-    private static byte[] makeMdnsV4Packet(String qname) throws IOException {
-        final ByteBuffer buf = ByteBuffer.wrap(new byte[100]);
+    private static void fillQuestionSection(ByteBuffer buf, String... qnames) throws IOException {
+        buf.put(new DnsPacket.DnsHeader(0 /* id */, 0 /* flags */, qnames.length, 0 /* ancount */)
+                .getBytes());
+        for (String qname : qnames) {
+            buf.put(DnsPacket.DnsRecord.makeQuestion(qname, 0 /* nsType */, 0 /* nsClass */)
+                    .getBytes());
+        }
+    }
+
+    private static byte[] makeMdnsV4Packet(String... qnames) throws IOException {
+        final ByteBuffer buf = ByteBuffer.wrap(new byte[256]);
         final PacketBuilder builder = new PacketBuilder(buf);
         builder.writeL2Header(MacAddress.fromString("11:22:33:44:55:66"),
                 MacAddress.fromBytes(ETH_MULTICAST_MDNS_v4_MAC_ADDRESS),
@@ -1306,13 +1333,12 @@
                 (Inet4Address) Inet4Address.getByAddress(IPV4_SOURCE_ADDR),
                 (Inet4Address) Inet4Address.getByAddress(IPV4_MDNS_MULTICAST_ADDR));
         builder.writeUdpHeader((short) MDNS_UDP_PORT, (short) MDNS_UDP_PORT);
-        buf.put(new DnsPacket.DnsHeader(0, 0, 1, 0).getBytes());
-        buf.put(DnsPacket.DnsRecord.makeQuestion(qname, 0, 0).getBytes());
+        fillQuestionSection(buf, qnames);
         return builder.finalizePacket().array();
     }
 
-    private static byte[] makeMdnsV6Packet(String qname) throws IOException {
-        ByteBuffer buf = ByteBuffer.wrap(new byte[100]);
+    private static byte[] makeMdnsV6Packet(String... qnames) throws IOException {
+        ByteBuffer buf = ByteBuffer.wrap(new byte[256]);
         final PacketBuilder builder = new PacketBuilder(buf);
         builder.writeL2Header(MacAddress.fromString("11:22:33:44:55:66"),
                 MacAddress.fromBytes(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS),
@@ -1321,11 +1347,171 @@
                 (Inet6Address) InetAddress.getByAddress(IPV6_ANOTHER_ADDR),
                 (Inet6Address) Inet6Address.getByAddress(IPV6_MDNS_MULTICAST_ADDR));
         builder.writeUdpHeader((short) MDNS_UDP_PORT, (short) MDNS_UDP_PORT);
-        buf.put(new DnsPacket.DnsHeader(0, 0, 1, 0).getBytes());
-        buf.put(DnsPacket.DnsRecord.makeQuestion(qname, 0, 0).getBytes());
+        fillQuestionSection(buf, qnames);
         return builder.finalizePacket().array();
     }
 
+    private static void putLabel(ByteBuffer buf, String label) {
+        final byte[] bytes = label.getBytes(StandardCharsets.UTF_8);
+        buf.put((byte) bytes.length);
+        buf.put(bytes);
+    }
+
+    private static void putPointer(ByteBuffer buf, int offset) {
+        short pointer = (short) (offset | 0xc000);
+        buf.putShort(pointer);
+    }
+
+
+    // Simplistic DNS compression code that intentionally does not depend on production code.
+    private static List<Pair<Integer, String>> getDnsLabels(int startOffset, String... names) {
+        // Maps all possible name suffixes to packet offsets.
+        final HashMap<String, Integer> mPointerOffsets = new HashMap<>();
+        final List<Pair<Integer, String>> out = new ArrayList<>();
+        int offset = startOffset;
+        for (int i = 0; i < names.length; i++) {
+            String name = names[i];
+            while (true) {
+                if (name.length() == 0) {
+                    out.add(label(""));
+                    offset += 1 + 4;  // 1-byte label, DNS query
+                    break;
+                }
+
+                final int pointerOffset = mPointerOffsets.getOrDefault(name, -1);
+                if (pointerOffset != -1) {
+                    out.add(pointer(pointerOffset));
+                    offset += 2 + 4; // 2-byte pointer, DNS query
+                    break;
+                }
+
+                mPointerOffsets.put(name, offset);
+
+                final int indexOfDot = name.indexOf(".");
+                final String label;
+                if (indexOfDot == -1) {
+                    label = name;
+                    name = "";
+                } else {
+                    label = name.substring(0, indexOfDot);
+                    name = name.substring(indexOfDot + 1);
+                }
+                out.add(label(label));
+                offset += 1 + label.length();
+            }
+        }
+        return out;
+    }
+
+    static Pair<Integer, String> label(String label) {
+        return Pair.create(label.length(), label);
+    }
+
+    static Pair<Integer, String> pointer(int offset) {
+        return Pair.create(0xc000 | offset, null);
+    }
+
+    @Test
+    public void testGetDnsLabels() throws Exception {
+        int startOffset = 12;
+        List<Pair<Integer, String>> actual = getDnsLabels(startOffset, "myservice.tcp.local");
+        assertEquals(4, actual.size());
+        assertEquals(label("myservice"), actual.get(0));
+        assertEquals(label("tcp"), actual.get(1));
+        assertEquals(label("local"), actual.get(2));
+        assertEquals(label(""), actual.get(3));
+
+        startOffset = 30;
+        actual = getDnsLabels(startOffset,
+                "myservice.tcp.local", "foo.tcp.local", "myhostname.local", "bar.udp.local",
+                "foo.myhostname.local");
+        final int tcpLocalOffset = startOffset + 1 + "myservice".length();
+        final int localOffset = startOffset + 1 + "myservice".length() + 1 + "tcp".length();
+        final int myhostnameLocalOffset = 30
+                + 1 + "myservice".length() + 1 + "tcp".length() + 1 + "local".length() + 1 + 4
+                + 1 + "foo".length() + 2 + 4;
+
+        assertEquals(13, actual.size());
+        assertEquals(label("myservice"), actual.get(0));
+        assertEquals(label("tcp"), actual.get(1));
+        assertEquals(label("local"), actual.get(2));
+        assertEquals(label(""), actual.get(3));
+        assertEquals(label("foo"), actual.get(4));
+        assertEquals(pointer(tcpLocalOffset), actual.get(5));
+        assertEquals(label("myhostname"), actual.get(6));
+        assertEquals(pointer(localOffset), actual.get(7));
+        assertEquals(label("bar"), actual.get(8));
+        assertEquals(label("udp"), actual.get(9));
+        assertEquals(pointer(localOffset), actual.get(10));
+        assertEquals(label("foo"), actual.get(11));
+        assertEquals(pointer(myhostnameLocalOffset), actual.get(12));
+
+    }
+
+    private static byte[] makeMdnsCompressedV6Packet(String... names) throws IOException {
+        ByteBuffer questions = ByteBuffer.allocate(1500);
+        questions.put(new DnsPacket.DnsHeader(123, 0, names.length, 0).getBytes());
+        final List<Pair<Integer, String>> labels = getDnsLabels(questions.position(), names);
+        for (Pair<Integer, String> label : labels) {
+            final String name = label.second;
+            if (name == null) {
+                putPointer(questions, label.first);
+            } else {
+                putLabel(questions, name);
+            }
+            if (TextUtils.isEmpty(name)) {
+                questions.put(new byte[4]);
+            }
+        }
+        questions.flip();
+
+        ByteBuffer buf = PacketBuilder.allocate(/*hasEther=*/ true, IPPROTO_IPV6, IPPROTO_UDP,
+                questions.limit());
+        final PacketBuilder builder = new PacketBuilder(buf);
+        builder.writeL2Header(MacAddress.fromString("11:22:33:44:55:66"),
+                MacAddress.fromBytes(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS),
+                (short) ETH_P_IPV6);
+        builder.writeIpv6Header(0x680515ca /* vtf */, (byte) IPPROTO_UDP, (short) 0 /* hopLimit */,
+                (Inet6Address) InetAddress.getByAddress(IPV6_ANOTHER_ADDR),
+                (Inet6Address) Inet6Address.getByAddress(IPV6_MDNS_MULTICAST_ADDR));
+        builder.writeUdpHeader((short) MDNS_UDP_PORT, (short) MDNS_UDP_PORT);
+
+        buf.put(questions);
+
+        return builder.finalizePacket().array();
+    }
+
+    private static byte[] makeMdnsCompressedV6Packet() throws IOException {
+        return makeMdnsCompressedV6Packet("myservice.tcp.local", "googlecast.tcp.local",
+                "matter.tcp.local", "myhostname.local");
+    }
+
+    private static byte[] makeMdnsCompressedV6PacketWithManyNames() throws IOException {
+        return makeMdnsCompressedV6Packet("myservice.tcp.local", "googlecast.tcp.local",
+                "matter.tcp.local", "myhostname.local", "myhostname2.local", "myhostname3.local",
+                "myhostname4.local", "myhostname5.local", "myhostname6.local", "myhostname7.local");
+
+    }
+
+    /** Adds to the program a no-op instruction that is one byte long. */
+    private void addOneByteNoop(ApfGenerator gen) {
+        gen.addOr(0);
+    }
+
+    @Test
+    public void testAddOneByteNoopAddsOneByte() throws Exception {
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        addOneByteNoop(gen);
+        assertEquals(1, gen.generate().length);
+
+        final int count = 42;
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        for (int i = 0; i < count; i++) {
+            addOneByteNoop(gen);
+        }
+        assertEquals(count, gen.generate().length);
+    }
+
     @Test
     public void testQnameEncoding() {
         String[] qname = new String[]{"abcd", "ef", "日本"};
@@ -1388,6 +1574,178 @@
         apfFilter.shutdown();
     }
 
+    private ApfGenerator generateDnsFilter(boolean ipv6, String... labels) throws Exception {
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(R1, ipv6 ? IPV6_HEADER_LEN : IPV4_HEADER_LEN);
+        DnsUtils.generateFilter(gen, labels);
+        return gen;
+    }
+
+    private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName,
+            byte[] pkt) throws Exception {
+        final String[] labels = filterName.split(/*regex=*/ "[.]");
+        ApfGenerator gen = generateDnsFilter(ipv6, labels);
+
+        // Hack to prevent the APF instruction limit triggering.
+        for (int i = 0; i < 500; i++) {
+            addOneByteNoop(gen);
+        }
+
+        byte[] program = gen.generate();
+        Log.d(TAG, "prog_len=" + program.length);
+        if (expectPass) {
+            assertPass(program, pkt, 0);
+        } else {
+            assertDrop(program, pkt, 0);
+        }
+    }
+
+    private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName,
+            String... packetNames) throws Exception {
+        final byte[] pkt = ipv6 ? makeMdnsV6Packet(packetNames) : makeMdnsV4Packet(packetNames);
+        doTestDnsParsing(expectPass, ipv6, filterName, pkt);
+    }
+
+    @Test
+    public void testDnsParsing() throws Exception {
+        final boolean ipv4 = false, ipv6 = true;
+
+        // Packets with one question.
+        // Names don't start with _ because DnsPacket thinks such names are invalid.
+        doTestDnsParsing(true, ipv6, "googlecast.tcp.local", "googlecast.tcp.local");
+        doTestDnsParsing(true, ipv4, "googlecast.tcp.local", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv6, "googlecast.tcp.lozal", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv4, "googlecast.tcp.lozal", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv6, "googlecast.udp.local", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv4, "googlecast.udp.local", "googlecast.tcp.local");
+
+        // Packets with multiple questions that can't be compressed. Not realistic for MDNS since
+        // everything ends in .local, but useful to ensure only the non-compression code is tested.
+        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
+                "googlecast.tcp.local", "developer.android.com");
+        doTestDnsParsing(true, ipv4, "googlecast.tcp.local",
+                "developer.android.com", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv4, "googlecast.tcp.local",
+                "developer.android.com", "googlecast.tcp.invalid");
+        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
+                "developer.android.com", "www.google.co.jp", "googlecast.tcp.local");
+        doTestDnsParsing(false, ipv4, "veryverylongservicename.tcp.local",
+                "www.google.co.jp", "veryverylongservicename.tcp.invalid");
+        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
+                "www.google.co.jp", "googlecast.tcp.local", "developer.android.com");
+
+        // Name with duplicate labels.
+        doTestDnsParsing(true, ipv6, "local.tcp.local", "local.tcp.local");
+
+        final byte[] pkt = makeMdnsCompressedV6Packet();
+        doTestDnsParsing(true, ipv6, "googlecast.tcp.local", pkt);
+        doTestDnsParsing(true, ipv6, "matter.tcp.local", pkt);
+        doTestDnsParsing(true, ipv6, "myservice.tcp.local", pkt);
+        doTestDnsParsing(false, ipv6, "otherservice.tcp.local", pkt);
+    }
+
+    private void doTestDnsParsingProgramLength(int expectedLength,
+            String filterName) throws Exception {
+        final String[] labels = filterName.split(/*regex=*/ "[.]");
+
+        ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        assertEquals("Program for " + filterName + " had unexpected length:",
+                expectedLength, gen.generate().length);
+    }
+
+    /**
+     * Rough metric of code size. Checks how large the generated filter is in various scenarios.
+     * Helps ensure any changes to the code do not substantially increase APF code size.
+     */
+    @Test
+    public void testDnsParsingProgramLength() throws Exception {
+        doTestDnsParsingProgramLength(237, "MyDevice.local");
+        doTestDnsParsingProgramLength(285, "_googlecast.tcp.local");
+        doTestDnsParsingProgramLength(291, "_googlecast12345.tcp.local");
+        doTestDnsParsingProgramLength(244, "_googlecastZtcp.local");
+        doTestDnsParsingProgramLength(249, "_googlecastZtcp12345.local");
+    }
+
+    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
+            String filterName, byte[] pkt, String description) throws Exception {
+        final String[] labels = filterName.split(/*regex=*/ "[.]");
+
+        // Check that the generated code, when the program contains the specified number of extra
+        // bytes, is capable of dropping the packet.
+        ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        for (int i = 0; i < expectedNecessaryOverhead; i++) {
+            addOneByteNoop(gen);
+        }
+        final byte[] programWithJustEnoughOverhead = gen.generate();
+        assertVerdict(
+                "Overhead too low: filter for " + filterName + " with " + expectedNecessaryOverhead
+                        + " extra instructions unexpectedly passed " + description,
+                DROP, programWithJustEnoughOverhead, pkt, 0);
+
+        if (expectedNecessaryOverhead == 0) return;
+
+        // Check that the generated code, without the specified number of extra program bytes,
+        // cannot correctly drop the packet because it hits the interpreter instruction limit.
+        gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        for (int i = 0; i < expectedNecessaryOverhead - 1; i++) {
+            addOneByteNoop(gen);
+        }
+        final byte[] programWithNotEnoughOverhead = gen.generate();
+
+        assertVerdict(
+                "Overhead too high: filter for " + filterName + " with " + expectedNecessaryOverhead
+                        + " extra instructions unexpectedly dropped " + description,
+                PASS, programWithNotEnoughOverhead, pkt, 0);
+    }
+
+    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
+            String filterName, String... packetNames) throws Exception {
+        doTestDnsParsingNecessaryOverhead(expectedNecessaryOverhead, filterName,
+                makeMdnsV6Packet(packetNames),
+                "IPv6 MDNS packet containing: " + Arrays.toString(packetNames));
+    }
+
+    /**
+     * Rough metric of filter efficiency. Because the filter uses backwards jumps, on complex
+     * packets it will not finish running before the interpreter hits the maximum number of allowed
+     * instructions (== number of bytes in the program) and unconditionally accepts the packet.
+     * This test checks much extra code the program must contain in order for the generated filter
+     * to successfully drop the packet. It helps ensure any changes to the code do not reduce the
+     * complexity of packets that the APF code can drop.
+     */
+    @Test
+    public void testDnsParsingNecessaryOverhead() throws Exception {
+        // Simple packets can be parsed with zero extra code.
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "matter.tcp.local", "developer.android.com");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org");
+
+        // More complicated packets cause more instructions to be run and can only be dropped if
+        // the program contains lots of extra code.
+        doTestDnsParsingNecessaryOverhead(57, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org", "otherexample.net");
+
+        doTestDnsParsingNecessaryOverhead(115, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org", "otherexample.net", "docs.new");
+
+        doTestDnsParsingNecessaryOverhead(0, "foo.tcp.local",
+                makeMdnsCompressedV6Packet(), "compressed packet");
+
+        doTestDnsParsingNecessaryOverhead(235, "foo.tcp.local",
+                makeMdnsCompressedV6PacketWithManyNames(), "compressed packet with many names");
+    }
+
     @Test
     public void testApfFilterMulticast() throws Exception {
         final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
diff --git a/tests/unit/src/android/net/apf/JumpTableTest.kt b/tests/unit/src/android/net/apf/JumpTableTest.kt
new file mode 100644
index 0000000..6fdf38f
--- /dev/null
+++ b/tests/unit/src/android/net/apf/JumpTableTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.apf
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertThrows
+import java.util.NoSuchElementException
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.test.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class JumpTableTest {
+
+    @Mock
+    lateinit var gen: ApfGenerator
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test(expected = NullPointerException::class)
+    fun testNullStartLabel() {
+        // Can't use "null" because the method is @NonNull.
+        JumpTable(AtomicReference<String>(null).get(), 10)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testNegativeSlot() {
+        JumpTable("my_jump_table", -1)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testSlotTooLarge() {
+        JumpTable("my_jump_table", 13)
+    }
+
+    @Test
+    fun testValidSlotNumbers() {
+        JumpTable("my_jump_table", 1)
+        JumpTable("my_jump_table", 10)
+        JumpTable("my_jump_table", 12)
+    }
+
+    @Test
+    fun testGetStartLabel() {
+        assertEquals("xyz", JumpTable("xyz", 3).startLabel)
+        assertEquals("abc", JumpTable("abc", 9).startLabel)
+    }
+
+    @Test
+    fun testCodeGeneration() {
+        val name = "my_jump_table"
+        val slot = 7
+
+        val j = JumpTable(name, slot)
+        j.addLabel("foo")
+        j.addLabel("bar")
+        j.addLabel("bar")
+        j.addLabel("baz")
+
+        assertEquals(0, j.getIndex("foo"))
+        assertEquals(1, j.getIndex("bar"))
+        assertEquals(2, j.getIndex("baz"))
+
+        assertThrows(NoSuchElementException::class.java) {
+            j.getIndex("nonexistent")
+        }
+
+        val inOrder = inOrder(gen)
+
+        j.generate(gen)
+
+        inOrder.verify(gen).defineLabel(name)
+        inOrder.verify(gen).addLoadFromMemory(ApfGenerator.Register.R0, slot)
+        inOrder.verify(gen).addJumpIfR0Equals(0, "foo")
+        inOrder.verify(gen).addJumpIfR0Equals(1, "bar")
+        inOrder.verify(gen).addJumpIfR0Equals(2, "baz")
+        inOrder.verify(gen).addJump(ApfGenerator.PASS_LABEL)
+        inOrder.verifyNoMoreInteractions()
+    }
+}
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
new file mode 100644
index 0000000..2d093f7
--- /dev/null
+++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dhcp6
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.net.module.util.HexDump
+import com.android.testutils.assertThrows
+import java.nio.ByteBuffer
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class Dhcp6PacketTest {
+    @Test
+    fun testDecodeDhcp6SolicitPacket() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6SolicitPacket)
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_incorrectOptionLength() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(wrong option_len=4)
+                "000800040000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_lastTruncatedOption() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25, missing one byte)
+                "001A0019000000000000000040000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_middleTruncatedOption() {
+        val solicitHex =
+                // Solicit, Transaction ID
+                "01000F51" +
+                // client identifier option(option_len=12, missing one byte)
+                "0001000C0003001B024CCBFFFE5F6E" +
+                // elapsed time option(option_len=2)
+                "000800020000" +
+                // IA_PD option(option_len=41, including IA prefix option)
+                "00190029DE3570F50000000000000000" +
+                // IA prefix option(option_len=25)
+                "001A001900000000000000004000000000000000000000000000000000"
+        val bytes = HexDump.hexStringToByteArray(solicitHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+                Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        }
+    }
+
+    @Test
+    fun testDecodeDhcp6AdvertisePacket() {
+        val advertiseHex =
+                // Advertise, Transaction ID
+                "0200078A" +
+                // server identifier option(option_len=10)
+                "0002000A0003000186C9B26AED4D" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // IA_PD option(option_len=70, including IA prefix option)
+                "001900460CDDCA0C000000CF0000014C" +
+                // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64")
+                "001A00190000019F0000064F402401FA00049C04810000000000000000" +
+                // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64")
+                "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000"
+        val bytes = HexDump.hexStringToByteArray(advertiseHex)
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6AdvertisePacket)
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_unsupportedOption() {
+        val advertiseHex =
+                // Advertise, Transaction ID
+                "0200078A" +
+                // server identifier option(option_len=10)
+                "0002000A0003000186C9B26AED4D" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // SOL_MAX_RT (don't support this option yet)
+                "005200040000003C" +
+                // IA_PD option(option_len=70, including IA prefix option)
+                "001900460CDDCA0C000000CF0000014C" +
+                // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64")
+                "001A00190000019F0000064F402401FA00049C04810000000000000000" +
+                // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64")
+                "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000"
+        val bytes = HexDump.hexStringToByteArray(advertiseHex)
+        // The unsupported option will be skipped normally and won't throw ParseException.
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6AdvertisePacket)
+    }
+}