Merge "Rename net-utils-tethering to net-utils-connectivity-apks." into main
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 0c21c9b..f6717c5 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -38,6 +38,7 @@
     cflags: [
         "-Wall",
         "-Werror",
+        "-Wextra",
     ],
     sdk_version: "30",
     min_sdk_version: "30",
@@ -65,75 +66,46 @@
 bpf {
     name: "block.o",
     srcs: ["block.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
     name: "dscpPolicy.o",
     srcs: ["dscpPolicy.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "offload.o",
     srcs: ["offload.c"],
     btf: false,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
     name: "offload@mainline.o",
     srcs: ["offload@mainline.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DMAINLINE",
-    ],
+    cflags: ["-DMAINLINE"],
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "test.o",
     srcs: ["test.c"],
     btf: false,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
     name: "test@mainline.o",
     srcs: ["test@mainline.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DMAINLINE",
-    ],
+    cflags: ["-DMAINLINE"],
 }
 
 bpf {
     name: "clatd.o",
     srcs: ["clatd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
@@ -141,11 +113,6 @@
     // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
     name: "netd.o",
     srcs: ["netd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
     sub_dir: "netd_shared",
 }
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1f1953c..52de1a3 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -497,16 +497,26 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
   }
 
+  @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isDhcpv6PdEnabled();
+    method public boolean isNat64Enabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
+  }
+
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
     method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
     method public int getThreadVersion();
     method public static boolean isAttached(int);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void registerConfigurationCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
     field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 282a11e..1760fa7 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -261,6 +261,12 @@
             IBpfMap<S32, UidOwnerValue> uidOwnerMap,
             IBpfMap<S32, U8> dataSaverEnabledMap
     ) {
+        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+            return BLOCKED_REASON_NONE;
+        }
+
         final long uidRuleConfig;
         final long uidMatch;
         try {
@@ -331,12 +337,6 @@
     ) {
         throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
 
-        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
-        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
-        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
-            return false;
-        }
-
         final int blockedReasons = getUidNetworkingBlockedReasons(
                 uid,
                 configurationMap,
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 419ec3a..5f672e7 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1936,6 +1936,8 @@
                         mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
                 .setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
+                .setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
                 .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
                         mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
                 .build();
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index c264f25..709dc79 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -67,6 +67,12 @@
      */
     public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
 
+    /**
+     * A feature flag to avoid advertising empty TXT records, as per RFC 6763 6.1.
+     */
+    public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
+            "nsd_avoid_advertising_empty_txt_records";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -91,6 +97,9 @@
     // Flag for query with known-answer
     public final boolean mIsQueryWithKnownAnswerEnabled;
 
+    // Flag for avoiding advertising empty TXT records
+    public final boolean mAvoidAdvertisingEmptyTxtRecords;
+
     @Nullable
     private final FlagOverrideProvider mOverrideProvider;
 
@@ -142,6 +151,15 @@
     }
 
     /**
+     * Indicates whether {@link #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS} is enabled, including for
+     * testing.
+     */
+    public boolean avoidAdvertisingEmptyTxtRecords() {
+        return mAvoidAdvertisingEmptyTxtRecords
+                || isForceEnabledForTest(NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS);
+    }
+
+    /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -152,6 +170,7 @@
             boolean isUnicastReplyEnabled,
             boolean isAggressiveQueryModeEnabled,
             boolean isQueryWithKnownAnswerEnabled,
+            boolean avoidAdvertisingEmptyTxtRecords,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -161,6 +180,7 @@
         mIsUnicastReplyEnabled = isUnicastReplyEnabled;
         mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
         mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+        mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mOverrideProvider = overrideProvider;
     }
 
@@ -181,6 +201,7 @@
         private boolean mIsUnicastReplyEnabled;
         private boolean mIsAggressiveQueryModeEnabled;
         private boolean mIsQueryWithKnownAnswerEnabled;
+        private boolean mAvoidAdvertisingEmptyTxtRecords;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -195,6 +216,7 @@
             mIsUnicastReplyEnabled = true; // Default enabled.
             mIsAggressiveQueryModeEnabled = false;
             mIsQueryWithKnownAnswerEnabled = false;
+            mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mOverrideProvider = null;
         }
 
@@ -291,6 +313,16 @@
         }
 
         /**
+         * Set whether to avoid advertising empty TXT records.
+         *
+         * @see #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS
+         */
+        public Builder setAvoidAdvertisingEmptyTxtRecords(boolean avoidAdvertisingEmptyTxtRecords) {
+            mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
@@ -302,6 +334,7 @@
                     mIsUnicastReplyEnabled,
                     mIsAggressiveQueryModeEnabled,
                     mIsQueryWithKnownAnswerEnabled,
+                    mAvoidAdvertisingEmptyTxtRecords,
                     mOverrideProvider);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 0e84764..ebd95c9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -227,11 +227,13 @@
         /**
          * Create a ServiceRegistration with only update the subType.
          */
-        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
+                boolean avoidEmptyTxtRecords) {
             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
             newServiceInfo.setSubtypes(newSubtypes);
             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
-                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
+                    avoidEmptyTxtRecords);
         }
 
         /**
@@ -239,7 +241,7 @@
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
                 int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
-                @Nullable Duration ttl) {
+                @Nullable Duration ttl, boolean avoidEmptyTxtRecords) {
             this.serviceInfo = serviceInfo;
 
             final long nonNameRecordsTtlMillis;
@@ -310,7 +312,8 @@
                                 // Service name is verified unique after probing
                                 true /* cacheFlush */,
                                 nonNameRecordsTtlMillis,
-                                attrsToTextEntries(serviceInfo.getAttributes())),
+                                attrsToTextEntries(
+                                        serviceInfo.getAttributes(), avoidEmptyTxtRecords)),
                         false /* sharedName */);
 
                 allRecords.addAll(ptrRecords);
@@ -393,9 +396,10 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
+                boolean avoidEmptyTxtRecords) {
             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */, ttl);
+                    false /* exiting */, true /* isProbing */, ttl, avoidEmptyTxtRecords);
         }
 
         void setProbing(boolean probing) {
@@ -446,7 +450,7 @@
                     "Service ID must already exist for an update request: " + serviceId);
         }
         final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
-                subtypes);
+                subtypes, mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -477,7 +481,8 @@
 
         final ServiceRegistration registration = new ServiceRegistration(
                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
-                NO_PACKET /* sentPacketCount */, ttl);
+                NO_PACKET /* sentPacketCount */, ttl,
+                mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -548,8 +553,17 @@
         return new MdnsProber.ProbingInfo(serviceId, probingRecords);
     }
 
-    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
-        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs,
+            boolean avoidEmptyTxtRecords) {
+        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
+                attrs.size() == 0 ? 1 : attrs.size());
+        if (avoidEmptyTxtRecords && attrs.size() == 0) {
+            // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
+            // single empty String must be treated as equivalent.
+            out.add(new MdnsServiceInfo.TextEntry("", (byte[]) null));
+            return out;
+        }
+
         for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
             out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
         }
