Merge "Changing the targetSdkVersion for RESTRICT_LOCAL_NETWORK" into main
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index f11b9a3..4c47f83 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -769,7 +769,7 @@
     const size_t max_name = 256;
     char kvTypeName[max_name];
     int64_t keySize, valueSize;
-    uint32_t kvId;
+    int32_t kvId;
 
     if (snprintf(kvTypeName, max_name, "____btf_map_%s", mapName) == max_name) {
         ALOGE("____btf_map_%s is too long", mapName);
@@ -856,20 +856,24 @@
     ret = getSectionSymNames(elfFile, "maps", mapNames);
     if (ret) return ret;
 
-    ret = readSectionByName(".BTF", elfFile, btfData);
-    if (ret) {
-        ALOGE("Failed to read .BTF section, ret:%d", ret);
-        return ret;
-    }
-    struct btf *btf = btf__new(btfData.data(), btfData.size());
-    if (btf == NULL) {
-        ALOGE("btf__new failed, errno: %d", errno);
-        return -errno;
-    }
-    auto scopeGuard = base::make_scope_guard([btf] { btf__free(btf); });
+    struct btf *btf = NULL;
+    auto scopeGuard = base::make_scope_guard([btf] { if (btf) btf__free(btf); });
+    if (isAtLeastKernelVersion(4, 18, 0)) {
+        // On Linux Kernels older than 4.18 BPF_BTF_LOAD command doesn't exist.
+        ret = readSectionByName(".BTF", elfFile, btfData);
+        if (ret) {
+            ALOGE("Failed to read .BTF section, ret:%d", ret);
+            return ret;
+        }
+        struct btf *btf = btf__new(btfData.data(), btfData.size());
+        if (btf == NULL) {
+            ALOGE("btf__new failed, errno: %d", errno);
+            return -errno;
+        }
 
-    ret = loadBtf(elfFile, btf);
-    if (ret) return ret;
+        ret = loadBtf(elfFile, btf);
+        if (ret) return ret;
+    }
 
     unsigned kvers = kernelVersion();
 
@@ -997,7 +1001,7 @@
             if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
 
-            bool haveBtf = isBtfSupported(type);
+            bool haveBtf = btf && isBtfSupported(type);
             if (haveBtf) {
                 uint32_t kTid, vTid;
                 ret = getKeyValueTids(btf, mapNames[i].c_str(), md[i].key_size,
diff --git a/bpf/loader/netbpfload.rc b/bpf/loader/netbpfload.rc
index 10bfbb2..4cc6284 100644
--- a/bpf/loader/netbpfload.rc
+++ b/bpf/loader/netbpfload.rc
@@ -1,3 +1,5 @@
+# 2025 2 36 0 0 # 25q2 sdk/api level 36.0 - Android 16 Baklava QPR0
+
 # Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
 # by virtue of 'service bpfloader' being overridden by the apex shipped .rc
 # Warning: most of the below settings are irrelevant unless the apex is missing.
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index d41aa81..680c05e 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -268,6 +268,16 @@
     RETURN_IF_NOT_OK(initMaps());
 
     if (isAtLeast25Q2) {
+        struct rlimit limit = {
+            .rlim_cur = 1u << 30,  // 1 GiB
+            .rlim_max = 1u << 30,  // 1 GiB
+        };
+        // 25Q2 netd.rc includes "rlimit memlock 1073741824 1073741824"
+        // so this should be a no-op, and thus just succeed.
+        // make sure it isn't lowered in platform netd.rc...
+        if (setrlimit(RLIMIT_MEMLOCK, &limit))
+            return statusFromErrno(errno, "Failed to set 1GiB RLIMIT_MEMLOCK");
+
         // Make sure netd can create & write maps.  sepolicy is V+, but enough to enforce on 25Q2+
         int key = 1;
         int value = 123;
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
index 95265b9..48e8b06 100644
--- a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -61,12 +61,46 @@
 
     @Override
     public String toString() {
-        return "LocalNetAccessKey{"
-                + "lpmBitlen=" + lpmBitlen
-                + ", ifIndex=" + ifIndex
-                + ", remoteAddress=" + remoteAddress
-                + ", protocol=" + protocol
-                + ", remotePort=" + remotePort
-                + "}";
+        String s = "LocalNetAccessKey{lpmBitlen=" + lpmBitlen;
+
+        long bits = lpmBitlen;
+
+        // u32 ifIndex
+        if (bits <= 0 && ifIndex != 0) s += " ??";
+        if (bits > 0 || ifIndex != 0) s += " ifIndex=" + ifIndex;
+        if (bits > 0 && bits < 32) s += "/" + bits + "[LE]";
+        bits -= 32;
+
+        // u128 remoteAddress
+        if (bits <= 0 && !remoteAddress.isAnyLocalAddress()) s += " ??";
+        if (bits > 0 || !remoteAddress.isAnyLocalAddress()) {
+            s += " remoteAddress=";
+            String ip = remoteAddress.toString();
+            if (ip.startsWith("/::ffff:")) { // technically wrong IPv4-mapped IPv6 address detection
+              s += ip.substring(8);
+              if (bits >= 96 && bits < 128) s += "/" + (bits - 96);
+            } else if (ip.startsWith("/")) {
+              s += ip.substring(1);
+              if (bits >= 0 && bits < 128) s += "/" + bits;
+            } else { // WTF, includes a hostname or what?
+              s += ip;
+            }
+        }
+        bits -= 128;
+
+        // u16 protocol
+        if (bits <= 0 && protocol != 0) s += " ??";
+        if (bits > 0 || protocol != 0) s += " protocol=" + protocol;
+        if (bits > 0 && bits < 16) s += "/" + bits + "[LE16]";
+        bits -= 16;
+
+        // be16 remotePort
+        if (bits <= 0 && remotePort != 0) s += " ??";
+        if (bits > 0 || remotePort != 0) s += " remotePort=" + remotePort;
+        if (bits > 0 && bits < 16) s += "/" + bits + "[BE16]";
+        bits -= 16;
+
+        s += "}";
+        return s;
     }
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index c6b62ee..8355d31 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -360,6 +360,8 @@
         mUnderlyingNetworks = null;
         mEnterpriseId = 0;
         mReservationId = RES_ID_UNSET;
+        // TODO: Change to default disabled when introduce this filtering.
+        mMatchNonThreadLocalNetworks = true;
     }
 
     /**
@@ -395,6 +397,7 @@
         mUnderlyingNetworks = nc.mUnderlyingNetworks;
         mEnterpriseId = nc.mEnterpriseId;
         mReservationId = nc.mReservationId;
+        mMatchNonThreadLocalNetworks = nc.mMatchNonThreadLocalNetworks;
     }
 
     /**
@@ -2236,7 +2239,8 @@
                 && (onlyImmutable || satisfiedBySSID(nc))
                 && (onlyImmutable || satisfiedByRequestor(nc))
                 && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
-                && satisfiedByReservationId(nc);
+                && satisfiedByReservationId(nc)
+                && satisfiedByMatchNonThreadLocalNetworks(nc);
     }
 
     /**
@@ -2351,7 +2355,8 @@
                 && equalsSubscriptionIds(that)
                 && equalsUnderlyingNetworks(that)
                 && equalsEnterpriseCapabilitiesId(that)
-                && equalsReservationId(that);
+                && equalsReservationId(that)
+                && equalsMatchNonThreadLocalNetworks(that);
     }
 
     @Override
@@ -2371,15 +2376,15 @@
                 + Objects.hashCode(mAllowedUids) * 41
                 + Objects.hashCode(mSSID) * 43
                 + Objects.hashCode(mTransportInfo) * 47
-                + Objects.hashCode(mPrivateDnsBroken) * 53
+                + Boolean.hashCode(mPrivateDnsBroken) * 53
                 + Objects.hashCode(mRequestorUid) * 59
                 + Objects.hashCode(mRequestorPackageName) * 61
                 + Arrays.hashCode(mAdministratorUids) * 67
                 + Objects.hashCode(mSubIds) * 71
                 + Objects.hashCode(mUnderlyingNetworks) * 73
                 + mEnterpriseId * 79
-                + mReservationId * 83;
-
+                + mReservationId * 83
+                + Boolean.hashCode(mMatchNonThreadLocalNetworks) * 89;
     }
 
     @Override
@@ -2418,6 +2423,7 @@
         dest.writeTypedList(mUnderlyingNetworks);
         dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
         dest.writeInt(mReservationId);
+        dest.writeBoolean(mMatchNonThreadLocalNetworks);
     }
 
     public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2454,8 +2460,10 @@
                 netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
                 netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
                 netCap.mReservationId = in.readInt();
+                netCap.mMatchNonThreadLocalNetworks = in.readBoolean();
                 return netCap;
             }
+
             @Override
             public NetworkCapabilities[] newArray(int size) {
                 return new NetworkCapabilities[size];
@@ -2561,6 +2569,10 @@
             sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
         }
 
+        if (mMatchNonThreadLocalNetworks) {
+            sb.append(" MatchNonThreadLocalNetworks");
+        }
+
         sb.append(" UnderlyingNetworks: ");
         if (mUnderlyingNetworks != null) {
             sb.append("[");
@@ -2945,7 +2957,45 @@
         return mReservationId == nc.mReservationId;
     }
 
+    /**
+     * Flag to control whether a NetworkRequest can match non-thread local networks.
+     * @hide
+     */
+    // TODO: Change to default disabled when introduce this filtering.
+    private boolean mMatchNonThreadLocalNetworks = true;
 
+    /**
+     * Returns the match non-thread local networks flag.
+     *
+     * @hide
+     */
+    public boolean getMatchNonThreadLocalNetworks() {
+        return mMatchNonThreadLocalNetworks;
+    }
+
+    /**
+     * Set the match non-thread local networks flag.
+     * @hide
+     */
+    public void setMatchNonThreadLocalNetworks(boolean enabled) {
+        mMatchNonThreadLocalNetworks = enabled;
+    }
+
+    private boolean equalsMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        return mMatchNonThreadLocalNetworks == nc.mMatchNonThreadLocalNetworks;
+    }
+
+    // If the flag was set, the NetworkRequest can match all local networks.
+    // Otherwise, it can only see local networks created by Thread.
+    @SuppressWarnings("FlaggedApi")
+    private boolean satisfiedByMatchNonThreadLocalNetworks(@NonNull NetworkCapabilities nc) {
+        // If the network is not a local network, out of scope.
+        if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) return true;
+        // If there is no restriction on matching non-thread local networks, return.
+        if (mMatchNonThreadLocalNetworks) return true;
+
+        return nc.hasTransport(TRANSPORT_THREAD);
+    }
 
     /**
      * Returns a bitmask of all the applicable redactions (based on the permissions held by the
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 9b3c7ba..48467ed 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -755,7 +755,17 @@
     private void parseEthernetConfig(String configString) {
         final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
         NetworkCapabilities nc;
-        if (TextUtils.isEmpty(config.mCapabilities)) {
+        // Starting with Android B (API level 36), we provide default NetworkCapabilities
+        // for Ethernet interfaces when no explicit capabilities are specified in the
+        // configuration string. This change is made to ensure consistent and expected
+        // network behavior for Ethernet devices.
+        //
+        // It's possible that OEMs or device manufacturers may have relied on the previous
+        // behavior (where interfaces without specified capabilities would have minimal
+        // capabilities) to prevent certain Ethernet interfaces from becoming
+        // the default network. To avoid breaking existing device configurations, this
+        // change is gated by the SDK level.
+        if (SdkLevel.isAtLeastB() && TextUtils.isEmpty(config.mCapabilities)) {
             boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
             nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
         } else {
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 128a98f..a458c7f 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -30,6 +30,15 @@
     -->
     <bool name="config_thread_border_router_default_enabled">false</bool>
 
+    <!-- Whether to enable or disable setting Thread country code from the telephony, wifi, location,
+     etc. The country code could be used by the Thread co-processor for setting the fixed output
+	 power of Thread radio. If the device needs to dynamically change the max output power according
+	 to the user scenario to meet the requirement of Specific Absorption Rate (SAR), it should call
+	 the API `setChannelMaxPowers()` to change the max output power, and this configuration could be
+	 set to false to disable the Thread service from setting the Thread country code.
+    -->
+    <bool name="config_thread_country_code_enabled">true</bool>
+
     <!-- Whether to use location APIs in the algorithm to determine country code or not.
     If disabled, will use other sources (telephony, wifi, etc) to determine device location for
     Thread Network regulatory purposes.
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 5c0ba78..f6dbf6c 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -48,6 +48,7 @@
 
             <!-- Configuration values for ThreadNetworkService -->
             <item type="bool" name="config_thread_default_enabled" />
+            <item type="bool" name="config_thread_border_router_default_enabled" />
             <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
             <item type="string" name="config_thread_vendor_name" />
             <item type="string" name="config_thread_vendor_oui" />
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 9bd407d..523ffee 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -1327,7 +1327,7 @@
                             + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
                             + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
             if (sLocalNetBlockedUidMap != null) {
-                BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+                BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap (default is true meaning global)",
                         (key, value) -> "" + key + ": " + value.val);
             }
             if (sLocalNetBlockedUidMap != null) {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index dc4a35b..bfb51da 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -10005,10 +10005,12 @@
             // Adds dns allow rule to LocalNetAccessMap for both TCP and UDP protocol at port 53,
             // if it is a local dns (ie. it falls in the local prefix range).
             if (prefix.contains(dnsServer)) {
-                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_UDP, 53, true);
-                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_TCP, 53, true);
+                mBpfNetMaps.addLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
+                        IPPROTO_TCP, 853, true);  // DNS over TLS
             }
         }
     }
@@ -10027,25 +10029,17 @@
             // Removes dns allow rule from LocalNetAccessMap for both TCP and UDP protocol
             // at port 53, if it is a local dns (ie. it falls in the prefix range).
             if (prefix.contains(dnsServer)) {
-                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_UDP, 53);
-                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
                         IPPROTO_TCP, 53);
+                mBpfNetMaps.removeLocalNetAccess(32 + 128 + 16 + 16, iface, dnsServer,
+                        IPPROTO_TCP, 853);  // DNS over TLS
             }
         }
     }
 
     /**
-     * Returns total bit length of an Ipv4 mapped address.
-     */
-    private int getIpv4MappedAddressBitLen() {
-        final int ifaceLen = 32; // bit length of interface
-        final int inetAddressLen = 32 + 96; // length of ipv4 mapped addresses
-        final int portProtocolLen = 32;  //16 for port + 16 for protocol;
-        return ifaceLen + inetAddressLen + portProtocolLen;
-    }
-
-    /**
      * Have netd update routes from oldLp to newLp.
      * @return true if routes changed between oldLp and newLp
      */
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 0352ad5..149979f 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -597,10 +597,12 @@
             final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
             if (cri == null) return;
 
+            // Release ClientNetworkRequest before sending onUnavailable() to ensure the app
+            // first receives an onLost() callback if a network had been created.
+            releaseClientNetworkRequest(cri);
             for (NetworkRequest request : cri.requests) {
                 mProvider.declareNetworkRequestUnfulfillable(request);
             }
-            releaseClientNetworkRequest(cri);
         }
     }
 
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 9c2b9e8..857d705 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -233,6 +233,11 @@
      */
     public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
         String iface = nai.linkProperties.getInterfaceName();