@@ -1403,7 +1417,8 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
+                mMdnsFeatureFlags.avoidAdvertisingEmptyTxtRecords());
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 1ec9e39..3fb92bb 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -355,6 +355,13 @@
 
     /** Represents a DNS TXT key-value pair defined by RFC 6763. */
     public static final class TextEntry implements Parcelable {
+        /**
+         * The value to use for attributes with no value.
+         *
+         * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an
+         * empty value (which would be an empty byte array).
+         */
+        public static final byte[] VALUE_NONE = null;
         public static final Parcelable.Creator<TextEntry> CREATOR =
                 new Parcelable.Creator<TextEntry>() {
                     @Override
@@ -389,7 +396,7 @@
             // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
             // boolean attribute, simply identified as being present, with no value.
             if (delimitPos < 0) {
-                return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+                return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE);
             } else if (delimitPos == 0) {
                 return null;
             }
@@ -400,13 +407,13 @@
 
         /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
         public TextEntry(String key, String value) {
-            this(key, value == null ? null : value.getBytes(UTF_8));
+            this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8));
         }
 
         /** Creates a new {@link TextEntry} with given key and value of a byte array. */
         public TextEntry(String key, byte[] value) {
             this.key = key;
-            this.value = value == null ? null : value.clone();
+            this.value = value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         private TextEntry(Parcel in) {
@@ -419,22 +426,26 @@
         }
 
         public byte[] getValue() {
-            return value == null ? null : value.clone();
+            return value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         /** Converts this {@link TextEntry} instance to '=' separated byte array. */
         public byte[] toBytes() {
             final byte[] keyBytes = key.getBytes(US_ASCII);
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return keyBytes;
             }
             return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
         }
 
+        public boolean isEmpty() {
+            return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0);
+        }
+
         /** Converts this {@link TextEntry} instance to '=' separated string. */
         @Override
         public String toString() {
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return key;
             }
             return key + "=" + new String(value, UTF_8);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 92cf324..77d1d7a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -89,6 +89,13 @@
         }
     }
 
+    private boolean isEmpty() {
+        return entries == null || entries.size() == 0
+                // RFC6763 6.1 indicates that a TXT record with a single zero byte is equivalent to
+                // an empty record.
+                || (entries.size() == 1 && entries.get(0).isEmpty());
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -105,7 +112,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Objects.hash(entries);
+        return (super.hashCode() * 31) + (isEmpty() ? 0 : Objects.hash(entries));
     }
 
     @Override
@@ -116,7 +123,19 @@
         if (!(other instanceof MdnsTextRecord)) {
             return false;
         }
-
-        return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+        if (!super.equals(other)) {
+            return false;
+        }
+        // As per RFC6763 6.1: DNS-SD clients MUST treat the following as equivalent:
+        // - A TXT record containing a single zero byte.
+        // - An empty (zero-length) TXT record. (This is not strictly legal, but should one be
+        //   received, it should be interpreted as the same as a single empty string.)
+        // - No TXT record
+        // Ensure that empty TXT records are considered equal, so that they are not considered
+        // conflicting for example.
+        if (isEmpty() && ((MdnsTextRecord) other).isEmpty()) {
+            return true;
+        }
+        return Objects.equals(entries, ((MdnsTextRecord) other).entries);
     }
 }
\ No newline at end of file
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 3db37e5..8a2e72c 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -567,7 +567,9 @@
 
         @Override
         public void close() {
-            IoUtils.closeQuietly(mFileDescriptor);
+            if (mFileDescriptor != null) {
+                IoUtils.closeQuietly(mFileDescriptor);
+            }
         }
     }
 
@@ -611,6 +613,7 @@
                 setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
             mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
@@ -695,6 +698,7 @@
                         NetworkConstants.DNS_SERVER_PORT);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
 
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index caaf959..b5a941b 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -26,11 +26,14 @@
     get_apf_counter,
     get_apf_counters_from_dumpsys,
     get_hardware_address,
-    send_broadcast_empty_ethercat_packet,
+    is_send_raw_packet_downstream_supported,
     send_raw_packet_downstream,
 )
 from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
 
+TEST_IFACE_NAME = "eth0"
+TEST_PACKET_IN_HEX = "AABBCCDDEEFF"
+
 
 class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
 
@@ -108,30 +111,18 @@
     with asserts.assert_raises(PatternNotFoundException):
       get_hardware_address(self.mock_ad, "wlan0")
 
-  @patch("net_tests_utils.host.python.apf_utils.get_hardware_address")
-  @patch("net_tests_utils.host.python.apf_utils.send_raw_packet_downstream")
-  def test_send_broadcast_empty_ethercat_packet(
-      self,
-      mock_send_raw_packet_downstream: MagicMock,
-      mock_get_hardware_address: MagicMock,
-  ) -> None:
-    mock_get_hardware_address.return_value = "12:34:56:78:90:AB"
-    send_broadcast_empty_ethercat_packet(self.mock_ad, "eth0")
-    # Assuming you'll mock the packet construction part, verify calls to send_raw_packet_downstream.
-    mock_send_raw_packet_downstream.assert_called_once()
-
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_success(
       self, mock_adb_shell: MagicMock
   ) -> None:
     mock_adb_shell.return_value = ""  # Successful command output
-    iface_name = "eth0"
-    packet_in_hex = "AABBCCDDEEFF"
-    send_raw_packet_downstream(self.mock_ad, iface_name, packet_in_hex)
+    send_raw_packet_downstream(
+        self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+    )
     mock_adb_shell.assert_called_once_with(
         self.mock_ad,
         "cmd network_stack send-raw-packet-downstream"
-        f" {iface_name} {packet_in_hex}",
+        f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}",
     )
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
@@ -142,7 +133,13 @@
         "Any Unexpected Output"
     )
     with asserts.assert_raises(UnexpectedBehaviorError):
-      send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_true(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should be supported.",
+    )
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_unsupported(
@@ -152,7 +149,13 @@
         cmd="", stdout="Unknown command", stderr="", ret_code=3
     )
     with asserts.assert_raises(UnsupportedOperationException):
-      send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_false(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should not be supported.",
+    )
 
   @parameterized.parameters(
       ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+        private val context: Context,
+    ) : TestRule {
+    private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+        context.getSystemService(TestNetworkManager::class.java)!!
+    }
+    private val ifaces = ArrayList<TestNetworkInterface>()
+
+    fun createTapInterface(): TestNetworkInterface {
+        return runAsShell(MANAGE_TEST_NETWORKS) {
+            tnm.createTapInterface()
+        }.also {
+            ifaces.add(it)
+        }
+    }
+
+    private fun closeAllInterfaces() {
+        // TODO: wait on RTM_DELLINK before proceeding.
+        for (iface in ifaces) {
+            // ParcelFileDescriptor prevents the fd from being double closed.
+            iface.getFileDescriptor().close()
+        }
+    }
+
+    private inner class AutoCloseTestInterfaceRuleStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                closeAllInterfaces()
+            }
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return AutoCloseTestInterfaceRuleStatement(base, description)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
index 4a594e6..dd52d0b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -88,7 +88,7 @@
         }
         cm.registerDefaultNetworkCallback(cb)
         try {
-            val cap = capFuture.get(100, TimeUnit.MILLISECONDS)
+            val cap = capFuture.get(10_000, TimeUnit.MILLISECONDS)
             initialTransports = BitUtils.packBits(cap.transportTypes)
         } catch (e: Exception) {
             firstFailure = IllegalStateException(
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
new file mode 100644
index 0000000..7203265
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -0,0 +1,80 @@
+#  Copyright (C) 2024 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.
+
+from mobly import asserts
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+
+
+class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
+
+  def setup_class(self):
+    super().setup_class()
+
+    # Check test preconditions.
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    asserts.abort_class_if(
+        not apf_utils.is_send_raw_packet_downstream_supported(
+            self.serverDevice
+        ),
+        "NetworkStack is too old to support send raw packet, skip test.",
+    )
+
+    # Fetch device properties and storing them locally for later use.
+    client = self.clientDevice.connectivity_multi_devices_snippet
+    self.server_iface_name, client_network = (
+        tether_utils.setup_hotspot_and_client_for_upstream_type(
+            self.serverDevice, self.clientDevice, UpstreamType.NONE
+        )
+    )
+    self.client_iface_name = client.getInterfaceNameFromNetworkHandle(
+        client_network
+    )
+    self.server_mac_address = apf_utils.get_hardware_address(
+        self.serverDevice, self.server_iface_name
+    )
+
+    # Enable doze mode to activate APF.
+    adb_utils.set_doze_mode(self.clientDevice, True)
+
+  def teardown_class(self):
+    adb_utils.set_doze_mode(self.clientDevice, False)
+    tether_utils.cleanup_tethering_for_upstream_type(
+        self.serverDevice, UpstreamType.NONE
+    )
+
+  def send_packet_and_expect_counter_increased(
+      self, packet: str, counter_name: str
+  ) -> None:
+    count_before_test = apf_utils.get_apf_counter(
+        self.clientDevice,
+        self.client_iface_name,
+        counter_name,
+    )
+    apf_utils.send_raw_packet_downstream(
+        self.serverDevice, self.server_iface_name, packet
+    )
+
+    assert_utils.expect_with_retry(
+        lambda: apf_utils.get_apf_counter(
+            self.clientDevice,
+            self.client_iface_name,
+            counter_name,
+        )
+        > count_before_test
+    )
+
+    # TODO: Verify the packet is not actually received.
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 415799c..a3ec6e9 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -20,11 +20,6 @@
 from net_tests_utils.host.python import adb_utils, assert_utils
 
 
-# Constants.
-ETHER_BROADCAST = "FFFFFFFFFFFF"
-ETH_P_ETHERCAT = "88A4"
-
-
 class PatternNotFoundException(Exception):
   """Raised when the given pattern cannot be found."""
 
@@ -121,25 +116,17 @@
     )
 
 
-def send_broadcast_empty_ethercat_packet(
-    ad: android_device.AndroidDevice, iface_name: str
-) -> None:
-  """Transmits a broadcast empty EtherCat packet on the specified interface."""
-
-  # Get the interface's MAC address.
-  mac_address = get_hardware_address(ad, iface_name)
-
-  # TODO: Build packet by using scapy library.
-  # Ethernet header (14 bytes).
-  packet = ETHER_BROADCAST  # Destination MAC (broadcast)
-  packet += mac_address.replace(":", "")  # Source MAC
-  packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
-
-  # EtherCAT header (2 bytes) + 44 bytes of zero padding.
-  packet += "00" * 46
-
-  # Send the packet using a raw socket.
-  send_raw_packet_downstream(ad, iface_name, packet)
+def is_send_raw_packet_downstream_supported(
+    ad: android_device.AndroidDevice,
+) -> bool:
+  try:
+    # Invoke the shell command with empty argument and see how NetworkStack respond.
+    # If supported, an IllegalArgumentException with help page will be printed.
+    send_raw_packet_downstream(ad, "", "")
+  except assert_utils.UnexpectedBehaviorError:
+    return True
+  except UnsupportedOperationException:
+    return False
 
 
 def send_raw_packet_downstream(
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
new file mode 100644
index 0000000..f8a92f3
--- /dev/null
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -0,0 +1,54 @@
+#  Copyright (C) 2024 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.
+#
+#  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.
+
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class MultiDevicesTestBase(base_test.BaseTestClass):
+
+  def setup_class(self):
+    # Declare that two Android devices are needed.
+    self.clientDevice, self.serverDevice = self.register_controller(
+        android_device, min_number=2
+    )
+
+    def setup_device(device):
+      device.load_snippet(
+          "connectivity_multi_devices_snippet",
+          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+      )
+
+    # Set up devices in parallel to save time.
+    utils.concurrent_exec(
+        setup_device,
+        ((self.clientDevice,), (self.serverDevice,)),
+        max_workers=2,
+        raise_on_exception=True,
+    )
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index dc90adb..5f062f1 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -19,9 +19,11 @@
 
 python_test_host {
     name: "CtsConnectivityMultiDevicesTestCases",
-    main: "connectivity_multi_devices_test.py",
+    main: "run_tests.py",
     srcs: [
+        "apfv4_test.py",
         "connectivity_multi_devices_test.py",
+        "run_tests.py",
     ],
     libs: [
         "mobly",
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
new file mode 100644
index 0000000..4633d37
--- /dev/null
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -0,0 +1,35 @@
+#  Copyright (C) 2024 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.
+
+from net_tests_utils.host.python import apf_test_base
+
+# Constants.
+COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
+ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+ETH_P_ETHERCAT = "88A4"
+
+
+class ApfV4Test(apf_test_base.ApfTestBase):
+
+  def test_apf_drop_ethercat(self):
+    # Ethernet header (14 bytes).
+    packet = ETHER_BROADCAST_ADDR  # Destination MAC (broadcast)
+    packet += self.server_mac_address.replace(":", "")  # Source MAC
+    packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
+
+    # EtherCAT header (2 bytes) + 44 bytes of zero padding.
+    packet += "00" * 46
+    self.send_packet_and_expect_counter_increased(
+        packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
+    )
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 7e7bbf5..eceb535 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,40 +1,24 @@
-# Lint as: python3
-"""Connectivity multi devices tests."""
-import sys
+#  Copyright (C) 2024 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.
 
-from mobly import asserts
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, mdns_utils, tether_utils
+from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
 from net_tests_utils.host.python.tether_utils import UpstreamType
 
-CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
-COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
 
-
-class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
-
-  def setup_class(self):
-    # Declare that two Android devices are needed.
-    self.clientDevice, self.serverDevice = self.register_controller(
-        android_device, min_number=2
-    )
-
-    def setup_device(device):
-      device.load_snippet(
-          "connectivity_multi_devices_snippet",
-          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
-      )
-
-    # Set up devices in parallel to save time.
-    utils.concurrent_exec(
-        setup_device,
-        ((self.clientDevice,), (self.serverDevice,)),
-        max_workers=2,
-        raise_on_exception=True,
-    )
+class ConnectivityMultiDevicesTest(
+    multi_devices_test_base.MultiDevicesTestBase
+):
 
   def test_hotspot_upstream_wifi(self):
     tether_utils.assume_hotspot_test_preconditions(
@@ -84,54 +68,3 @@
       tether_utils.cleanup_tethering_for_upstream_type(
           self.serverDevice, UpstreamType.NONE
       )
-
-  def test_apf_drop_ethercat(self):
-    tether_utils.assume_hotspot_test_preconditions(
-        self.serverDevice, self.clientDevice, UpstreamType.NONE
-    )
-    client = self.clientDevice.connectivity_multi_devices_snippet
-    try:
-      server_iface_name, client_network = (
-          tether_utils.setup_hotspot_and_client_for_upstream_type(
-              self.serverDevice, self.clientDevice, UpstreamType.NONE
-          )
-      )
-      client_iface_name = client.getInterfaceNameFromNetworkHandle(client_network)
-
-      adb_utils.set_doze_mode(self.clientDevice, True)
-
-      count_before_test = apf_utils.get_apf_counter(
-          self.clientDevice,
-          client_iface_name,
-          COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED,
-      )
-      try:
-        apf_utils.send_broadcast_empty_ethercat_packet(
-            self.serverDevice, server_iface_name
-        )
-      except apf_utils.UnsupportedOperationException:
-        asserts.skip(
-            "NetworkStack is too old to support send raw packet, skip test."
-        )
-
-      assert_utils.expect_with_retry(
-          lambda: apf_utils.get_apf_counter(
-              self.clientDevice,
-              client_iface_name,
-              COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED,
-          )
-          > count_before_test
-      )
-    finally:
-      adb_utils.set_doze_mode(self.clientDevice, False)
-      tether_utils.cleanup_tethering_for_upstream_type(
-          self.serverDevice, UpstreamType.NONE
-      )
-
-
-if __name__ == "__main__":
-  # Take test args
-  if "--" in sys.argv:
-    index = sys.argv.index("--")
-    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
-  test_runner.main()
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
new file mode 100644
index 0000000..1391d13
--- /dev/null
+++ b/tests/cts/multidevices/run_tests.py
@@ -0,0 +1,38 @@
+#  Copyright (C) 2024 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.
+
+"""Main entrypoint for all of test cases."""
+
+import sys
+from apfv4_test import ApfV4Test
+from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+  # For MoblyBinaryHostTest, this entry point will be called twice:
+  # 1. List tests.
+  #   <mobly-par-file-name> -- --list_tests
+  # 2. Run tests.
+  #   <mobly-par-file-name> -- --config=<yaml-path> \
+  #      --device_serial=<device-serial> --log_path=<log-path>
+  # Strip the "--" since suite runner doesn't recognize it.
+  # While the parameters before "--" is for the infrastructure,
+  # ignore them if any. Also, do not alter parameters if there
+  # is no "--", in case the binary is invoked manually.
+  if "--" in sys.argv:
+    index = sys.argv.index("--")
+    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+  # TODO: make the tests can be executed without manually list classes.
+  suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index b5f43d3..e94d94f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -451,9 +451,10 @@
             long uidTxDelta = 0;
             long uidRxDelta = 0;
             for (int i = 0; i < 100; i++) {
-                // Clear TrafficStats cache is needed to avoid rate-limit caching for
-                // TrafficStats API results on V+ devices.
-                if (SdkLevel.isAtLeastV()) {
+                // Since the target SDK of this test should always equal or be larger than V,
+                // TrafficStats caching is always enabled. Clearing the cache is needed to
+                // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+                if (SdkLevel.isAtLeastT()) {
                     runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
                 }
                 uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
@@ -530,9 +531,10 @@
         }
 
         private static void initStatsChecker() throws Exception {
-            // Clear TrafficStats cache is needed to avoid rate-limit caching for
-            // TrafficStats API results on V+ devices.
-            if (SdkLevel.isAtLeastV()) {
+            // Since the target SDK of this test should always equal or be larger than V,
+            // TrafficStats caching is always enabled. Clearing the cache is needed to
+            // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+            if (SdkLevel.isAtLeastT()) {
                 runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
             }
             uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
index 621af23..f9acb66 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -19,7 +19,6 @@
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.content.Context
-import android.content.pm.PackageManager
 import android.net.ConnectivityManager
 import android.net.EthernetManager
 import android.net.InetAddresses
@@ -36,7 +35,6 @@
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
 import android.net.dhcp.DhcpRequestPacket
-import android.os.Build
 import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.platform.app.InstrumentationRegistry
@@ -44,7 +42,7 @@
 import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
 import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.DhcpClientPacketFilter
 import com.android.testutils.DhcpOptionFilter
 import com.android.testutils.RecorderCallback.CallbackEntry
@@ -79,10 +77,6 @@
 @AppModeFull(reason = "Instant apps cannot create test networks")
 @RunWith(AndroidJUnit4::class)
 class NetworkValidationTest {
-    @JvmField
-    @Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
     private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
@@ -104,23 +98,24 @@
 
     private var testSkipped = false
 
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     fun setUp() {
         // This test requires using a tap interface as an ethernet interface.
-        val pm = context.getPackageManager()
-        testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
-                context.getSystemService(EthernetManager::class.java) == null
+        testSkipped = context.getSystemService(EthernetManager::class.java) == null
         assumeFalse(testSkipped)
 
         // Register a request so the network does not get torn down
         cm.requestNetwork(ethRequest, ethRequestCb)
         runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
             eth.setIncludeTestInterfaces(true)
-            // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
-            // does not go out of scope, which would cause it to close the underlying FileDescriptor
-            // in its finalizer.
-            iface = tnm.createTapInterface()
         }
+        // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+        // does not go out of scope, which would cause it to close the underlying FileDescriptor
+        // in its finalizer.
+        iface = testInterfaceRule.createTapInterface()
 
         handlerThread.start()
         reader = TapPacketReader(
@@ -147,8 +142,6 @@
         handlerThread.threadHandler.post { reader.stop() }
         handlerThread.quitSafely()
         handlerThread.join()
-
-        iface.fileDescriptor.close()
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f45f881..24af42b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,19 +17,21 @@
 
 import android.net.EthernetTetheringTestBase
 import android.net.LinkAddress
-import android.net.TestNetworkInterface
 import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
 import android.net.TetheringManager.TETHERING_ETHERNET
 import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
 import android.net.nsd.NsdManager
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.NsdDiscoveryRecord
-import com.android.testutils.TapPacketReader
 import com.android.testutils.pollForQuery
 import com.android.testutils.tryTest
 import java.util.Random
@@ -38,9 +40,12 @@
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @ConnectivityModuleTest
@@ -50,14 +55,29 @@
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
 
+    private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var downstreamIface: EthernetTestInterface
+    private var tetheringEventCallback: MyTetheringEventCallback? = null
+
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     override fun setUp() {
         super.setUp()
-        setIncludeTestInterfaces(true)
+        val iface = testInterfaceRule.createTapInterface()
+        downstreamIface = EthernetTestInterface(context, handler, iface)
     }
 
     @After
     override fun tearDown() {
+        if (::downstreamIface.isInitialized) {
+            downstreamIface.destroy()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+        maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         super.tearDown()
     }
 
@@ -65,16 +85,11 @@
     fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
-
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
             tetheringEventCallback = enableEthernetTethering(
@@ -83,23 +98,15 @@
             ).apply {
                 awaitInterfaceLocalOnly()
             }
-            // This shouldn't be flaky because the TAP interface will buffer all packets even
-            // before the reader is started.
-            downstreamReader = makePacketReader(downstreamIface)
+            val downstreamReader = downstreamIface.packetReader
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
 
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 
@@ -107,16 +114,11 @@
     fun testMdnsDiscoveryWorkOnTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
-
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
 
             val localAddr = LinkAddress("192.0.2.3/28")
             val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,23 +132,14 @@
                 awaitInterfaceTethered()
             }
 
-            val fd = downstreamIface?.fileDescriptor?.fileDescriptor
-            assertNotNull(fd)
-            downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
-
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            val downstreamReader = downstreamIface.packetReader
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
             // TODO: Add another test to check packet reply can trigger serviceFound.
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index be80787..5c1099d 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -105,7 +105,6 @@
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.assertContainsExactly
 import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
@@ -128,6 +127,7 @@
 import java.util.Random
 import java.util.concurrent.Executor
 import kotlin.math.min
+import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertNotNull
@@ -2471,6 +2471,44 @@
         }
     }
 
+    @Test
+    fun testAdvertiseServiceWithNoAttributes_TxtRecordIstNotEmpty() {
+        deviceConfigRule.setConfig(
+            NAMESPACE_TETHERING,
+            "test_nsd_avoid_advertising_empty_txt_records",
+            "1"
+        )
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+            testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        // Test behavior described in RFC6763 6.1: empty TXT records are not allowed, but TXT
+        // records with a zero length string are equivalent.
+        val si = makeTestServiceInfo(testNetwork1.network)
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+
+        tryTest {
+            val announcement =
+                packetReader.pollForReply("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT)
+            assertNotNull(announcement)
+            val txtRecords = announcement.records[ANSECTION].filter {
+                it.nsType == DnsResolver.TYPE_TXT
+            }
+            assertEquals(1, txtRecords.size)
+            // The TXT record should contain as single zero
+            assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
         return clients.any { client -> client.substring(
                 client.indexOf("network=") + "network=".length,
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..32d6899
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 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.cts.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TapPacketReader
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
+import kotlin.concurrent.Volatile
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+    private val context: Context,
+    private val handler: Handler,
+    val testIface: TestNetworkInterface
+) {
+    private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+        val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+        sealed class CallbackEntry {
+            data class InterfaceStateChanged(
+                val iface: String,
+                val state: Int,
+                val role: Int,
+                val cfg: IpConfiguration?
+            ) : CallbackEntry()
+        }
+
+        override fun onInterfaceStateChanged(
+            iface: String,
+            state: Int,
+            role: Int,
+            cfg: IpConfiguration?
+        ) {
+            // filter out callbacks for other interfaces
+            if (iface != trackedIface) return
+            events.add(InterfaceStateChanged(iface, state, role, cfg))
+        }
+
+        fun eventuallyExpect(state: Int) {
+            val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+            assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+        }
+    }
+
+    val name get() = testIface.interfaceName
+    val mtu: Int
+        get() {
+            val nif = NetworkInterface.getByName(name)
+            assertNotNull(nif)
+            return nif.mtu
+        }
+    val packetReader = TapPacketReader(handler, testIface.fileDescriptor.fileDescriptor, mtu)
+    private val listener = EthernetStateListener(name)
+    private val em = context.getSystemService(EthernetManager::class.java)!!
+    @Volatile private var cleanedUp = false
+
+    init{
+        em.addInterfaceStateListener(handler::post, listener)
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(true)
+        }
+        // Wait for link up to be processed in EthernetManager before returning.
+        listener.eventuallyExpect(STATE_LINK_UP)
+        handler.post { packetReader.start() }
+        handler.waitForIdle(TIMEOUT_MS)
+    }
+
+    fun destroy() {
+        // packetReader.stop() closes the test interface.
+        handler.post { packetReader.stop() }
+        handler.waitForIdle(TIMEOUT_MS)
+        listener.eventuallyExpect(STATE_ABSENT)
+
+        // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+        // there should be no need for a synchronization mechanism here. If the next test is
+        // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+        // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(false)
+        }
+        em.removeInterfaceStateListener(listener)
+
+        cleanedUp = true
+    }
+
+    protected fun finalize() {
+        if (!cleanedUp) {
+            Log.wtf(TAG, "destroy() was not called for interface $name.")
+            destroy()
+        }
+    }
+}
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index ab2f28c..39a08fa 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -21,6 +21,7 @@
     stl: "libc++_static",
     shared_libs: [
         "libbinder_ndk",
+        "libcom.android.tethering.connectivity_native",
         "liblog",
         "libnetutils",
         "libprocessgroup",
diff --git a/tests/native/connectivity_native_test/AndroidTestTemplate.xml b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
index 44e35a9..4ea114c 100644
--- a/tests/native/connectivity_native_test/AndroidTestTemplate.xml
+++ b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
@@ -15,8 +15,10 @@
 <configuration description="Configuration for connectivity {MODULE} tests">
     <option name="test-suite-tag" value="mts" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
-    <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+    <!-- The tested library is only usable on U+ -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+    <!-- The tested library is only available with the primary abi -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
 
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index f62a30b..faaf150 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -14,25 +14,17 @@
  * limitations under the License.
  */
 
+#include <android-modules-utils/sdk_level.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
-#include <android-modules-utils/sdk_level.h>
-#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <connectivity_native.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
 #include <dlfcn.h>
 #include <gtest/gtest.h>
 #include <netinet/in.h>
 
 #include "bpf/BpfUtils.h"
 
-typedef int (*GetPortsBlockedForBind)(in_port_t*, size_t*);
-GetPortsBlockedForBind getPortsBlockedForBind;
-typedef int (*BlockPortForBind)(in_port_t);
-BlockPortForBind blockPortForBind;
-typedef int (*UnblockPortForBind)(in_port_t);
-UnblockPortForBind unblockPortForBind;
-typedef int (*UnblockAllPortsForBind)();
-UnblockAllPortsForBind unblockAllPortsForBind;
-
 class ConnectivityNativeBinderTest : public ::testing::Test {
   public:
     in_port_t mActualBlockedPorts[65535];
@@ -50,29 +42,12 @@
         if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
             GTEST_SKIP() << "Kernel should be at least 5.4.";
 
-        // Necessary to use dlopen/dlsym since the lib is only available on U and there
-        // is no Sdk34ModuleController in tradefed yet.
-        // TODO: link against the library directly and add Sdk34ModuleController to
-        // AndroidTest.txml when available.
-        void* nativeLib = dlopen("libcom.android.tethering.connectivity_native.so", RTLD_NOW);
-        ASSERT_NE(nullptr, nativeLib);
-        getPortsBlockedForBind = reinterpret_cast<GetPortsBlockedForBind>(
-                dlsym(nativeLib, "AConnectivityNative_getPortsBlockedForBind"));
-        ASSERT_NE(nullptr, getPortsBlockedForBind);
-        blockPortForBind = reinterpret_cast<BlockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_blockPortForBind"));
-        ASSERT_NE(nullptr, blockPortForBind);
-        unblockPortForBind = reinterpret_cast<UnblockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockPortForBind"));
-        ASSERT_NE(nullptr, unblockPortForBind);
-        unblockAllPortsForBind = reinterpret_cast<UnblockAllPortsForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockAllPortsForBind"));
-        ASSERT_NE(nullptr, unblockAllPortsForBind);
-
-        // If there are already ports being blocked on device unblockAllPortsForBind() store
-        // the currently blocked ports and add them back at the end of the test. Do this for
-        // every test case so additional test cases do not forget to add ports back.
-        int err = getPortsBlockedForBind(mActualBlockedPorts, &mActualBlockedPortsCount);
+        // If there are already ports being blocked on device store the
+        // currently blocked ports and add them back at the end of the test. Do
+        // this for every test case so additional test cases do not forget to
+        // add ports back.
+        int err = AConnectivityNative_getPortsBlockedForBind(
+            mActualBlockedPorts, &mActualBlockedPortsCount);
         EXPECT_EQ(err, 0);
         restoreBlockedPorts = true;
     }
@@ -81,8 +56,9 @@
         int err;
         if (mActualBlockedPortsCount > 0 && restoreBlockedPorts) {
             for (int i=0; i < mActualBlockedPortsCount; i++) {
-                err = blockPortForBind(mActualBlockedPorts[i]);
-                EXPECT_EQ(err, 0);
+              err =
+                  AConnectivityNative_blockPortForBind(mActualBlockedPorts[i]);
+              EXPECT_EQ(err, 0);
             }
         }
     }
@@ -99,7 +75,7 @@
         int blockedPort = 0;
         if (blockPort) {
             blockedPort = ntohs(port);
-            err = blockPortForBind(blockedPort);
+            err = AConnectivityNative_blockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         }
 
@@ -107,7 +83,7 @@
 
         if (blockPort) {
             EXPECT_EQ(-1, sock3);
-            err = unblockPortForBind(blockedPort);
+            err = AConnectivityNative_unblockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         } else {
             EXPECT_NE(-1, sock3);
@@ -197,11 +173,11 @@
 }
 
 TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
-    int err = blockPortForBind(5555);
+    int err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = blockPortForBind(5555);
+    err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = unblockPortForBind(5555);
+    err = AConnectivityNative_unblockPortForBind(5555);
     EXPECT_EQ(err, 0);
 }
 
@@ -210,16 +186,17 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_NE(actualBlockedPortsCount, 0);
     for (int i=0; i < actualBlockedPortsCount; i++) {
@@ -227,9 +204,10 @@
     }
 
     // Remove the ports we added.
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind(actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(actualBlockedPorts,
+                                                     &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
 }
@@ -239,23 +217,25 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
 
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 8);
 
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
     // If mActualBlockedPorts is not empty, ports will be added back in teardown.
@@ -264,7 +244,7 @@
 TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
     int curUid = getuid();
     EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
-    int err = blockPortForBind((in_port_t) 5555);
+    int err = AConnectivityNative_blockPortForBind((in_port_t)5555);
     EXPECT_EQ(EPERM, err);
     EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
 }
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 859c54a..c1c15ca 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -41,6 +41,7 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
 import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
 import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
@@ -136,6 +137,12 @@
 
     private static final int TEST_UID = 10086;
     private static final int[] TEST_UIDS = {10002, 10003};
+    private static final int[] CORE_AIDS = {
+            Process.ROOT_UID,
+            Process.SYSTEM_UID,
+            Process.FIRST_APPLICATION_UID - 10,
+            Process.FIRST_APPLICATION_UID - 1,
+    };
     private static final String TEST_IF_NAME = "wlan0";
     private static final int TEST_IF_INDEX = 7;
     private static final int NO_IIF = 0;
@@ -1261,15 +1268,9 @@
         assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
                 mUidOwnerMap, mDataSaverEnabledMap));
 
-        final int[] coreAids = new int[] {
-                Process.ROOT_UID,
-                Process.SYSTEM_UID,
-                Process.FIRST_APPLICATION_UID - 10,
-                Process.FIRST_APPLICATION_UID - 1,
-        };
         // Core appIds are not on the chain but should still be allowed on any user.
         for (int userId = 0; userId < 20; userId++) {
-            for (final int aid : coreAids) {
+            for (final int aid : CORE_AIDS) {
                 final int uid = UserHandle.getUid(userId, aid);
                 assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
                         mUidOwnerMap, mDataSaverEnabledMap));
@@ -1277,6 +1278,26 @@
         }
     }
 
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidNetworkingBlockedReasonsForCoreUids() throws Exception {
+        // Enable BACKGROUND_MATCH that is an allowlist match.
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(BACKGROUND_MATCH));
+
+        // Non-core uid that is not on this chain is blocked by BLOCKED_REASON_APP_BACKGROUND.
+        assertEquals(BLOCKED_REASON_APP_BACKGROUND, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                TEST_UID, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+
+        // Core appIds are not on the chain but should not be blocked on any users.
+        for (int userId = 0; userId < 20; userId++) {
+            for (final int aid : CORE_AIDS) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertEquals(BLOCKED_REASON_NONE, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                        uid, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+            }
+        }
+    }
+
     private void doTestIsUidRestrictedOnMeteredNetworks(
             final long enabledMatches,
             final long uidRules,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 2cb97c9..8c44abd 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -172,11 +172,13 @@
     private fun makeFlags(
         includeInetAddressesInProbing: Boolean = false,
         isKnownAnswerSuppressionEnabled: Boolean = false,
-        unicastReplyEnabled: Boolean = true
+        unicastReplyEnabled: Boolean = true,
+        avoidAdvertisingEmptyTxtRecords: Boolean = true
     ) = MdnsFeatureFlags.Builder()
         .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
         .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
         .setIsUnicastReplyEnabled(unicastReplyEnabled)
+        .setAvoidAdvertisingEmptyTxtRecords(avoidAdvertisingEmptyTxtRecords)
         .build()
 
     @Test
@@ -1721,6 +1723,30 @@
     }
 
     @Test
+    fun testGetConflictingServices_ZeroLengthTxtRecord_NoConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(
+                    MdnsTextRecord(
+                        arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        listOf(TextEntry("", null as ByteArray?))
+                    ),
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+
+        assertEquals(emptyMap(), repository.getConflictingServices(packet))
+    }
+
+    @Test
     fun testGetServiceRepliedRequestsCount() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -2168,6 +2194,46 @@
         assertEquals(knownAnswers, reply.knownAnswers)
     }
 
+    private fun doAddServiceWithEmptyTxtRecordTest(flags: MdnsFeatureFlags): MdnsTextRecord {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val questions = listOf(MdnsTextRecord(
+            arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+            true /* isUnicast */
+        ))
+        val query = MdnsPacket(
+            0 /* flags */,
+            questions,
+            emptyList() /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(1, reply.answers.size)
+        assertTrue(reply.answers[0] is MdnsTextRecord)
+        return reply.answers[0] as MdnsTextRecord
+    }
+
+    @Test
+    fun testAddService_AvoidEmptyTxtRecords_HasTxtRecordWithEmptyString() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags())
+        assertEquals(1, answerRecord.entries.size)
+        assertEquals(0, answerRecord.entries[0].key.length)
+        assertNull(answerRecord.entries[0].value)
+    }
+
+    @Test
+    fun testAddService_UsesEmptyTxtRecords_HasEmptyTxtRecord() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags(
+            avoidAdvertisingEmptyTxtRecords = false
+        ))
+        assertEquals(0, answerRecord.entries.size)
+    }
+
     @Test
     fun testRestartProbingForHostname() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index 63548c1..784c502 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -28,6 +28,8 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import static java.util.Collections.emptyList;