+        if (null == iface) {
+            Log.e(TAG, "DSCP policies are not supported on null interfaces.");
+            sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
+            return;
+        }
         if (!isEthernet(iface)) {
             Log.e(TAG, "DSCP policies are not supported on raw IP interfaces.");
             sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
diff --git a/staticlibs/device/com/android/net/module/util/TcUtils.java b/staticlibs/device/com/android/net/module/util/TcUtils.java
index a6b222f..eb119c8 100644
--- a/staticlibs/device/com/android/net/module/util/TcUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TcUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.net.module.util;
 
+import androidx.annotation.NonNull;
+
 import java.io.IOException;
 
 /**
@@ -33,7 +35,7 @@
      * @return true if the interface uses an ethernet L2 header.
      * @throws IOException
      */
-    public static native boolean isEthernet(String iface) throws IOException;
+    public static native boolean isEthernet(@NonNull String iface) throws IOException;
 
     /**
      * Attach a tc bpf filter.
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
index 2a587b6..22b084c 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -32,6 +32,10 @@
 static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env,
                                                                jclass clazz,
                                                                jstring iface) {
+  if (nullptr == iface) {
+    jniThrowNullPointerException(env, "iface is null");
+    return false;
+  }
   ScopedUtfChars interface(env, iface);
   bool result = false;
   int error = isEthernet(interface.c_str(), result);
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index d4753b7..55fbe58 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -27,6 +27,7 @@
     get_apf_counters_from_dumpsys,
     get_ipv4_addresses,
     get_non_tentative_ipv6_addresses,
+    get_exclude_all_host_ipv6_multicast_addresses,
     get_hardware_address,
     is_send_raw_packet_downstream_supported,
     is_packet_capture_supported,
@@ -170,6 +171,31 @@
     ip_addresses = get_non_tentative_ipv6_addresses(self.mock_ad, "wlan0")
     asserts.assert_equal(ip_addresses, [])
 
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_exclude_all_host_ipv6_multicast_addresses_success(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = """
+47:     wlan0
+        inet6 ff02::1:ff99:37b0
+        inet6 ff02::1:ffb7:cba2 users 2
+        inet6 ff02::1
+        inet6 ff01::1
+"""
+    ip_addresses = get_exclude_all_host_ipv6_multicast_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses,
+                         ["ff02::1:ff99:37b0",
+                          "ff02::1:ffb7:cba2"])
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_exclude_all_host_ipv6_multicast_addresses_not_found(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = ""
+    ip_addresses = get_exclude_all_host_ipv6_multicast_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses, [])
+
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_success(
       self, mock_adb_shell: MagicMock
diff --git a/staticlibs/tests/unit/host/python/packet_utils_test.py b/staticlibs/tests/unit/host/python/packet_utils_test.py
deleted file mode 100644
index 8ad9576..0000000
--- a/staticlibs/tests/unit/host/python/packet_utils_test.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#  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 net_tests_utils.host.python import packet_utils
-
-class TestPacketUtils(base_test.BaseTestClass):
-    def test_unicast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            01020304050600010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac="01:02:03:04:05:06",
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_broadcast_arp_request(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
-        #   arp = ARP(op=1, pdst="192.168.1.1", hwsrc="00:01:02:03:04:05", psrc="192.168.1.2")
-        #   pkt = eth/arp
-        expect_arp_request = """
-            ffffffffffff00010203040508060001080006040001000102030405c0a80102000000000000c0a80101
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac="00:01:02:03:04:05",
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip="192.168.1.2",
-            dst_ip="192.168.1.1",
-            op=packet_utils.ARP_REQUEST_OP
-        )
-        asserts.assert_equal(expect_arp_request, arp_request)
-
-    def test_arp_reply(self):
-        # Using scapy to generate unicast arp request packet:
-        #   eth = Ether(src="01:02:03:04:05:06", dst="00:01:02:03:04:05")
-        #   arp = ARP(op=2, pdst="192.168.1.2", \
-        #             hwsrc="01:02:03:04:05:06", \
-        #             psrc="192.168.1.1", \
-        #             hwdst="00:01:02:03:04:05")
-        #   pkt = eth/arp
-        expect_arp_reply = """
-            00010203040501020304050608060001080006040002010203040506c0a80101000102030405c0a80102
-        """.upper().replace(" ", "").replace("\n", "")
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac="01:02:03:04:05:06",
-            dst_mac="00:01:02:03:04:05",
-            src_ip="192.168.1.1",
-            dst_ip="192.168.1.2",
-            op=packet_utils.ARP_REPLY_OP
-        )
-        asserts.assert_equal(expect_arp_reply, arp_reply)
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
index 498dbaf..fa6a310 100644
--- a/staticlibs/tests/unit/host/python/run_tests.py
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -18,7 +18,6 @@
 from host.python.adb_utils_test import TestAdbUtils
 from host.python.apf_utils_test import TestApfUtils
 from host.python.assert_utils_test import TestAssertUtils
-from host.python.packet_utils_test import TestPacketUtils
 from mobly import suite_runner
 
 
@@ -32,5 +31,5 @@
   sys.argv.pop(1)
   # TODO: make the tests can be executed without manually list classes.
   suite_runner.run_suite(
-      [TestAssertUtils, TestAdbUtils, TestApfUtils, TestPacketUtils], sys.argv
+      [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv
   )
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
index 33b3838..6a62e21 100644
--- a/staticlibs/testutils/host/python/apf_test_base.py
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -70,6 +70,9 @@
     # Enable doze mode to activate APF.
     adb_utils.set_doze_mode(self.clientDevice, True)
 
+    # wait for APF to become active.
+    time.sleep(3)
+
   def teardown_class(self):
     adb_utils.set_doze_mode(self.clientDevice, False)
     tether_utils.cleanup_tethering_for_upstream_type(
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 1648d36..4835c23 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -148,6 +148,38 @@
   else:
     return []
 
+def get_exclude_all_host_ipv6_multicast_addresses(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> list[str]:
+  """Retrieves the IPv6 multicast addresses of a given interface on an Android device.
+
+  This function executes an ADB shell command (`ip -6 maddr show`) to get the
+  network interface information and extracts the IPv6 multicast address from the output.
+  If devices have no IPv6 multicast address, raise PatternNotFoundException.
+
+  Args:
+      ad: The Android device object.
+      iface_name: The name of the network interface (e.g., "wlan0").
+
+  Returns:
+      The IPv6 multicast addresses of the interface as a list of string.
+      Return empty list if no IPv6 multicast address.
+  """
+  # output format
+  # 47:     wlan0
+  #         inet6 ff02::1:ff99:37b0
+  #         inet6 ff02::1:ffb7:cba2 users 2
+  #         inet6 ff02::1
+  #         inet6 ff01::1
+  output = adb_utils.adb_shell(ad, f"ip -6 maddr show {iface_name}")
+  pattern = r"inet6\s+([a-fA-F0-9:]+)(?:\s+users\s+\d+)?"
+  matches = re.findall(pattern, output)
+
+  if matches:
+    return [addr for addr in matches if addr not in ("ff02::1", "ff01::1")]
+  else:
+    return []
+
 def get_hardware_address(
     ad: android_device.AndroidDevice, iface_name: str
 ) -> str:
@@ -407,10 +439,28 @@
     def wrapper(self, *args, **kwargs):
       asserts.abort_class_if(
         (not hasattr(self, 'client')) or (not hasattr(self.client, 'isAtLeastB')),
-        "client device is not B+"
+        "no valid client attribute"
       )
 
-      asserts.abort_class_if(not self.client.isAtLeastB(), "not B+")
+      asserts.abort_class_if(not self.client.isAtLeastB(), "client device is not Android B+")
+      return test_function(self, *args, **kwargs)
+    return wrapper
+  return decorator
+
+def apf_ram_at_least(size):
+  def decorator(test_function):
+    @functools.wraps(test_function)
+    def wrapper(self, *args, **kwargs):
+      asserts.abort_class_if(
+        (not hasattr(self, 'clientDevice')) or (not hasattr(self, 'client_iface_name')),
+        "no valid client attribute"
+      )
+
+      caps = get_apf_capabilities(self.clientDevice, self.client_iface_name)
+      asserts.skip_if(
+        caps.apf_ram_size < size,
+        f'APF rame size {caps.apf_ram_size} < {size}'
+      )
       return test_function(self, *args, **kwargs)
     return wrapper
   return decorator
diff --git a/staticlibs/testutils/host/python/packet_utils.py b/staticlibs/testutils/host/python/packet_utils.py
deleted file mode 100644
index b613f03..0000000
--- a/staticlibs/testutils/host/python/packet_utils.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#  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 ipaddress import IPv4Address
-from socket import inet_aton
-
-ETHER_BROADCAST_MAC_ADDRESS = "FF:FF:FF:FF:FF:FF"
-ARP_REQUEST_OP = 1
-ARP_REPLY_OP = 2
-
-"""
-This variable defines a template for constructing ARP packets in hexadecimal format.
-It's used to provide the common fields for ARP packet, and replaced needed fields when constructing
-"""
-ARP_TEMPLATE = (
-    # Ether Header (14 bytes)
-    "{dst_mac}" + # DA
-    "{src_mac}" + # SA
-    "0806" + # ARP
-    # ARP Header (28 bytes)
-    "0001" + # Hardware type (Ethernet)
-    "0800" + # Protocol type (IPv4)
-    "06" + # hardware address length
-    "04" + # protocol address length
-    "{opcode}" + # opcode
-    "{sender_mac}" + # sender MAC
-    "{sender_ip}" + # sender IP
-    "{target_mac}" + # target MAC
-    "{target_ip}" # target IP
-)
-
-def construct_arp_packet(src_mac, dst_mac, src_ip, dst_ip, op) -> str:
-    """Constructs an ARP packet as a hexadecimal string.
-
-    This function creates an ARP packet by filling in the required fields
-    in a predefined ARP packet template.
-
-    Args:
-    src_mac: The MAC address of the sender. (e.g. "11:22:33:44:55:66")
-    dst_mac: The MAC address of the recipient. (e.g. "aa:bb:cc:dd:ee:ff")
-    src_ip: The IP address of the sender. (e.g. "1.1.1.1")
-    dst_ip: The IP address of the target machine. (e.g. "2.2.2.2")
-    op: The op code of the ARP packet, refer to ARP_*_OP
-
-    Returns:
-    A string representing the ARP packet in hexadecimal format.
-    """
-    # Replace the needed fields from packet template
-    arp_pkt = ARP_TEMPLATE.format(
-            dst_mac=dst_mac.replace(":",""),
-            src_mac=src_mac.replace(":",""),
-            opcode=str(op).rjust(4, "0"),
-            sender_mac=src_mac.replace(":",""),
-            sender_ip=inet_aton(src_ip).hex(),
-            target_mac=("000000000000" if op == ARP_REQUEST_OP else dst_mac.replace(":", "")),
-            target_ip=inet_aton(dst_ip).hex()
-    )
-
-    # always convert to upper case hex string
-    return arp_pkt.upper()
\ No newline at end of file
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index d694637..3fc2af0 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -56,6 +56,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
 import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -1532,4 +1533,93 @@
         nc.setReservationId(43);
         assertNotEquals(nc, other);
     }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_equals() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setMatchNonThreadLocalNetworks(true);
+        final NetworkCapabilities other = new NetworkCapabilities(nc);
+        assertEquals(nc, other);
+
+        nc.setMatchNonThreadLocalNetworks(false);
+        assertNotEquals(nc, other);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_enabled() {
+        doTestMatchNonThreadLocalNetworks(true);
+    }
+
+    @Test
+    public void testMatchNonThreadLocalNetworks_disabled() {
+        doTestMatchNonThreadLocalNetworks(false);
+    }
+
+    private void doTestMatchNonThreadLocalNetworks(boolean enabled) {
+        // Setup request NCs.
+        final NetworkCapabilities noTransportRequestNc = new NetworkCapabilities();
+        final NetworkCapabilities threadRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities wifiRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities multiTransportRequestNc =
+                new NetworkCapabilities.Builder().addTransportType(
+                        TRANSPORT_THREAD).addTransportType(TRANSPORT_WIFI).build();
+
+        // Setup network NCs.
+        final NetworkCapabilities localNoTransportNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+        final NetworkCapabilities localThreadsNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_THREAD).build();
+        final NetworkCapabilities localWifiNc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkCapabilities wanWifiNc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+
+        // Mark flags accordingly.
+        noTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        threadRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        wifiRequestNc.setMatchNonThreadLocalNetworks(enabled);
+        multiTransportRequestNc.setMatchNonThreadLocalNetworks(enabled);
+
+        if (enabled) {
+            // A request with no specific transport matches all networks.
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with no specific transport only matches thread networks.
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+            assertFalse(noTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(noTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        // A request with TRANSPORT_THREAD only matches thread networks.
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(threadRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        assertFalse(threadRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+
+        assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request with multiple transports only matches thread networks.
+            assertFalse(multiTransportRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localNoTransportNc));
+        assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localThreadsNc));
+        assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(wanWifiNc));
+        if (enabled) {
+            assertTrue(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        } else {
+            // A request without TRANSPORT_THREAD matches nothing.
+            assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc));
+        }
+    }
 }
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index fb45f4a..2404966 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -14,10 +14,23 @@
 
 from mobly import asserts
 from scapy.layers.inet import IP, ICMP, IPOption_Router_Alert
-from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest, ICMPv6EchoReply
-from scapy.layers.l2 import Ether
+from scapy.layers.inet6 import (
+    IPv6,
+    IPv6ExtHdrHopByHop,
+    ICMPv6EchoRequest,
+    ICMPv6EchoReply,
+    ICMPv6MLQuery2,
+    ICMPv6MLReport2,
+    ICMPv6MLDMultAddrRec,
+    ICMPv6NDOptSrcLLAddr,
+    ICMPv6NDOptDstLLAddr,
+    ICMPv6ND_NS,
+    ICMPv6ND_NA,
+    RouterAlert
+)
+from scapy.layers.l2 import ARP, Ether
 from scapy.contrib.igmpv3 import IGMPv3, IGMPv3mq, IGMPv3mr, IGMPv3gr
-from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils
 
 APFV6_VERSION = 6000
 ARP_OFFLOAD_REPLY_LEN = 60
@@ -40,51 +53,75 @@
         super().teardown_class()
 
     def test_unicast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=self.client_mac_address,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
         )
 
     def test_broadcast_arp_request_offload(self):
-        arp_request = packet_utils.construct_arp_packet(
-            src_mac=self.server_mac_address,
-            dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
-            src_ip=self.server_ipv4_addresses[0],
-            dst_ip=self.client_ipv4_addresses[0],
-            op=packet_utils.ARP_REQUEST_OP
+        eth = Ether(src=self.server_mac_address, dst='ff:ff:ff:ff:ff:ff')
+        arp = ARP(
+            op=1,
+            psrc=self.server_ipv4_addresses[0],
+            pdst=self.client_ipv4_addresses[0],
+            hwsrc=self.server_mac_address
         )
+        arp_request = bytes(eth/arp).hex()
 
-        arp_reply = packet_utils.construct_arp_packet(
-            src_mac=self.client_mac_address,
-            dst_mac=self.server_mac_address,
-            src_ip=self.client_ipv4_addresses[0],
-            dst_ip=self.server_ipv4_addresses[0],
-            op=packet_utils.ARP_REPLY_OP
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        arp = ARP(
+            op=2,
+            psrc=self.client_ipv4_addresses[0],
+            pdst=self.server_ipv4_addresses[0],
+            hwsrc=self.client_mac_address,
+            hwdst=self.server_mac_address
         )
+        expected_arp_reply = bytes(eth/arp).hex()
 
         # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
-        arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+        expected_arp_reply = expected_arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
 
         self.send_packet_and_expect_reply_received(
-            arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+            arp_request, "DROPPED_ARP_REQUEST_REPLIED", expected_arp_reply
+        )
+
+    def test_non_dad_ipv6_neighbor_solicitation_offload(self):
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NS(tgt=self.client_ipv6_addresses[0])
+        opt = ICMPv6NDOptSrcLLAddr(lladdr=self.server_mac_address)
+        neighbor_solicitation = bytes(eth/ip/icmpv6/opt).hex()
+
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst=self.server_ipv6_addresses[0])
+        icmpv6 = ICMPv6ND_NA(tgt=self.client_ipv6_addresses[0], R=1, S=1, O=1)
+        opt = ICMPv6NDOptDstLLAddr(lladdr=self.client_mac_address)
+        expected_neighbor_advertisement = bytes(eth/ip/icmpv6/opt).hex()
+        self.send_packet_and_expect_reply_received(
+            neighbor_solicitation,
+            "DROPPED_IPV6_NS_REPLIED_NON_DAD",
+            expected_neighbor_advertisement
         )
 
     @apf_utils.at_least_B()
@@ -103,6 +140,7 @@
         )
 
     @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
     def test_ipv6_icmp_echo_request_offload(self):
         eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
         ip = IPv6(src=self.server_ipv6_addresses[0], dst=self.client_ipv6_addresses[0])
@@ -161,3 +199,28 @@
                 self.clientDevice,
                 f'ip addr del {addr}/32 dev {self.client_iface_name}'
             )
+
+    @apf_utils.at_least_B()
+    @apf_utils.apf_ram_at_least(3000)
+    def test_mldv2_general_query_offload(self):
+        ether = Ether(src=self.server_mac_address, dst='33:33:00:00:00:01')
+        ip = IPv6(src=self.server_ipv6_addresses[0], dst='ff02::1', hlim=1)
+        hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        mld = ICMPv6MLQuery2()
+        mldv2_general_query = bytes(ether/ip/hopOpts/mld).hex()
+
+        ether = Ether(src=self.client_mac_address, dst='33:33:00:00:00:16')
+        ip = IPv6(src=self.client_ipv6_addresses[0], dst='ff02::16', hlim=1)
+
+        mcast_addrs = apf_utils.get_exclude_all_host_ipv6_multicast_addresses(
+            self.clientDevice, self.client_iface_name
+        )
+
+        mld_records = []
+        for addr in mcast_addrs:
+            mld_records.append(ICMPv6MLDMultAddrRec(dst=addr, rtype=2))
+        mld = ICMPv6MLReport2(records=mld_records)
+        expected_mldv2_report = bytes(ether/ip/hopOpts/mld).hex()
+        self.send_packet_and_expect_reply_received(
+            mldv2_general_query, "DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED", expected_mldv2_report
+        )
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index ceccf0b..df4dab5 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -757,6 +757,25 @@
         assertEquals(IPPROTO_UDP, policy2.protocol)
         assertParcelingIsLossless(policy2)
     }
+
+    @Test
+    fun testSendDscpPolicyWithoutInterfaceName() {
+        val nc = NetworkCapabilities().apply {
+            addTransportType(TRANSPORT_TEST)
+        }
+        val agent = TestableNetworkAgent(
+                realContext,
+                handlerThread.looper,
+                nc,
+                LinkProperties() /* note: no interface name */,
+                NetworkAgentConfig.Builder().build()
+        )
+        agentsToCleanUp.add(agent)
+        runAsShell(MANAGE_TEST_NETWORKS) { agent.register() }
+        // Without the fix, this will crash the system with SIGSEGV.
+        agent.sendAddDscpPolicy(DscpPolicy.Builder(1, 1).build())
+        agent.expectCallback<OnDscpPolicyStatusUpdated>()
+    }
 }
 
 private fun ByteBuffer.readAsArray(): ByteArray {
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
index babcba9..ee5b4ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -38,12 +38,14 @@
 import android.net.NetworkSpecifier
 import android.net.RouteInfo
 import android.os.Build
+import android.os.Handler
 import android.os.HandlerThread
 import android.os.ParcelFileDescriptor
 import com.android.server.net.L2capNetwork.L2capIpClient
 import com.android.server.net.L2capPacketForwarder
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
 import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
 import com.android.testutils.TestableNetworkCallback
@@ -59,6 +61,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mockito.doAnswer
@@ -394,4 +397,34 @@
         val cb2 = requestNetwork(nr)
         cb2.expectAvailableCallbacks(anyNetwork(), validated = false)
     }