+
 import android.util.Log;
 
 import com.android.net.module.util.HexDump;
@@ -372,6 +374,30 @@
         assertEquals(dataInText, dataOutText);
     }
 
+    private static MdnsTextRecord makeTextRecordWithEntries(List<TextEntry> entries) {
+        return new MdnsTextRecord(new String[] { "test", "record" }, 0L /* receiptTimeMillis */,
+                true /* cacheFlush */, 120_000L /* ttlMillis */, entries);
+    }
+
+    @Test
+    public void testTextRecord_EmptyRecordsAreEquivalent() {
+        final MdnsTextRecord record1 = makeTextRecordWithEntries(emptyList());
+        final MdnsTextRecord record2 = makeTextRecordWithEntries(
+                List.of(new TextEntry("", (byte[]) null)));
+        final MdnsTextRecord record3 = makeTextRecordWithEntries(
+                List.of(new TextEntry(null, (byte[]) null)));
+        final MdnsTextRecord nonEmptyRecord = makeTextRecordWithEntries(
+                List.of(new TextEntry("a", (byte[]) null)));
+
+        assertEquals(record1, record1);
+        assertEquals(record1, record2);
+        assertEquals(record1, record3);
+
+        assertNotEquals(nonEmptyRecord, record1);
+        assertNotEquals(nonEmptyRecord, record2);
+        assertNotEquals(nonEmptyRecord, record3);
+    }
+
     private static String toHex(MdnsRecord record) throws IOException {
         MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
         record.write(writer, record.getReceiptTime());
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e09b3a6..be2632c 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -15,7 +15,9 @@
  */
 package android.net.thread;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -37,19 +39,19 @@
  * @see ThreadNetworkController#unregisterConfigurationCallback
  * @hide
  */
-// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-// @SystemApi
+@FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
 public final class ThreadConfiguration implements Parcelable {
     private final boolean mNat64Enabled;
-    private final boolean mDhcp6PdEnabled;
+    private final boolean mDhcpv6PdEnabled;
 
     private ThreadConfiguration(Builder builder) {
-        this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+        this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
     }
 
-    private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
         this.mNat64Enabled = nat64Enabled;
-        this.mDhcp6PdEnabled = dhcp6PdEnabled;
+        this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
     }
 
     /** Returns {@code true} if NAT64 is enabled. */
@@ -58,8 +60,8 @@
     }
 
     /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
-    public boolean isDhcp6PdEnabled() {
-        return mDhcp6PdEnabled;
+    public boolean isDhcpv6PdEnabled() {
+        return mDhcpv6PdEnabled;
     }
 
     @Override
@@ -71,13 +73,13 @@
         } else {
             ThreadConfiguration otherConfig = (ThreadConfiguration) other;
             return mNat64Enabled == otherConfig.mNat64Enabled
-                    && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+                    && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+        return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
     }
 
     @Override
@@ -85,7 +87,7 @@
         StringBuilder sb = new StringBuilder();
         sb.append('{');
         sb.append("Nat64Enabled=").append(mNat64Enabled);
-        sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+        sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
         sb.append('}');
         return sb.toString();
     }
@@ -98,7 +100,7 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeBoolean(mNat64Enabled);
-        dest.writeBoolean(mDhcp6PdEnabled);
+        dest.writeBoolean(mDhcpv6PdEnabled);
     }
 
     public static final @NonNull Creator<ThreadConfiguration> CREATOR =
@@ -107,7 +109,7 @@
                 public ThreadConfiguration createFromParcel(Parcel in) {
                     ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
                     builder.setNat64Enabled(in.readBoolean());
-                    builder.setDhcp6PdEnabled(in.readBoolean());
+                    builder.setDhcpv6PdEnabled(in.readBoolean());
                     return builder.build();
                 }
 