+
+    /** Test to ensure onLost() is sent before onUnavailable() when the network is torn down. */
+    @Test
+    fun testClientNetwork_gracefulTearDown() {
+        val specifier = L2capNetworkSpecifier.Builder()
+            .setRole(ROLE_CLIENT)
+            .setHeaderCompression(HEADER_COMPRESSION_NONE)
+            .setRemoteAddress(MacAddress.fromBytes(REMOTE_MAC))
+            .setPsm(PSM)
+            .build()
+
+        val nr = REQUEST.copyWithSpecifier(specifier)
+        val cb = requestNetwork(nr)
+        cb.expectAvailableCallbacks(anyNetwork(), validated = false)
+
+        // Capture the L2capPacketForwarder callback object to tear down the network.
+        val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+        val forwarderCbCaptor = ArgumentCaptor.forClass(L2capPacketForwarder.ICallback::class.java)
+        verify(providerDeps).createL2capPacketForwarder(
+                handlerCaptor.capture(), any(), any(), any(), forwarderCbCaptor.capture())
+        val handler = handlerCaptor.value
+        val forwarderCb = forwarderCbCaptor.value
+
+        // Trigger a forwarding error
+        handler.post { forwarderCb.onError() }
+        handler.waitForIdle(HANDLER_TIMEOUT_MS)
+
+        cb.expect<Lost>()
+        cb.expect<Unavailable>()
+    }
 }
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
index f90a23b..2687e26 100644
--- a/thread/docs/build-an-android-border-router.md
+++ b/thread/docs/build-an-android-border-router.md
@@ -380,10 +380,12 @@
 [config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
 for the full list.
 
-Typically, you must change the `config_thread_vendor_name`,
-`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
-product values. Those values will be included in the `_meshcop._udp` mDNS
-service which is always advertised by a Thread Border Router.
+Typically, you must set `config_thread_border_router_default_enabled` to `true`
+to enable your device as a Thread Border Router, and change the
+`config_thread_vendor_name`, `config_thread_vendor_oui` and
+`config_thread_model_name` to your vendor or product values. Those values will
+be included in the `_meshcop._udp` mDNS service which is always advertised by a
+Thread Border Router.
 
 To add the overlay, you need to create a new `ConnectivityOverlayOrange`
 runtime_resource_overlay target for your Orange device. Create a new
@@ -436,6 +438,7 @@
   ```
 - `config_thread.xml`:
   ```
+  <bool name="config_thread_border_router_default_enabled">true</bool>
   <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
   <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
   <string translatable="false" name="config_thread_model_name">Orange</string>
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 7063357..d859fb2 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -628,12 +628,15 @@
         boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
         int autoJoinConfig = R.bool.config_thread_border_router_auto_join_enabled;
         boolean autoJoinEnabled = mResources.get().getBoolean(autoJoinConfig);
+        boolean countryCodeEnabled =
+                mResources.get().getBoolean(R.bool.config_thread_country_code_enabled);
         return new OtDaemonConfiguration.Builder()
                 .setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
                 .setNat64Enabled(threadConfig.isNat64Enabled())
                 .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
                 .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
                 .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
+                .setCountryCodeEnabled(countryCodeEnabled)
                 .build();
     }
 
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index a96d06e..16196fa 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -17,6 +17,7 @@
 package com.android.server.thread;
 
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
+
 import static com.android.server.thread.ThreadPersistentSettings.KEY_COUNTRY_CODE;
 
 import android.annotation.Nullable;
@@ -223,6 +224,10 @@
                 .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
     }
 
+    private boolean isCountryCodeEnabled() {
+        return mResources.get().getBoolean(R.bool.config_thread_country_code_enabled);
+    }
+
     public ThreadNetworkCountryCode(
             LocationManager locationManager,
             ThreadNetworkControllerService threadNetworkControllerService,
@@ -270,6 +275,11 @@
 
     /** Sets up this country code module to listen to location country code changes. */
     public synchronized void initialize() {
+        if (!isCountryCodeEnabled()) {
+            LOG.i("Thread country code is disabled");
+            return;
+        }
+
         registerGeocoderCountryCodeCallback();
         registerWifiCountryCodeCallback();
         registerTelephonyCountryCodeCallback();
@@ -654,6 +664,7 @@
     /** Dumps the current state of this ThreadNetworkCountryCode object. */
     public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
+        pw.println("isCountryCodeEnabled            : " + isCountryCodeEnabled());
         pw.println("mIsCpSettingCountryCodeSupported: " + mIsCpSettingCountryCodeSupported);
         pw.println("mOverrideCountryCodeInfo        : " + mOverrideCountryCodeInfo);
         pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
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 95ebda5..63d6130 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -261,6 +261,7 @@
                 .thenReturn(TEST_MODEL_NAME);
         when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
                 .thenReturn(new String[] {});
+        when(mResources.getBoolean(eq(R.bool.config_thread_country_code_enabled))).thenReturn(true);
 
         final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
         mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
index 6eb9b50..1a6b3cc 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -206,6 +206,21 @@
     }
 
     @Test
+    public void initialize_countryCodeDisabled_defaultCountryCodeIsUsed() {
+        when(mResources.getBoolean(R.bool.config_thread_country_code_enabled)).thenReturn(false);
+
+        mThreadNetworkCountryCode.initialize();
+
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mTelephonyManager);
+        verifyNoMoreInteractions(mSubscriptionManager);
+        verifyNoMoreInteractions(mGeocoder);
+        verifyNoMoreInteractions(mLocationManager);
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+    }
+
+    @Test
     public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
         when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
                 .thenReturn(false);
@@ -507,6 +522,7 @@
         mThreadNetworkCountryCode.dump(new FileDescriptor(), printWriter, null);
         String outputString = stringWriter.toString();
 
+        assertThat(outputString).contains("isCountryCodeEnabled");
         assertThat(outputString).contains("mIsCpSettingCountryCodeSupported");
         assertThat(outputString).contains("mOverrideCountryCodeInfo");
         assertThat(outputString).contains("mTelephonyCountryCodeSlotInfoMap");