@@ -117,10 +119,14 @@
                 }
             };
 
-    /** The builder for creating {@link ThreadConfiguration} objects. */
+    /**
+     * The builder for creating {@link ThreadConfiguration} objects.
+     *
+     * @hide
+     */
     public static final class Builder {
         private boolean mNat64Enabled = false;
-        private boolean mDhcp6PdEnabled = false;
+        private boolean mDhcpv6PdEnabled = false;
 
         /** Creates a new {@link Builder} object with all features disabled. */
         public Builder() {}
@@ -134,7 +140,7 @@
             Objects.requireNonNull(config);
 
             mNat64Enabled = config.mNat64Enabled;
-            mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+            mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
         }
 
         /**
@@ -156,8 +162,8 @@
          * IPv6.
          */
         @NonNull
-        public Builder setDhcp6PdEnabled(boolean enabled) {
-            this.mDhcp6PdEnabled = enabled;
+        public Builder setDhcpv6PdEnabled(boolean enabled) {
+            this.mDhcpv6PdEnabled = enabled;
             return this;
         }
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 30b3d6a..b4e581c 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -619,16 +619,15 @@
     /**
      * Registers a callback to be called when the configuration is changed.
      *
-     * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+     * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
      * {@link ThreadConfiguration}.
      *
      * @param executor the executor to execute the {@code callback}
      * @param callback the callback to receive Thread configuration changes
      * @throws IllegalArgumentException if {@code callback} has already been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void registerConfigurationCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<ThreadConfiguration> callback) {
@@ -656,10 +655,9 @@
      * @param callback the callback which has been registered with {@link
      *     #registerConfigurationCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
-     * @hide
      */
-    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
-    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
         requireNonNull(callback, "callback cannot be null");
         synchronized (mConfigurationCallbackMapLock) {
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e6f272b..19084c6 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -271,10 +271,12 @@
     }
 
     private NetworkRequest newUpstreamNetworkRequest() {
-        NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
 
         if (mUpstreamTestNetworkSpecifier != null) {
-            return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            // Test networks don't have NET_CAPABILITY_TRUSTED
+            return builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                     .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
                     .build();
         }
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 747cc96..7c4c72d 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -197,7 +197,7 @@
             return false;
         }
         putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
-        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
         writeToStoreFile();
         return true;
     }
@@ -206,7 +206,7 @@
     public ThreadConfiguration getConfiguration() {
         return new ThreadConfiguration.Builder()
                 .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
-                .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
                 .build();
     }
 
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..6db7c9c 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
 
 android_test {
     name: "CtsThreadNetworkTestCases",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
     min_sdk_version: "33",
-    sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     srcs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..386412e
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    public final boolean mIsNat64Enabled;
+    public final boolean mIsDhcpv6PdEnabled;
+
+    @Parameterized.Parameters
+    public static Collection configArguments() {
+        return Arrays.asList(
+                new Object[][] {
+                    {false, false}, // All disabled
+                    {true, false}, // NAT64 enabled
+                    {false, true}, // DHCP6-PD enabled
+                    {true, true}, // All enabled
+                });
+    }
+
+    public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+        mIsNat64Enabled = isNat64Enabled;
+        mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        assertParcelingIsLossless(config);
+    }
+
+    @Test
+    public void builder_correctValuesAreSet() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+
+        assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+        assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+    }
+
+    @Test
+    public void builderConstructor_configsAreEqual() {
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+        assertThat(config1).isEqualTo(config2);
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 11c4819..1a101b6 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -58,6 +58,7 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
 import android.net.thread.ThreadNetworkController.StateCallback;
@@ -95,9 +96,11 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /** CTS tests for {@link ThreadNetworkController}. */
@@ -110,11 +113,14 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+    private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
     private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
     private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
+    private static final ThreadConfiguration DEFAULT_CONFIG =
+            new ThreadConfiguration.Builder().build();
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
@@ -127,6 +133,9 @@
     private HandlerThread mHandlerThread;
     private TapTestNetworkTracker mTestNetworkTracker;
 
+    private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+            new ArrayList<>();
+
     @Before
     public void setUp() throws Exception {
         mController =
@@ -141,6 +150,7 @@
         mHandlerThread.start();
 
         setEnabledAndWait(mController, true);
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
     }
 
     @After
@@ -148,6 +158,18 @@
         dropAllPermissions();
         leaveAndWait(mController);
         tearDownTestNetwork();
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
+        for (Consumer<ThreadConfiguration> configurationCallback :
+                mConfigurationCallbacksToCleanUp) {
+            try {
+                runAsShell(
+                        THREAD_NETWORK_PRIVILEGED,
+                        () -> mController.unregisterConfigurationCallback(configurationCallback));
+            } catch (IllegalArgumentException e) {
+                // Ignore the exception when the callback is not registered.
+            }
+        }
+        mConfigurationCallbacksToCleanUp.clear();
     }
 
     @Test
@@ -832,6 +854,152 @@
                         NET_CAPABILITY_TRUSTED);
     }
 
+    @Test
+    public void setConfiguration_null_throwsNullPointerException() throws Exception {
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mController.setConfiguration(
+                                null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+    }
+
+    @Test
+    public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> {
+                    mController.setConfiguration(
+                            configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+                });
+    }
+
+    @Test
+    public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+            throws Exception {
+        CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+        Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+        assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                .isEqualTo(DEFAULT_CONFIG);
+    }
+
+    @Test
+    public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+    }
+
+    @Test
+    public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        ConfigurationListener listener = new ConfigurationListener(mController);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config1, mExecutor, newOutcomeReceiver(setFuture1)));
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config2, mExecutor, newOutcomeReceiver(setFuture2)));
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            listener.expectConfiguration(DEFAULT_CONFIG);
+            listener.expectConfiguration(config1);
+            listener.expectConfiguration(config2);
+            listener.expectNoMoreConfiguration();
+        } finally {
+            listener.unregisterConfigurationCallback();
+        }
+    }
+
+    @Test
+    public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    registerConfigurationCallback(mController, mExecutor, callback);
+                    mController.unregisterConfigurationCallback(callback);
+                });
+    }
+
+    @Test
+    public void
+            unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+                    throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+        mController.unregisterConfigurationCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
     private void grantPermissions(String... permissions) {
         for (String permission : permissions) {
             mGrantedPermissions.add(permission);
@@ -1038,6 +1206,35 @@
         }
     }
 
+    private class ConfigurationListener {
+        private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+                mConfigurations.newReadHead();
+        ThreadNetworkController mController;
+        Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+        ConfigurationListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+        }
+
+        public void expectConfiguration(ThreadConfiguration config) {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+        }
+
+        public void expectNoMoreConfiguration() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+        }
+
+        public void unregisterConfigurationCallback() {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterConfigurationCallback(mCallback));
+        }
+    }
+
     private int booleanToEnabledState(boolean enabled) {
         return enabled ? STATE_ENABLED : STATE_DISABLED;
     }
@@ -1052,6 +1249,18 @@
         waitForEnabledState(controller, booleanToEnabledState(enabled));
     }
 
+    private void setConfigurationAndWait(
+            ThreadNetworkController controller, ThreadConfiguration configuration)
+            throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.setConfiguration(
+                                configuration, mExecutor, newOutcomeReceiver(setFuture)));
+        setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
     private CompletableFuture joinRandomizedDataset(
             ThreadNetworkController controller, String networkName) throws Exception {
         ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1118,6 +1327,14 @@
         };
     }
 
+    private void registerConfigurationCallback(
+            ThreadNetworkController controller,
+            Executor executor,
+            Consumer<ThreadConfiguration> callback) {
+        controller.registerConfigurationCallback(executor, callback);
+        mConfigurationCallbacksToCleanUp.add(callback);
+    }
+
     private static void assertDoesNotThrow(ThrowingRunnable runnable) {
         try {
             runnable.run();
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index eaf11b1..df1a65b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.thread;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -38,6 +39,7 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -56,7 +58,9 @@
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.NetworkAgent;
+import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
 import android.net.thread.IOperationReceiver;
@@ -181,6 +185,9 @@
                 .when(mContext)
                 .enforceCallingOrSelfPermission(
                         eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+        doNothing()
+                .when(mContext)
+                .enforceCallingOrSelfPermission(eq(NETWORK_SETTINGS), anyString());
 
         mTestLooper = new TestLooper();
         final Handler handler = new Handler(mTestLooper.getLooper());
@@ -716,12 +723,12 @@
         ThreadConfiguration config1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         ThreadConfiguration config2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         ThreadConfiguration config3 =
                 new ThreadConfiguration.Builder(config2).build(); // Same as config2
@@ -737,4 +744,34 @@
         inOrder.verify(mockReceiver2).onSuccess();
         inOrder.verify(mockReceiver3).onSuccess();
     }
+
+    @Test
+    public void setTestNetworkAsUpstream_upstreamNetworkRequestAlwaysDisallowsVpn() {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockConnectivityManager);
+
+        final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+        mService.setTestNetworkAsUpstream("test-network", mockReceiver1);
+        mService.setTestNetworkAsUpstream(null, mockReceiver2);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mMockConnectivityManager, times(2))
+                .registerNetworkCallback(
+                        networkRequestCaptor.capture(),
+                        any(ConnectivityManager.NetworkCallback.class),
+                        any(Handler.class));
+        assertThat(networkRequestCaptor.getAllValues().size()).isEqualTo(2);
+        NetworkRequest networkRequest1 = networkRequestCaptor.getAllValues().get(0);
+        NetworkRequest networkRequest2 = networkRequestCaptor.getAllValues().get(1);
+        assertThat(networkRequest1.getNetworkSpecifier()).isNotNull();
+        assertThat(networkRequest1.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN))
+                .isTrue();
+        assertThat(networkRequest2.getNetworkSpecifier()).isNull();
+        assertThat(networkRequest2.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN))
+                .isTrue();
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index c932ac8..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -152,7 +152,7 @@
         ThreadConfiguration configuration =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
@@ -164,13 +164,13 @@
         ThreadConfiguration configuration1 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(false)
-                        .setDhcp6PdEnabled(false)
+                        .setDhcpv6PdEnabled(false)
                         .build();
         mThreadPersistentSettings.putConfiguration(configuration1);
         ThreadConfiguration configuration2 =
                 new ThreadConfiguration.Builder()
                         .setNat64Enabled(true)
-                        .setDhcp6PdEnabled(true)
+                        .setDhcpv6PdEnabled(true)
                         .build();
 
         assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
@@ -188,9 +188,9 @@
     }
 
     @Test
-    public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+    public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
         ThreadConfiguration configuration =
-                new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+                new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
         mThreadPersistentSettings.putConfiguration(configuration);
 
         assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);