Merge "Wrapping Network and Traffic permissions in a class for module usage." into main
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c67be66..40d1281 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -61,6 +61,7 @@
 
 // The following matches bpf_helpers.h, which is only for inclusion in bpf code
 #define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_25Q2_VERSION 47u
 
 using android::base::EndsWith;
 using android::base::GetIntProperty;
@@ -1102,11 +1103,33 @@
             }
         }
 
-        int progId = bpfGetFdProgId(fd);
-        if (progId == -1) {
-            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
-        } else {
-            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+        if (isAtLeastKernelVersion(4, 14, 0)) {
+            int progId = bpfGetFdProgId(fd);
+            if (progId == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdProgId failed, errno: %d", err);
+                return -err;
+            }
+
+            int jitLen = bpfGetFdJitProgLen(fd);
+            if (jitLen == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdJitProgLen failed, ret: %d", err);
+                return -err;
+            }
+
+            int xlatLen = bpfGetFdXlatProgLen(fd);
+            if (xlatLen == -1) {
+                const int err = errno;
+                ALOGE("bpfGetFdXlatProgLen failed, ret: %d", err);
+                return -err;
+            }
+            ALOGI("prog %s id %d len jit:%d xlat:%d", progPinLoc.c_str(), progId, jitLen, xlatLen);
+
+            if (!jitLen && bpfloader_ver >= BPFLOADER_MAINLINE_25Q2_VERSION) {
+                ALOGE("Kernel eBPF JIT failure for %s", progPinLoc.c_str());
+                return -ENOTSUP;
+            }
         }
     }
 
diff --git a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index a31445a..1d72b77 100644
--- a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
@@ -269,7 +269,7 @@
     return info.FIELD; \
 }
 
-// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
+// All 9 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
 // while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
 DEFINE_BPF_GET_FD(map, MapType, type)            // int bpfGetFdMapType(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(map, MapId, id)                // int bpfGetFdMapId(const borrowed_fd& map_fd)
@@ -278,6 +278,8 @@
 DEFINE_BPF_GET_FD(map, MaxEntries, max_entries)  // int bpfGetFdMaxEntries(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(map, MapFlags, map_flags)      // int bpfGetFdMapFlags(const borrowed_fd& map_fd)
 DEFINE_BPF_GET_FD(prog, ProgId, id)              // int bpfGetFdProgId(const borrowed_fd& prog_fd)
+DEFINE_BPF_GET_FD(prog, JitProgLen, jited_prog_len)   // int bpfGetFdJitProgLen(...)
+DEFINE_BPF_GET_FD(prog, XlatProgLen, xlated_prog_len) // int bpfGetFdXlatProgLen(...)
 
 #undef DEFINE_BPF_GET_FD
 
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 2cfa546..0ecda3d 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -21,7 +21,6 @@
 #include <string>
 
 #include <android-base/properties.h>
-#include <android-modules-utils/sdk_level.h>
 #include <android/api-level.h>
 #include <bpf/BpfUtils.h>
 
@@ -32,11 +31,6 @@
 using std::string;
 
 using android::bpf::isAtLeastKernelVersion;
-using android::modules::sdklevel::IsAtLeastR;
-using android::modules::sdklevel::IsAtLeastS;
-using android::modules::sdklevel::IsAtLeastT;
-using android::modules::sdklevel::IsAtLeastU;
-using android::modules::sdklevel::IsAtLeastV;
 
 #define PLATFORM "/sys/fs/bpf/"
 #define TETHERING "/sys/fs/bpf/tethering/"
@@ -48,10 +42,15 @@
 class BpfExistenceTest : public ::testing::Test {
 };
 
-//ToDo: replace isAtLeast25Q2 with IsAtLeastB once sdk_level have been upgraded to 36 on aosp/main
 const bool unreleased = (android::base::GetProperty("ro.build.version.codename", "REL") != "REL");
-const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
-const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__);
+const int api_level = unreleased ? 10000 : android_get_device_api_level();
+const bool isAtLeastR = (api_level >= 30);
+const bool isAtLeastS = (api_level >= 31);
+// Sv2 is 32
+const bool isAtLeastT = (api_level >= 33);
+const bool isAtLeastU = (api_level >= 34);
+const bool isAtLeastV = (api_level >= 35);
+const bool isAtLeast25Q2 = (api_level >= 36);
 
 // Part of Android R platform (for 4.9+), but mainlined in S
 static const set<string> PLATFORM_ONLY_IN_R = {
@@ -194,33 +193,33 @@
     // and for the presence of mainline stuff.
 
     // Note: Q is no longer supported by mainline
-    ASSERT_TRUE(IsAtLeastR());
+    ASSERT_TRUE(isAtLeastR);
 
     // R can potentially run on pre-4.9 kernel non-eBPF capable devices.
-    DO_EXPECT(IsAtLeastR() && !IsAtLeastS() && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
+    DO_EXPECT(isAtLeastR && !isAtLeastS && isAtLeastKernelVersion(4, 9, 0), PLATFORM_ONLY_IN_R);
 
     // S requires Linux Kernel 4.9+ and thus requires eBPF support.
-    if (IsAtLeastS()) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
-    DO_EXPECT(IsAtLeastS(), MAINLINE_FOR_S_PLUS);
+    if (isAtLeastS) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
+    DO_EXPECT(isAtLeastS, MAINLINE_FOR_S_PLUS);
 
     // Nothing added or removed in SCv2.
 
     // T still only requires Linux Kernel 4.9+.
-    DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
-    DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
+    DO_EXPECT(isAtLeastT, MAINLINE_FOR_T_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_T_5_10_PLUS);
+    DO_EXPECT(isAtLeastT && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
 
     // U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
-    if (IsAtLeastU()) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
-    DO_EXPECT(IsAtLeastU(), MAINLINE_FOR_U_PLUS);
-    DO_EXPECT(IsAtLeastU() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
+    if (isAtLeastU) ASSERT_TRUE(isAtLeastKernelVersion(4, 14, 0));
+    DO_EXPECT(isAtLeastU, MAINLINE_FOR_U_PLUS);
+    DO_EXPECT(isAtLeastU && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_U_5_10_PLUS);
 
     // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
-    if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
-    DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
-    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+    if (isAtLeastV) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
+    DO_EXPECT(isAtLeastV, MAINLINE_FOR_V_PLUS);
+    DO_EXPECT(isAtLeastV && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
 
     if (isAtLeast25Q2) ASSERT_TRUE(isAtLeastKernelVersion(5, 4, 0));
     DO_EXPECT(isAtLeast25Q2, MAINLINE_FOR_25Q2_PLUS);
diff --git a/framework/Android.bp b/framework/Android.bp
index d6ee759..f66bc60 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -162,7 +162,6 @@
     srcs: [":httpclient_api_sources"],
     static_libs: [
         "com.android.net.http.flags-aconfig-java",
-        "modules-utils-expresslog",
     ],
     libs: [
         "androidx.annotation_annotation",
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 0536263..317854b 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -143,7 +143,7 @@
      * @hide
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
     public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
 
     private ConnectivityCompatChanges() {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index a8e3203..27f039f 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -191,7 +191,6 @@
 import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.BpfNetMaps;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.File;
@@ -726,11 +725,6 @@
             mTrafficStatsUidCache = null;
         }
 
-        // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
-        // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
-        // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
-        // NetworkStatsService starts Java SkDestroyListener (new code).
-        final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
         mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
         mHandler.post(mSkDestroyListener::start);
     }
@@ -952,11 +946,6 @@
             return Build.isDebuggable();
         }
 
-        /** Create a new BpfNetMaps. */
-        public BpfNetMaps makeBpfNetMaps(Context ctx) {
-            return new BpfNetMaps(ctx);
-        }
-
         /** Create a new SkDestroyListener. */
         public SkDestroyListener makeSkDestroyListener(
                 IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
diff --git a/service/libconnectivity/include/connectivity_native.h b/service/libconnectivity/include/connectivity_native.h
index f4676a9..f264b68 100644
--- a/service/libconnectivity/include/connectivity_native.h
+++ b/service/libconnectivity/include/connectivity_native.h
@@ -20,12 +20,6 @@
 #include <sys/cdefs.h>
 #include <netinet/in.h>
 
-// For branches that do not yet have __ANDROID_API_U__ defined, like module
-// release branches.
-#ifndef __ANDROID_API_U__
-#define __ANDROID_API_U__ 34
-#endif
-
 __BEGIN_DECLS
 
 /**
@@ -41,7 +35,7 @@
  *
  * @param port Int corresponding to port number.
  */
-int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_blockPortForBind(in_port_t port) __INTRODUCED_IN(34);
 
 /**
  * Unblocks a port that has previously been blocked.
@@ -54,7 +48,7 @@
  *
  * @param port Int corresponding to port number.
  */
-int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockPortForBind(in_port_t port) __INTRODUCED_IN(34);
 
 /**
  * Unblocks all ports that have previously been blocked.
@@ -64,7 +58,7 @@
  *  - EPERM if the UID of the client doesn't have network stack permission
  *  - Other errors as per https://man7.org/linux/man-pages/man2/bpf.2.html
  */
-int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(__ANDROID_API_U__);
+int AConnectivityNative_unblockAllPortsForBind() __INTRODUCED_IN(34);
 
 /**
  * Gets the list of ports that have been blocked.
@@ -79,7 +73,7 @@
  *              blocked ports, which may be larger than the ports array that was filled.
  */
 int AConnectivityNative_getPortsBlockedForBind(in_port_t* _Nonnull ports, size_t* _Nonnull count)
-    __INTRODUCED_IN(__ANDROID_API_U__);
+    __INTRODUCED_IN(34);
 
 __END_DECLS
 
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 6940c7e..7c0c223 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -912,7 +912,7 @@
                     + "(" + iface + ")");
             return;
         }
-        LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
                 address, protocol, remotePort);
 
         try {
@@ -924,13 +924,43 @@
     }
 
     /**
-     * False if the configuration is disallowed.
-     *
+     * Remove configuration to local_net_access trie map.
+     * @param lpmBitlen prefix length that will be used for longest matching
+     * @param iface interface name
+     * @param address remote address. ipv4 addresses would be mapped to v6
+     * @param protocol required for longest match in special cases
+     * @param remotePort src/dst port for ingress/egress
+     */
+    @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+    public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+            final InetAddress address, final int protocol, final int remotePort) {
+        throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
+        final int ifIndex = mDeps.getIfIndex(iface);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
+                    + "(" + iface + ")");
+            return;
+        }
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+                address, protocol, remotePort);
+
+        try {
+            sLocalNetAccessMap.deleteEntry(localNetAccessKey);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to remove local network access for localNetAccessKey : "
+                    + localNetAccessKey);
+        }
+    }
+
+    /**
+     * Fetches value available in local_net_access bpf map for provided configuration
      * @param lpmBitlen  prefix length that will be used for longest matching
      * @param iface    interface name
      * @param address    remote address. ipv4 addresses would be mapped to v6
      * @param protocol   required for longest match in special cases
      * @param remotePort src/dst port for ingress/egress
+     * @return false if the configuration is disallowed, true if the configuration is absent i.e. it
+     * is not local network or if configuration is allowed like local dns servers.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
     public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
@@ -942,10 +972,10 @@
                     + address + "(" + iface + ")");
             return true;
         }
-        LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+        final LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
                 address, protocol, remotePort);
         try {
-            Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+            final Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
             return value == null ? true : value.val;
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to find local network access configuration for "
@@ -973,10 +1003,10 @@
      * @param uid application uid that needs check if local network calls are blocked.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public boolean getUidValueFromLocalNetBlockMap(final int uid) {
-        throwIfPre25Q2("getUidValueFromLocalNetBlockMap is not available on pre-B devices");
+    public boolean isUidBlockedFromUsingLocalNetwork(final int uid) {
+        throwIfPre25Q2("isUidBlockedFromUsingLocalNetwork is not available on pre-B devices");
         try {
-            Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+            final Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
             return value == null ? false : value.val;
         } catch (ErrnoException e) {
             Log.e(TAG, "Failed to find uid(" + uid
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 18801f0..661a2f7 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -109,8 +109,8 @@
 import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
 import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
 import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
-import static android.net.NetworkCapabilities.RES_ID_UNSET;
 import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -146,6 +146,8 @@
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
 import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LOCAL_PREFIXES;
+import static com.android.net.module.util.NetworkStackConstants.MULTICAST_AND_BROADCAST_PREFIXES;
 import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
@@ -214,6 +216,7 @@
 import android.net.InetAddresses;
 import android.net.IpMemoryStore;
 import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
 import android.net.LocalNetworkInfo;
@@ -1020,6 +1023,8 @@
     private final LingerMonitor mLingerMonitor;
     private final SatelliteAccessController mSatelliteAccessController;
 
+    private final L2capNetworkProvider mL2capNetworkProvider;
+
     // sequence number of NetworkRequests
     private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
 
@@ -1620,6 +1625,11 @@
                     connectivityServiceInternalHandler);
         }
 
+        /** Creates an L2capNetworkProvider */
+        public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+            return new L2capNetworkProvider(context);
+        }
+
         /** Returns the data inactivity timeout to be used for cellular networks */
         public int getDefaultCellularDataInactivityTimeout() {
             return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
@@ -2091,6 +2101,8 @@
         }
         mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
                 && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
+
+        mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
     }
 
     /**
@@ -4126,6 +4138,10 @@
             mCarrierPrivilegeAuthenticator.start();
         }
 
+        if (mL2capNetworkProvider != null) {
+            mL2capNetworkProvider.start();
+        }
+
         // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
         if (mDeps.isAtLeastT()) {
             mBpfNetMaps.setPullAtomCallback(mContext);
@@ -5683,6 +5699,7 @@
         // destroyed pending replacement they will be sent when it is disconnected.
         maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
         updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
+        updateLocalNetworkAddresses(null, nai.linkProperties);
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -9583,6 +9600,8 @@
 
         updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
 
+        updateLocalNetworkAddresses(newLp, oldLp);
+
         updateMtu(newLp, oldLp);
         // TODO - figure out what to do for clat
 //        for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9762,6 +9781,219 @@
     }
 
     /**
+     * Update Local Network Addresses to LocalNetAccess BPF map.
+     * @param newLp new link properties
+     * @param oldLp old link properties
+     */
+    private void updateLocalNetworkAddresses(@Nullable final LinkProperties newLp,
+            @NonNull final LinkProperties oldLp) {
+
+        // The maps are available only after 25Q2 release
+        if (!BpfNetMaps.isAtLeast25Q2()) {
+            return;
+        }
+
+        final CompareResult<String> interfaceDiff = new CompareResult<>(
+                oldLp != null ? oldLp.getAllInterfaceNames() : null,
+                newLp != null ? newLp.getAllInterfaceNames() : null);
+
+        for (final String iface : interfaceDiff.added) {
+            addLocalAddressesToBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, newLp);
+        }
+        for (final String iface : interfaceDiff.removed) {
+            removeLocalAddressesFromBpfMap(iface, MULTICAST_AND_BROADCAST_PREFIXES, oldLp);
+        }
+
+        // The both list contain current link properties + stacked links for new and old LP.
+        List<LinkProperties> newLinkProperties = new ArrayList<>();
+        List<LinkProperties> oldLinkProperties = new ArrayList<>();
+
+        if (newLp != null) {
+            newLinkProperties.add(newLp);
+            newLinkProperties.addAll(newLp.getStackedLinks());
+        }
+        if (oldLp != null) {
+            oldLinkProperties.add(oldLp);
+            oldLinkProperties.addAll(oldLp.getStackedLinks());
+        }
+
+        // map contains interface name to list of local network prefixes added because of change
+        // in link properties
+        Map<String, List<IpPrefix>> prefixesAddedForInterface = new ArrayMap<>();
+
+        final CompareResult<LinkProperties> linkPropertiesDiff = new CompareResult<>(
+                oldLinkProperties, newLinkProperties);
+
+        for (LinkProperties linkProperty : linkPropertiesDiff.added) {
+            List<IpPrefix> unicastLocalPrefixesToBeAdded = new ArrayList<>();
+            for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+                unicastLocalPrefixesToBeAdded.addAll(
+                        getLocalNetworkPrefixesForAddress(linkAddress));
+            }
+            addLocalAddressesToBpfMap(linkProperty.getInterfaceName(),
+                    unicastLocalPrefixesToBeAdded, linkProperty);
+
+            // adding iterface name -> ip prefixes that we added to map
+            if (!prefixesAddedForInterface.containsKey(linkProperty.getInterfaceName())) {
+                prefixesAddedForInterface.put(linkProperty.getInterfaceName(), new ArrayList<>());
+            }
+            prefixesAddedForInterface.get(linkProperty.getInterfaceName())
+                    .addAll(unicastLocalPrefixesToBeAdded);
+        }
+
+        for (LinkProperties linkProperty : linkPropertiesDiff.removed) {
+            List<IpPrefix> unicastLocalPrefixesToBeRemoved = new ArrayList<>();
+            List<IpPrefix> unicastLocalPrefixesAdded = prefixesAddedForInterface.getOrDefault(
+                    linkProperty.getInterfaceName(), new ArrayList<>());
+
+            for (LinkAddress linkAddress : linkProperty.getLinkAddresses()) {
+                unicastLocalPrefixesToBeRemoved.addAll(
+                        getLocalNetworkPrefixesForAddress(linkAddress));
+            }
+
+            // This is to ensure if 10.0.10.0/24 was added and 10.0.11.0/24 was removed both will
+            // still populate the same prefix of 10.0.0.0/8, which mean we should not remove the
+            // prefix because of removal of 10.0.11.0/24
+            unicastLocalPrefixesToBeRemoved.removeAll(unicastLocalPrefixesAdded);
+
+            removeLocalAddressesFromBpfMap(linkProperty.getInterfaceName(),
+                    new ArrayList<>(unicastLocalPrefixesToBeRemoved), linkProperty);
+        }
+    }
+
+    /**
+     * Filters IpPrefix that are local prefixes and LinkAddress is part of them.
+     * @param linkAddress link address used for filtering
+     * @return list of IpPrefix that are local addresses.
+     */
+    private List<IpPrefix> getLocalNetworkPrefixesForAddress(LinkAddress linkAddress) {
+        List<IpPrefix> localPrefixes = new ArrayList<>();
+        if (linkAddress.isIpv6()) {
+            // For IPv6, if the prefix length is greater than zero then they are part of local
+            // network
+            if (linkAddress.getPrefixLength() != 0) {
+                localPrefixes.add(
+                        new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+            }
+        } else {
+            // For IPv4, if the linkAddress is part of IpPrefix adding prefix to result.
+            for (IpPrefix ipv4LocalPrefix : IPV4_LOCAL_PREFIXES) {
+                if (ipv4LocalPrefix.containsPrefix(
+                        new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()))) {
+                    localPrefixes.add(ipv4LocalPrefix);
+                }
+            }
+        }
+        return localPrefixes;
+    }
+
+    /**
+     * Adds list of prefixes(addresses) to local network access map.
+     * @param iface interface name
+     * @param prefixes list of prefixes/addresses
+     * @param lp LinkProperties
+     */
+    private void addLocalAddressesToBpfMap(final String iface, final List<IpPrefix> prefixes,
+                                           @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+        for (IpPrefix prefix : prefixes) {
+            // Add local dnses allow rule To BpfMap before adding the block rule for prefix
+            addLocalDnsesToBpfMap(iface, prefix, lp);
+            /*
+            Prefix length is used by LPM trie map(local_net_access_map) for performing longest
+            prefix matching, this length represents the maximum number of bits used for matching.
+            The interface index should always be matched which is 32-bit integer. For IPv6, prefix
+            length is calculated by adding the ip address prefix length along with interface index
+            making it (32 + length). IPv4 addresses are stored as ipv4-mapped-ipv6 which implies
+            first 96 bits are common for all ipv4 addresses. Hence, prefix length is calculated as
+            32(interface index) + 96 (common for ipv4-mapped-ipv6) + length.
+             */
+            final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+            mBpfNetMaps.addLocalNetAccess(prefixLengthConstant + prefix.getPrefixLength(),
+                    iface, prefix.getAddress(), 0, 0, false);
+
+        }
+
+    }
+
+    /**
+     * Removes list of prefixes(addresses) from local network access map.
+     * @param iface interface name
+     * @param prefixes list of prefixes/addresses
+     * @param lp LinkProperties
+     */
+    private void removeLocalAddressesFromBpfMap(final String iface, final List<IpPrefix> prefixes,
+                                                @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2()) return;
+
+        for (IpPrefix prefix : prefixes) {
+            // The reasoning for prefix length is explained in addLocalAddressesToBpfMap()
+            final int prefixLengthConstant = (prefix.isIPv4() ? (32 + 96) : 32);
+            mBpfNetMaps.removeLocalNetAccess(prefixLengthConstant
+                    + prefix.getPrefixLength(), iface, prefix.getAddress(), 0, 0);
+
+            // Also remove the allow rule for dnses included in the prefix after removing the block
+            // rule for prefix.
+            removeLocalDnsesFromBpfMap(iface, prefix, lp);
+        }
+    }
+
+    /**
+     * Adds DNS servers to local network access map, if included in the interface prefix
+     * @param iface interface name
+     * @param prefix IpPrefix
+     * @param lp LinkProperties
+     */
+    private void addLocalDnsesToBpfMap(final String iface, IpPrefix prefix,
+            @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+        for (InetAddress dnsServer : lp.getDnsServers()) {
+            // 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,
+                        IPPROTO_UDP, 53, true);
+                mBpfNetMaps.addLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_TCP, 53, true);
+            }
+        }
+    }
+
+    /**
+     * Removes DNS servers from local network access map, if included in the interface prefix
+     * @param iface interface name
+     * @param prefix IpPrefix
+     * @param lp LinkProperties
+     */
+    private void removeLocalDnsesFromBpfMap(final String iface, IpPrefix prefix,
+            @Nullable final LinkProperties lp) {
+        if (!BpfNetMaps.isAtLeast25Q2() || lp == null) return;
+
+        for (InetAddress dnsServer : lp.getDnsServers()) {
+            // 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,
+                        IPPROTO_UDP, 53);
+                mBpfNetMaps.removeLocalNetAccess(getIpv4MappedAddressBitLen(), iface, dnsServer,
+                        IPPROTO_TCP, 53);
+            }
+        }
+    }
+
+    /**
+     * 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 15c860b..72bd858 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -16,9 +16,10 @@
 
 package com.android.server;
 
-import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
 import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
 import static android.net.L2capNetworkSpecifier.PSM_ANY;
+import static android.net.L2capNetworkSpecifier.ROLE_CLIENT;
 import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
@@ -29,13 +30,13 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
-import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
 import static android.system.OsConstants.F_GETFL;
 import static android.system.OsConstants.F_SETFL;
 import static android.system.OsConstants.O_NONBLOCK;
 
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
@@ -48,12 +49,11 @@
 import android.net.NetworkProvider.NetworkOfferCallback;
 import android.net.NetworkRequest;
 import android.net.NetworkScore;
-import android.net.NetworkSpecifier;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.system.Os;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -62,6 +62,9 @@
 import com.android.server.net.L2capNetwork;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 
@@ -88,6 +91,7 @@
     private final NetworkProvider mProvider;
     private final BlanketReservationOffer mBlanketOffer;
     private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+    private final ClientOffer mClientOffer;
     // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
     @Nullable
     private BluetoothManager mBluetoothManager;
@@ -104,12 +108,7 @@
      * requests that do not have a NetworkSpecifier set.
      */
     private class BlanketReservationOffer implements NetworkOfferCallback {
-        // Set as transport primary to ensure that the BlanketReservationOffer always outscores the
-        // ReservedServerOffer, because as soon as the BlanketReservationOffer receives an
-        // onNetworkUnneeded() callback, it will tear down the associated reserved offer.
-        public static final NetworkScore SCORE = new NetworkScore.Builder()
-                .setTransportPrimary(true)
-                .build();
+        public static final NetworkScore SCORE = new NetworkScore.Builder().build();
         // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
         public static final NetworkCapabilities CAPABILITIES;
         static {
@@ -127,12 +126,7 @@
         }
 
         // TODO: consider moving this into L2capNetworkSpecifier as #isValidServerReservation().
-        private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
-            if (spec == null) return false;
-            // If spec is not null, L2capNetworkSpecifier#canBeSatisfiedBy() guarantees the
-            // specifier is of type L2capNetworkSpecifier.
-            final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
-
+        private boolean isValidL2capServerSpecifier(L2capNetworkSpecifier l2capSpec) {
             // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
             if (l2capSpec.getRole() != ROLE_SERVER) return false;
 
@@ -150,9 +144,13 @@
 
         @Override
         public void onNetworkNeeded(NetworkRequest request) {
-            Log.d(TAG, "New reservation request: " + request);
-            if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
-                Log.w(TAG, "Ignoring invalid reservation request: " + request);
+            // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+            // this cast is safe.
+            final L2capNetworkSpecifier specifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+            if (specifier == null) return;
+            if (!isValidL2capServerSpecifier(specifier)) {
+                Log.i(TAG, "Ignoring invalid reservation request: " + request);
                 return;
             }
 
@@ -237,48 +235,128 @@
     }
 
     @Nullable
-    private static ParcelFileDescriptor createTunInterface(String ifname) {
-        final ParcelFileDescriptor fd;
-        try {
-            fd = ParcelFileDescriptor.adoptFd(
-                    ServiceConnectivityJni.createTunTap(
-                            true /*isTun*/, true /*hasCarrier*/, true /*setIffMulticast*/, ifname));
-            ServiceConnectivityJni.bringUpInterface(ifname);
-            // TODO: consider adding a parameter to createTunTap() (or the Builder that should
-            // be added) to configure i/o blocking.
-            final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
-            Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
-        } catch (Exception e) {
-            // Note: createTunTap currently throws an IllegalStateException on failure.
-            // TODO: native functions should throw ErrnoException.
-            Log.e(TAG, "Failed to create tun interface", e);
-            return null;
-        }
-        return fd;
-    }
-
-    @Nullable
     private L2capNetwork createL2capNetwork(BluetoothSocket socket, NetworkCapabilities caps,
             L2capNetwork.ICallback cb) {
         final String ifname = TUN_IFNAME + String.valueOf(sTunIndex++);
-        final ParcelFileDescriptor tunFd = createTunInterface(ifname);
+        final ParcelFileDescriptor tunFd = mDeps.createTunInterface(ifname);
         if (tunFd == null) {
             return null;
         }
 
-        return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
+        return L2capNetwork.create(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
     }
 
+    private static void closeBluetoothSocket(BluetoothSocket socket) {
+        try {
+            socket.close();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to close BluetoothSocket", e);
+        }
+    }
 
     private class ReservedServerOffer implements NetworkOfferCallback {
         private final NetworkCapabilities mReservedCapabilities;
-        private final BluetoothServerSocket mServerSocket;
+        private final AcceptThread mAcceptThread;
+        // This set should almost always contain at most one network. This is because all L2CAP
+        // server networks created by the same reserved offer are indistinguishable from each other,
+        // so that ConnectivityService will tear down all but the first. However, temporarily, there
+        // can be more than one network.
+        private final Set<L2capNetwork> mL2capNetworks = new ArraySet<>();
+
+        private class AcceptThread extends Thread {
+            private static final int TIMEOUT_MS = 500;
+            private final BluetoothServerSocket mServerSocket;
+            private volatile boolean mIsRunning = true;
+
+            public AcceptThread(BluetoothServerSocket serverSocket) {
+                mServerSocket = serverSocket;
+            }
+
+            private void postDestroyAndUnregisterReservedOffer() {
+                mHandler.post(() -> {
+                    destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+                });
+            }
+
+            private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
+                mHandler.post(() -> {
+                    final boolean success = createServerNetwork(connectedSocket);
+                    if (!success) closeBluetoothSocket(connectedSocket);
+                });
+            }
+
+            public void run() {
+                while (mIsRunning) {
+                    final BluetoothSocket connectedSocket;
+                    try {
+                        connectedSocket = mServerSocket.accept();
+                    } catch (IOException e) {
+                        // BluetoothServerSocket was closed().
+                        if (!mIsRunning) return;
+
+                        // Else, BluetoothServerSocket encountered exception.
+                        Log.e(TAG, "BluetoothServerSocket#accept failed", e);
+                        postDestroyAndUnregisterReservedOffer();
+                        return; // stop running immediately on error
+                    }
+                    postCreateServerNetwork(connectedSocket);
+                }
+            }
+
+            public void tearDown() {
+                mIsRunning = false;
+                try {
+                    // BluetoothServerSocket.close() is thread-safe.
+                    mServerSocket.close();
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+                }
+                try {
+                    join();
+                } catch (InterruptedException e) {
+                    // join() interrupted during tearDown(). Do nothing.
+                }
+            }
+        }
+
+        private boolean createServerNetwork(BluetoothSocket socket) {
+            // It is possible the offer went away.
+            if (!mReservedServerOffers.contains(this)) return false;
+
+            if (!socket.isConnected()) {
+                Log.wtf(TAG, "BluetoothSocket must be connected");
+                return false;
+            }
+
+            final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
+                    new L2capNetwork.ICallback() {
+                @Override
+                public void onError(L2capNetwork network) {
+                    destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+                }
+                @Override
+                public void onNetworkUnwanted(L2capNetwork network) {
+                    // Leave reservation in place.
+                    final boolean networkExists = mL2capNetworks.remove(network);
+                    if (!networkExists) return; // already torn down.
+                    network.tearDown();
+                }
+            });
+
+            if (network == null) {
+                Log.e(TAG, "Failed to create L2capNetwork");
+                return false;
+            }
+
+            mL2capNetworks.add(network);
+            return true;
+        }
 
         public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
                 BluetoothServerSocket serverSocket) {
             mReservedCapabilities = reservedCapabilities;
-            // TODO: ServerSocket will be managed by an AcceptThread.
-            mServerSocket = serverSocket;
+            mAcceptThread = new AcceptThread(serverSocket);
+            mAcceptThread.start();
         }
 
         public NetworkCapabilities getReservedCapabilities() {
@@ -287,41 +365,292 @@
 
         @Override
         public void onNetworkNeeded(NetworkRequest request) {
-            // TODO: implement
+            // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
         }
 
         @Override
         public void onNetworkUnneeded(NetworkRequest request) {
-            // TODO: implement
+            // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
+        }
+
+        /** Called when the reservation goes away and the reserved offer must be torn down. */
+        public void tearDown() {
+            mAcceptThread.tearDown();
+            for (L2capNetwork network : mL2capNetworks) {
+                network.tearDown();
+            }
+        }
+    }
+
+    private class ClientOffer implements NetworkOfferCallback {
+        public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+        public static final NetworkCapabilities CAPABILITIES;
+        static {
+            // Below capabilities will match any request with an L2capNetworkSpecifier
+            // that specifies ROLE_CLIENT or without a NetworkSpecifier.
+            final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_CLIENT)
+                    .build();
+            CAPABILITIES = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+                    .setNetworkSpecifier(l2capNetworkSpecifier)
+                    .build();
+        }
+
+        private final Map<L2capNetworkSpecifier, ClientRequestInfo> mClientNetworkRequests =
+                new ArrayMap<>();
+
+        /**
+         * State object to store information for client NetworkRequests.
+         */
+        private static class ClientRequestInfo {
+            public final L2capNetworkSpecifier specifier;
+            public final List<NetworkRequest> requests = new ArrayList<>();
+            // TODO: add support for retries.
+            public final ConnectThread connectThread;
+            @Nullable
+            public L2capNetwork network;
+
+            public ClientRequestInfo(NetworkRequest request, ConnectThread connectThread) {
+                this.specifier = (L2capNetworkSpecifier) request.getNetworkSpecifier();
+                this.requests.add(request);
+                this.connectThread = connectThread;
+            }
+        }
+
+        // TODO: consider using ExecutorService
+        private class ConnectThread extends Thread {
+            private final L2capNetworkSpecifier mSpecifier;
+            private final BluetoothSocket mSocket;
+            private volatile boolean mIsAborted = false;
+
+            public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+                mSpecifier = specifier;
+                mSocket = socket;
+            }
+
+            public void run() {
+                try {
+                    mSocket.connect();
+                    mHandler.post(() -> {
+                        final boolean success = createClientNetwork(mSpecifier, mSocket);
+                        if (!success) closeBluetoothSocket(mSocket);
+                    });
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to connect", e);
+                    if (mIsAborted) return;
+
+                    closeBluetoothSocket(mSocket);
+                    mHandler.post(() -> {
+                        declareAllNetworkRequestsUnfulfillable(mSpecifier);
+                    });
+                }
+            }
+
+            public void abort() {
+                mIsAborted = true;
+                // Closing the BluetoothSocket is the only way to unblock connect() because it calls
+                // shutdown on the underlying (connected) SOCK_SEQPACKET.
+                // It is safe to call BluetoothSocket#close() multiple times.
+                closeBluetoothSocket(mSocket);
+                try {
+                    join();
+                } catch (InterruptedException e) {
+                    Log.i(TAG, "Interrupted while joining ConnectThread", e);
+                }
+            }
+        }
+
+        private boolean createClientNetwork(L2capNetworkSpecifier specifier,
+                BluetoothSocket socket) {
+            // Check whether request still exists
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return false;
+
+            final NetworkCapabilities caps = new NetworkCapabilities.Builder(CAPABILITIES)
+                    .setNetworkSpecifier(specifier)
+                    .build();
+
+            final L2capNetwork network = createL2capNetwork(socket, caps,
+                    new L2capNetwork.ICallback() {
+                // TODO: do not send onUnavailable() after the network has become available. The
+                // right thing to do here is to tearDown the network (if it still exists, because
+                // note that the request might have already been removed in the meantime, so
+                // `network` cannot be used directly.
+                @Override
+                public void onError(L2capNetwork network) {
+                    declareAllNetworkRequestsUnfulfillable(specifier);
+                }
+                @Override
+                public void onNetworkUnwanted(L2capNetwork network) {
+                    declareAllNetworkRequestsUnfulfillable(specifier);
+                }
+            });
+            if (network == null) return false;
+
+            cri.network = network;
+            return true;
+        }
+
+        private boolean isValidL2capClientSpecifier(L2capNetworkSpecifier l2capSpec) {
+            // The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
+            if (l2capSpec.getRole() != ROLE_CLIENT) return false;
+
+            // HEADER_COMPRESSION_ANY is never valid in a request.
+            if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
+
+            // remoteAddr must not be null for ROLE_CLIENT requests.
+            if (l2capSpec.getRemoteAddress() == null) return false;
+
+            // Client network requests require a PSM to be specified.
+            // Ensure the PSM is within the valid range of dynamic BLE L2CAP values.
+            if (l2capSpec.getPsm() < 0x80) return false;
+            if (l2capSpec.getPsm() > 0xFF) return false;
+
+            return true;
+        }
+
+        @Override
+        public void onNetworkNeeded(NetworkRequest request) {
+            // The NetworkSpecifier is guaranteed to be either null or an L2capNetworkSpecifier, so
+            // this cast is safe.
+            final L2capNetworkSpecifier requestSpecifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+            if (requestSpecifier == null) return;
+            if (!isValidL2capClientSpecifier(requestSpecifier)) {
+                Log.i(TAG, "Ignoring invalid client request: " + request);
+                return;
+            }
+
+             // Check whether this exact request is already being tracked.
+            final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
+            if (cri != null) {
+                Log.d(TAG, "The request is already being tracked. NetworkRequest: " + request);
+                cri.requests.add(request);
+                return;
+            }
+
+            // Check whether a fuzzy match shows a mismatch in header compression by calling
+            // canBeSatisfiedBy().
+            // TODO: Add a copy constructor to L2capNetworkSpecifier.Builder.
+            final L2capNetworkSpecifier matchAnyHeaderCompressionSpecifier =
+                    new L2capNetworkSpecifier.Builder()
+                            .setRole(requestSpecifier.getRole())
+                            .setRemoteAddress(requestSpecifier.getRemoteAddress())
+                            .setPsm(requestSpecifier.getPsm())
+                            .setHeaderCompression(HEADER_COMPRESSION_ANY)
+                            .build();
+            for (L2capNetworkSpecifier existingSpecifier : mClientNetworkRequests.keySet()) {
+                if (existingSpecifier.canBeSatisfiedBy(matchAnyHeaderCompressionSpecifier)) {
+                    // This requeset can never be serviced as this network already exists with a
+                    // different header compression mechanism.
+                    mProvider.declareNetworkRequestUnfulfillable(request);
+                    return;
+                }
+            }
+
+            // If the code reaches here, this is a new request.
+            final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+            if (bluetoothAdapter == null) {
+                Log.w(TAG, "Failed to get BluetoothAdapter");
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final byte[] macAddress = requestSpecifier.getRemoteAddress().toByteArray();
+            final BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress);
+            final BluetoothSocket socket;
+            try {
+                socket = bluetoothDevice.createInsecureL2capChannel(requestSpecifier.getPsm());
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to createInsecureL2capChannel", e);
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final ConnectThread connectThread = new ConnectThread(requestSpecifier, socket);
+            connectThread.start();
+            final ClientRequestInfo newRequestInfo = new ClientRequestInfo(request, connectThread);
+            mClientNetworkRequests.put(requestSpecifier, newRequestInfo);
+        }
+
+        @Override
+        public void onNetworkUnneeded(NetworkRequest request) {
+            final L2capNetworkSpecifier specifier =
+                    (L2capNetworkSpecifier) request.getNetworkSpecifier();
+
+            // Map#get() is safe to call with null key
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return;
+
+            cri.requests.remove(request);
+            if (cri.requests.size() > 0) return;
+
+            // If the code reaches here, the network needs to be torn down.
+            releaseClientNetworkRequest(cri);
         }
 
         /**
-         * Called when the reservation goes away and the reserved offer must be torn down.
+         * Release the client network request and tear down all associated state.
          *
-         * This method can be called multiple times.
+         * Only call this when all associated NetworkRequests have been released.
          */
-        public void tearDown() {
-            try {
-                mServerSocket.close();
-            } catch (IOException e) {
-                Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+        private void releaseClientNetworkRequest(ClientRequestInfo cri) {
+            mClientNetworkRequests.remove(cri.specifier);
+            if (cri.connectThread.isAlive()) {
+                // Note that if ConnectThread succeeds between calling #isAlive() and #abort(), the
+                // request will already be removed from mClientNetworkRequests by the time the
+                // createClientNetwork() call is processed on the handler, so it is safe to call
+                // #abort().
+                cri.connectThread.abort();
             }
+
+            if (cri.network != null) {
+                cri.network.tearDown();
+            }
+        }
+
+        private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return;
+
+            for (NetworkRequest request : cri.requests) {
+                mProvider.declareNetworkRequestUnfulfillable(request);
+            }
+            releaseClientNetworkRequest(cri);
         }
     }
 
     @VisibleForTesting
     public static class Dependencies {
-        /** Get NetworkProvider */
-        public NetworkProvider getNetworkProvider(Context context, Looper looper) {
-            return new NetworkProvider(context, looper, TAG);
-        }
-
         /** Get the HandlerThread for L2capNetworkProvider to run on */
         public HandlerThread getHandlerThread() {
             final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
             thread.start();
             return thread;
         }
+
+        @Nullable
+        public ParcelFileDescriptor createTunInterface(String ifname) {
+            final ParcelFileDescriptor fd;
+            try {
+                fd = ParcelFileDescriptor.adoptFd(ServiceConnectivityJni.createTunTap(
+                        true /*isTun*/,
+                        true /*hasCarrier*/,
+                        true /*setIffMulticast*/,
+                        ifname));
+                ServiceConnectivityJni.bringUpInterface(ifname);
+                // TODO: consider adding a parameter to createTunTap() (or the Builder that should
+                // be added) to configure i/o blocking.
+                final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
+                Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
+            } catch (Exception e) {
+                // Note: createTunTap currently throws an IllegalStateException on failure.
+                // TODO: native functions should throw ErrnoException.
+                Log.e(TAG, "Failed to create tun interface", e);
+                return null;
+            }
+            return fd;
+        }
     }
 
     public L2capNetworkProvider(Context context) {
@@ -334,8 +663,9 @@
         mContext = context;
         mHandlerThread = mDeps.getHandlerThread();
         mHandler = new Handler(mHandlerThread.getLooper());
-        mProvider = mDeps.getNetworkProvider(context, mHandlerThread.getLooper());
+        mProvider = new NetworkProvider(context, mHandlerThread.getLooper(), TAG);
         mBlanketOffer = new BlanketReservationOffer();
+        mClientOffer = new ClientOffer();
     }
 
     /**
@@ -358,6 +688,8 @@
             mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
             mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
                     BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+            mProvider.registerNetworkOffer(ClientOffer.SCORE,
+                    ClientOffer.CAPABILITIES, mHandler::post, mClientOffer);
         });
     }
 }
diff --git a/service/src/com/android/server/net/HeaderCompressionUtils.java b/service/src/com/android/server/net/HeaderCompressionUtils.java
new file mode 100644
index 0000000..5bd3a76
--- /dev/null
+++ b/service/src/com/android/server/net/HeaderCompressionUtils.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2025 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.server.net;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class HeaderCompressionUtils {
+    private static final String TAG = "L2capHeaderCompressionUtils";
+    private static final int IPV6_HEADER_SIZE = 40;
+
+    private static byte[] decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)
+            throws BufferUnderflowException, IOException {
+        // Mode is equivalent between SAM and DAM; however, isMulticast only applies to DAM.
+        final byte[] address = new byte[16];
+        // If multicast bit is set, mix it in the mode, so that the lower two bits represent the
+        // address mode, and the upper bit represents multicast compression.
+        switch ((isMulticast ? 0b100 : 0) | mode) {
+            case 0b000: // 128 bits. The full address is carried in-line.
+            case 0b100:
+                buffer.get(address);
+                break;
+            case 0b001: // 64 bits. The first 64-bits of the fe80:: address are elided.
+                address[0] = (byte) 0xfe;
+                address[1] = (byte) 0x80;
+                buffer.get(address, 8 /*off*/, 8 /*len*/);
+                break;
+            case 0b010: // 16 bits. fe80::ff:fe00:XXXX, where XXXX are the bits carried in-line
+                address[0] = (byte) 0xfe;
+                address[1] = (byte) 0x80;
+                address[11] = (byte) 0xff;
+                address[12] = (byte) 0xfe;
+                buffer.get(address, 14 /*off*/, 2 /*len*/);
+                break;
+            case 0b011: // 0 bits. The address is fully elided and derived from BLE MAC address
+                // Note that on Android, the BLE MAC addresses are not exposed via the API;
+                // therefore, this compression mode cannot be supported.
+                throw new IOException("Address cannot be fully elided");
+            case 0b101: // 48 bits. The address takes the form ffXX::00XX:XXXX:XXXX.
+                address[0] = (byte) 0xff;
+                address[1] = buffer.get();
+                buffer.get(address, 11 /*off*/, 5 /*len*/);
+                break;
+            case 0b110: // 32 bits. The address takes the form ffXX::00XX:XXXX
+                address[0] = (byte) 0xff;
+                address[1] = buffer.get();
+                buffer.get(address, 13 /*off*/, 3 /*len*/);
+                break;
+            case 0b111: // 8 bits. The address takes the form ff02::00XX.
+                address[0] = (byte) 0xff;
+                address[1] = (byte) 0x02;
+                address[15] = buffer.get();
+                break;
+        }
+        return address;
+    }
+
+    /**
+     * Performs 6lowpan header decompression in place.
+     *
+     * Note that the passed in buffer must have enough capacity for successful decompression.
+     *
+     * @param bytes The buffer containing the packet.
+     * @param len The size of the packet
+     * @return decompressed size or zero
+     * @throws BufferUnderflowException if an illegal packet is encountered.
+     * @throws IOException if an unsupported option is encountered.
+     */
+    public static int decompress6lowpan(byte[] bytes, int len)
+            throws BufferUnderflowException, IOException {
+        // Note that ByteBuffer's default byte order is big endian.
+        final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+        inBuffer.limit(len);
+
+        // LOWPAN_IPHC base encoding:
+        //   0   1   2   3   4   5   6   7 | 8   9  10  11  12  13  14  15
+        // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+        // | 0 | 1 | 1 |  TF   |NH | HLIM  |CID|SAC|  SAM  | M |DAC|  DAM  |
+        // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
+        final int iphc1 = inBuffer.get() & 0xff;
+        final int iphc2 = inBuffer.get() & 0xff;
+        // Dispatch must start with 0b011.
+        if ((iphc1 & 0xe0) != 0x60) {
+            throw new IOException("LOWPAN_IPHC does not start with 011");
+        }
+
+        final int tf = (iphc1 >> 3) & 3;         // Traffic class
+        final boolean nh = (iphc1 & 4) != 0;     // Next header
+        final int hlim = iphc1 & 3;              // Hop limit
+        final boolean cid = (iphc2 & 0x80) != 0; // Context identifier extension
+        final boolean sac = (iphc2 & 0x40) != 0; // Source address compression
+        final int sam = (iphc2 >> 4) & 3;        // Source address mode
+        final boolean m = (iphc2 & 8) != 0;      // Multicast compression
+        final boolean dac = (iphc2 & 4) != 0;    // Destination address compression
+        final int dam = iphc2 & 3;               // Destination address mode
+
+        final ByteBuffer ipv6Header = ByteBuffer.allocate(IPV6_HEADER_SIZE);
+
+        final int trafficClass;
+        final int flowLabel;
+        switch (tf) {
+            case 0b00: // ECN + DSCP + 4-bit Pad + Flow Label (4 bytes)
+                trafficClass = inBuffer.get() & 0xff;
+                flowLabel = (inBuffer.get() & 0x0f) << 16
+                        | (inBuffer.get() & 0xff) << 8
+                        | (inBuffer.get() & 0xff);
+                break;
+            case 0b01: // ECN + 2-bit Pad + Flow Label (3 bytes), DSCP is elided.
+                final int firstByte = inBuffer.get() & 0xff;
+                //     0     1     2     3     4     5     6     7
+                // +-----+-----+-----+-----+-----+-----+-----+-----+
+                // |          DS FIELD, DSCP           | ECN FIELD |
+                // +-----+-----+-----+-----+-----+-----+-----+-----+
+                // rfc6282 does not explicitly state what value to use for DSCP, assuming 0.
+                trafficClass = firstByte >> 6;
+                flowLabel = (firstByte & 0x0f) << 16
+                        | (inBuffer.get() & 0xff) << 8
+                        | (inBuffer.get() & 0xff);
+                break;
+            case 0b10: // ECN + DSCP (1 byte), Flow Label is elided.
+                trafficClass = inBuffer.get() & 0xff;
+                // rfc6282 does not explicitly state what value to use, assuming 0.
+                flowLabel = 0;
+                break;
+            case 0b11: // Traffic Class and Flow Label are elided.
+                // rfc6282 does not explicitly state what value to use, assuming 0.
+                trafficClass = 0;
+                flowLabel = 0;
+                break;
+            default:
+                // This cannot happen. Crash if it does.
+                throw new IllegalStateException("Illegal TF value");
+        }
+
+        // Write version, traffic class, and flow label
+        final int versionTcFlowLabel = (6 << 28) | (trafficClass << 20) | flowLabel;
+        ipv6Header.putInt(versionTcFlowLabel);
+
+        // Payload length is still unknown. Use 0 for now.
+        ipv6Header.putShort((short) 0);
+
+        // We do not use UDP or extension header compression, therefore the next header
+        // cannot be compressed.
+        if (nh) throw new IOException("Next header cannot be compressed");
+        // Write next header
+        ipv6Header.put(inBuffer.get());
+
+        final byte hopLimit;
+        switch (hlim) {
+            case 0b00: // The Hop Limit field is carried in-line.
+                hopLimit = inBuffer.get();
+                break;
+            case 0b01: // The Hop Limit field is compressed and the hop limit is 1.
+                hopLimit = 1;
+                break;
+            case 0b10: // The Hop Limit field is compressed and the hop limit is 64.
+                hopLimit = 64;
+                break;
+            case 0b11: // The Hop Limit field is compressed and the hop limit is 255.
+                hopLimit = (byte) 255;
+                break;
+            default:
+                // This cannot happen. Crash if it does.
+                throw new IllegalStateException("Illegal HLIM value");
+        }
+        ipv6Header.put(hopLimit);
+
+        if (cid) throw new IOException("Context based compression not supported");
+        if (sac) throw new IOException("Context based compression not supported");
+        if (dac) throw new IOException("Context based compression not supported");
+
+        // Write source address
+        ipv6Header.put(decodeIpv6Address(inBuffer, sam, false /* isMulticast */));
+
+        // Write destination address
+        ipv6Header.put(decodeIpv6Address(inBuffer, dam, m));
+
+        // Go back and fix up payloadLength
+        final short payloadLength = (short) inBuffer.remaining();
+        ipv6Header.putShort(4, payloadLength);
+
+        // Done! Check that 40 bytes were written.
+        if (ipv6Header.position() != IPV6_HEADER_SIZE) {
+            // This indicates a bug in our code -> crash.
+            throw new IllegalStateException("Faulty decompression wrote less than 40 bytes");
+        }
+
+        // Ensure there is enough room in the buffer
+        final int packetLength = payloadLength + IPV6_HEADER_SIZE;
+        if (bytes.length < packetLength) {
+            throw new IOException("Decompressed packet exceeds buffer size");
+        }
+
+        // Move payload bytes back to make room for the header
+        inBuffer.limit(packetLength);
+        System.arraycopy(bytes, inBuffer.position(), bytes, IPV6_HEADER_SIZE, payloadLength);
+        // Copy IPv6 header to the beginning of the buffer.
+        inBuffer.position(0);
+        ipv6Header.flip();
+        inBuffer.put(ipv6Header);
+
+        return packetLength;
+    }
+
+    /**
+     * Performs 6lowpan header compression in place.
+     *
+     * @param bytes The buffer containing the packet.
+     * @param len The size of the packet
+     * @return compressed size or zero
+     * @throws BufferUnderflowException if an illegal packet is encountered.
+     * @throws IOException if an unsupported option is encountered.
+     */
+    public static int compress6lowpan(byte[] bytes, final int len)
+            throws BufferUnderflowException, IOException {
+        // Compression only happens on egress, i.e. the packet is read from the tun fd.
+        // This means that this code can be a bit more lenient.
+        if (len < 40) {
+            Log.wtf(TAG, "Encountered short (<40 byte) packet");
+            return 0;
+        }
+
+        // Note that ByteBuffer's default byte order is big endian.
+        final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
+        inBuffer.limit(len);
+
+        // Check that the packet is an IPv6 packet
+        final int versionTcFlowLabel = inBuffer.getInt() & 0xffffffff;
+        if ((versionTcFlowLabel >> 28) != 6) {
+            return 0;
+        }
+
+        // Check that the payload length matches the packet length - 40.
+        int payloadLength = inBuffer.getShort();
+        if (payloadLength != len - IPV6_HEADER_SIZE) {
+            throw new IOException("Encountered packet with payload length mismatch");
+        }
+
+        // Implements rfc 6282 6lowpan header compression using iphc 0110 0000 0000 0000 (all
+        // fields are carried inline).
+        inBuffer.position(0);
+        inBuffer.put((byte) 0x60);
+        inBuffer.put((byte) 0x00);
+        final byte trafficClass = (byte) ((versionTcFlowLabel >> 20) & 0xff);
+        inBuffer.put(trafficClass);
+        final byte flowLabelMsb = (byte) ((versionTcFlowLabel >> 16) & 0x0f);
+        final short flowLabelLsb = (short) (versionTcFlowLabel & 0xffff);
+        inBuffer.put(flowLabelMsb);
+        // Note: the next putShort overrides the payload length. This is WAI as the payload length
+        // is reconstructed via L2CAP packet length.
+        inBuffer.putShort(flowLabelLsb);
+
+        // Since the iphc (2 bytes) matches the payload length that was elided (2 bytes), the length
+        // of the packet did not change.
+        return len;
+    }
+}
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
index b9d5f13..b624bca 100644
--- a/service/src/com/android/server/net/L2capNetwork.java
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -16,9 +16,12 @@
 
 package com.android.server.net;
 
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothSocket;
 import android.content.Context;
+import android.net.L2capNetworkSpecifier;
 import android.net.LinkProperties;
 import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
@@ -39,10 +42,8 @@
     private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
     private final String mLogTag;
     private final Handler mHandler;
-    private final String mIfname;
     private final L2capPacketForwarder mForwarder;
     private final NetworkCapabilities mNetworkCapabilities;
-    private final L2capIpClient mIpClient;
     private final NetworkAgent mNetworkAgent;
 
     /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
@@ -56,7 +57,7 @@
         @Nullable
         private IpClientManager mIpClient;
         @Nullable
-        private LinkProperties mLinkProperties;
+        private volatile LinkProperties mLinkProperties;
 
         L2capIpClient(String logTag, Context context, String ifname) {
             mLogTag = logTag;
@@ -71,11 +72,24 @@
 
         @Override
         public void onProvisioningSuccess(LinkProperties lp) {
-            Log.d(mLogTag, "Successfully provisionined l2cap tun: " + lp);
+            Log.d(mLogTag, "Successfully provisioned l2cap tun: " + lp);
             mLinkProperties = lp;
             mOnProvisioningSuccessCv.open();
         }
 
+        @Override
+        public void onProvisioningFailure(LinkProperties lp) {
+            Log.i(mLogTag, "Failed to provision l2cap tun: " + lp);
+            mLinkProperties = null;
+            mOnProvisioningSuccessCv.open();
+        }
+
+        /**
+         * Starts IPv6 link-local provisioning.
+         *
+         * @return LinkProperties on success, null on failure.
+         */
+        @Nullable
         public LinkProperties start() {
             mOnIpClientCreatedCv.block();
             // mIpClient guaranteed non-null.
@@ -99,27 +113,27 @@
         void onNetworkUnwanted(L2capNetwork network);
     }
 
-    public L2capNetwork(Handler handler, Context context, NetworkProvider provider, String ifname,
-            BluetoothSocket socket, ParcelFileDescriptor tunFd,
-            NetworkCapabilities networkCapabilities, ICallback cb) {
-        // TODO: add a check that this constructor is invoked on the handler thread.
-        mLogTag = String.format("L2capNetwork[%s]", ifname);
+    public L2capNetwork(String logTag, Handler handler, Context context, NetworkProvider provider,
+            BluetoothSocket socket, ParcelFileDescriptor tunFd, NetworkCapabilities nc,
+            LinkProperties lp, ICallback cb) {
+        mLogTag = logTag;
         mHandler = handler;
-        mIfname = ifname;
-        mForwarder = new L2capPacketForwarder(handler, tunFd, socket, () -> {
+        mNetworkCapabilities = nc;
+
+        final L2capNetworkSpecifier spec = (L2capNetworkSpecifier) nc.getNetworkSpecifier();
+        final boolean compressHeaders = spec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+        mForwarder = new L2capPacketForwarder(handler, tunFd, socket, compressHeaders, () -> {
             // TODO: add a check that this callback is invoked on the handler thread.
             cb.onError(L2capNetwork.this);
         });
-        mNetworkCapabilities = networkCapabilities;
-        mIpClient = new L2capIpClient(mLogTag, context, ifname);
-        final LinkProperties linkProperties = mIpClient.start();
 
         final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
         mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
-                networkCapabilities, linkProperties, NETWORK_SCORE, config, provider) {
+                nc, lp, NETWORK_SCORE, config, provider) {
             @Override
             public void onNetworkUnwanted() {
-                Log.i(mLogTag, mIfname + ": Network is unwanted");
+                Log.i(mLogTag, "Network is unwanted");
                 // TODO: add a check that this callback is invoked on the handler thread.
                 cb.onNetworkUnwanted(L2capNetwork.this);
             }
@@ -128,6 +142,24 @@
         mNetworkAgent.markConnected();
     }
 
+    /** Create an L2capNetwork or return null on failure. */
+    @Nullable
+    public static L2capNetwork create(Handler handler, Context context, NetworkProvider provider,
+            String ifname, BluetoothSocket socket, ParcelFileDescriptor tunFd,
+            NetworkCapabilities nc, ICallback cb) {
+        // TODO: add a check that this function is invoked on the handler thread.
+        final String logTag = String.format("L2capNetwork[%s]", ifname);
+
+        // L2capIpClient#start() blocks until provisioning either succeeds (and returns
+        // LinkProperties) or fails (and returns null).
+        // Note that since L2capNetwork is using IPv6 link-local provisioning the most likely
+        // (only?) failure mode is due to the interface disappearing.
+        final LinkProperties lp = new L2capIpClient(logTag, context, ifname).start();
+        if (lp == null) return null;
+
+        return new L2capNetwork(logTag, handler, context, provider, socket, tunFd, nc, lp, cb);
+    }
+
     /** Get the NetworkCapabilities used for this Network */
     public NetworkCapabilities getNetworkCapabilities() {
         return mNetworkCapabilities;
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
index cef351c..00faecf 100644
--- a/service/src/com/android/server/net/L2capPacketForwarder.java
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -16,6 +16,9 @@
 
 package com.android.server.net;
 
+import static com.android.server.net.HeaderCompressionUtils.compress6lowpan;
+import static com.android.server.net.HeaderCompressionUtils.decompress6lowpan;
+
 import android.bluetooth.BluetoothSocket;
 import android.os.Handler;
 import android.os.ParcelFileDescriptor;
@@ -29,6 +32,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.BufferUnderflowException;
 
 /**
  * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
@@ -222,13 +226,18 @@
         private volatile boolean mIsRunning = true;
 
         private final String mLogTag;
-        private IReadWriteFd mReadFd;
-        private IReadWriteFd mWriteFd;
+        private final IReadWriteFd mReadFd;
+        private final IReadWriteFd mWriteFd;
+        private final boolean mIsIngress;
+        private final boolean mCompressHeaders;
 
-        L2capThread(String logTag, IReadWriteFd readFd, IReadWriteFd writeFd) {
-            mLogTag = logTag;
+        L2capThread(IReadWriteFd readFd, IReadWriteFd writeFd, boolean isIngress,
+                boolean compressHeaders) {
+            mLogTag = isIngress ? "L2capForwarderThread-Ingress" : "L2capForwarderThread-Egress";
             mReadFd = readFd;
             mWriteFd = writeFd;
+            mIsIngress = isIngress;
+            mCompressHeaders = compressHeaders;
         }
 
         private void postOnError() {
@@ -242,20 +251,28 @@
         public void run() {
             while (mIsRunning) {
                 try {
-                    final int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+                    int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
                     // No bytes to write, continue.
                     if (readBytes <= 0) {
                         Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
                         continue;
                     }
 
-                    // If the packet exceeds MTU, drop it.
+                    if (mCompressHeaders) {
+                        if (mIsIngress) {
+                            readBytes = decompress6lowpan(mBuffer, readBytes);
+                        } else {
+                            readBytes = compress6lowpan(mBuffer, readBytes);
+                        }
+                    }
+
+                    // If the packet is 0-length post de/compression or exceeds MTU, drop it.
                     // Note that a large read on BluetoothSocket throws an IOException to tear down
                     // the network.
-                    if (readBytes > MTU) continue;
+                    if (readBytes <= 0 || readBytes > MTU) continue;
 
                     mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
-                } catch (IOException e) {
+                } catch (IOException|BufferUnderflowException e) {
                     Log.e(mLogTag, "L2capThread exception", e);
                     // Tear down the network on any error.
                     mIsRunning = false;
@@ -273,19 +290,20 @@
     }
 
     public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
-            ICallback cb) {
-        this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), cb);
+            boolean compressHdrs, ICallback cb) {
+        this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), compressHdrs, cb);
     }
 
     @VisibleForTesting
-    L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd, ICallback cb) {
+    L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd,
+            boolean compressHeaders, ICallback cb) {
         mHandler = handler;
         mTunFd = tunFd;
         mL2capFd = l2capFd;
         mCallback = cb;
 
-        mIngressThread = new L2capThread("L2capThread-Ingress", l2capFd, tunFd);
-        mEgressThread = new L2capThread("L2capThread-Egress", tunFd, l2capFd);
+        mIngressThread = new L2capThread(l2capFd, tunFd, true /*isIngress*/, compressHeaders);
+        mEgressThread = new L2capThread(tunFd, l2capFd, false /*isIngress*/, compressHeaders);
 
         mIngressThread.start();
         mEgressThread.start();
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 026e985..8d7ae5c 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -23,6 +23,7 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.List;
 
 /**
  * Network constants used by the network stack.
@@ -341,6 +342,26 @@
      */
     public static final String TEST_URL_EXPIRATION_TIME = "test_url_expiration_time";
 
+    /**
+     * List of IpPrefix that are local network prefixes.
+     */
+    public static final List<IpPrefix> IPV4_LOCAL_PREFIXES = List.of(
+            new IpPrefix("169.254.0.0/16"), // Link Local
+            new IpPrefix("100.64.0.0/10"),  // CGNAT
+            new IpPrefix("10.0.0.0/8"),     // RFC1918
+            new IpPrefix("172.16.0.0/12"),  // RFC1918
+            new IpPrefix("192.168.0.0/16")  // RFC1918
+    );
+
+    /**
+     * List of IpPrefix that are multicast and broadcast prefixes.
+     */
+    public static final List<IpPrefix> MULTICAST_AND_BROADCAST_PREFIXES = List.of(
+            new IpPrefix("224.0.0.0/4"),               // Multicast
+            new IpPrefix("ff00::/8"),                  // Multicast
+            new IpPrefix("255.255.255.255/32")         // Broadcast
+    );
+
     // TODO: Move to Inet4AddressUtils
     // See aosp/1455936: NetworkStackConstants can't depend on it as it causes jarjar-related issues
     // for users of both the net-utils-device-common and net-utils-framework-common libraries.
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index c63de1f..710f8a8 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -95,7 +95,9 @@
   hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
 
   # Make the client connects to the hotspot.
-  client_network = client.connectToWifi(test_ssid, test_passphrase)
+  client_network = client.connectToWifi(
+      test_ssid, test_passphrase, upstream_type != UpstreamType.NONE
+  )
 
   return hotspot_interface, client_network
 
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 6da7e9a..252052e 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -27,13 +27,11 @@
 import android.net.NetworkRequest
 import android.net.cts.util.CtsNetUtils
 import android.net.cts.util.CtsTetheringUtils
-import android.net.wifi.ScanResult
 import android.net.wifi.SoftApConfiguration
 import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
 import android.net.wifi.WifiConfiguration
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
-import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PropertyUtil
@@ -109,10 +107,7 @@
     // Suppress warning because WifiManager methods to connect to a config are
     // documented not to be deprecated for privileged users.
     @Suppress("DEPRECATION")
-    fun connectToWifi(ssid: String, passphrase: String): Long {
-        val specifier = WifiNetworkSpecifier.Builder()
-            .setBand(ScanResult.WIFI_BAND_24_GHZ)
-            .build()
+    fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Long {
         val wifiConfig = WifiConfiguration()
         wifiConfig.SSID = "\"" + ssid + "\""
         wifiConfig.preSharedKey = "\"" + passphrase + "\""
@@ -141,7 +136,8 @@
             return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
                 // Remove double quotes.
                 val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
-                ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+                ssidFromCaps == ssid && (!requireValidation ||
+                        it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
             }.network.networkHandle
         }
     }
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index ee2e7db..8e790ca 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -489,15 +489,15 @@
 
     fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
         // If not IPv6 -> PASS
-        addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+        addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
         addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
 
         // If not ICMPv6 -> PASS
-        addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+        addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
         addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
 
         // If not echo reply -> PASS
-        addLoad8(R0, ICMP6_TYPE_OFFSET)
+        addLoad8intoR0(ICMP6_TYPE_OFFSET)
         addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
     }
 
@@ -744,11 +744,11 @@
         //     transmit 3 ICMPv6 echo requests with random first byte
         //     increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
         //     drop
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+        gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
                 .addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
-                .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+                .addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
                 .addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
-                .addLoad8(R0, ICMP6_TYPE_OFFSET)
+                .addLoad8intoR0(ICMP6_TYPE_OFFSET)
                 .addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
                 .addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
                 .addCountAndPassIfR0Equals(
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index e94d94f..4a21f09 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -375,6 +375,7 @@
     }
 
     private static boolean isIpv6UdpEncapSupportedByKernel() {
+        if (SdkLevel.isAtLeastB() && isKernelVersionAtLeast("5.10.0")) return true;
         return isKernelVersionAtLeast("5.15.31")
                 || (isKernelVersionAtLeast("5.10.108") && !isKernelVersionAtLeast("5.15.0"));
     }
@@ -390,8 +391,8 @@
         assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
     }
 
-    // TODO: b/319532485 Figure out whether to support x86_32
     private static boolean isRequestTransformStateSupportedByKernel() {
+        if (SdkLevel.isAtLeastB()) return true;
         return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
index f05bf15..a9af34f 100644
--- a/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -16,15 +16,21 @@
 
 package android.net.cts
 
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.net.ConnectivityManager
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkProvider
@@ -43,7 +49,10 @@
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.TestableNetworkOfferCallback
 import com.android.testutils.runAsShell
+import kotlin.test.assertContains
 import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -83,6 +92,8 @@
     private val handler = Handler(handlerThread.looper)
     private val provider = NetworkProvider(context, handlerThread.looper, TAG)
 
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
     @Before
     fun setUp() {
         runAsShell(NETWORK_SETTINGS) {
@@ -92,6 +103,7 @@
 
     @After
     fun tearDown() {
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         runAsShell(NETWORK_SETTINGS) {
             // unregisterNetworkProvider unregisters all associated NetworkOffers.
             cm.unregisterNetworkProvider(provider)
@@ -104,6 +116,13 @@
         it.reservationId = resId
     }
 
+    fun reserveNetwork(nr: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.reserveNetwork(nr, handler, it)
+            registeredCallbacks.add(it)
+        }
+    }
+
     @Test
     fun testReserveNetwork() {
         // register blanket offer
@@ -112,8 +131,7 @@
             provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
         }
 
-        val cb = TestableNetworkCallback()
-        cm.reserveNetwork(ETHERNET_REQUEST, handler, cb)
+        val cb = reserveNetwork(ETHERNET_REQUEST)
 
         // validate the reservation matches the blanket offer.
         val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
@@ -137,4 +155,28 @@
         provider.unregisterNetworkOffer(reservedOffer)
         cb.expect<Unavailable>()
     }
+
+    @Test
+    fun testReserveL2capNetwork() {
+        val l2capReservationSpecifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        val l2capRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_BLUETOOTH)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setNetworkSpecifier(l2capReservationSpecifier)
+                .build()
+        val cb = runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+            reserveNetwork(l2capRequest)
+        }
+
+        val caps = cb.expect<Reserved>().caps
+        val reservedSpec = caps.networkSpecifier
+        assertTrue(reservedSpec is L2capNetworkSpecifier)
+        assertContains(0x80..0xFF, reservedSpec.psm, "PSM is outside of dynamic range")
+        assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+        assertNull(reservedSpec.remoteAddress)
+    }
 }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 375d604..cc9445d 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -55,6 +55,7 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl
 import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
+import com.android.server.L2capNetworkProvider
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
@@ -272,6 +273,8 @@
             connectivityServiceInternalHandler: Handler
         ): SatelliteAccessController? = mock(
             SatelliteAccessController::class.java)
+
+        override fun makeL2capNetworkProvider(context: Context) = null
     }
 
     @After
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 73eb24d..fd92672 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -269,9 +269,11 @@
     public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
         assertTrue(mLocalNetAccessMap.isEmpty());
 
+        // wlan2 is an incorrect interface
         mBpfNetMaps.addLocalNetAccess(160, "wlan2",
                 Inet4Address.getByName("196.68.0.0"), 0, 0, true);
 
+        // As we tried to add incorrect interface, it would be skipped and map should be empty.
         assertTrue(mLocalNetAccessMap.isEmpty());
     }
 
@@ -302,6 +304,53 @@
 
     @Test
     @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessBeforeV() {
+        assertThrows(UnsupportedOperationException.class, () ->
+                mBpfNetMaps.removeLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessAfterV() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+        mBpfNetMaps.removeLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0);
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemoveLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+        assertTrue(mLocalNetAccessMap.isEmpty());
+
+        mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+                Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+        assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("100.68.0.0"), 0, 0)));
+
+        mBpfNetMaps.removeLocalNetAccess(160, "wlan2",
+                Inet4Address.getByName("196.68.0.0"), 0, 0);
+        assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+                Inet4Address.getByName("196.68.0.0"), 0, 0)));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public void testAddUidToLocalNetBlockMapBeforeV() {
         assertThrows(UnsupportedOperationException.class, () ->
                 mBpfNetMaps.addUidToLocalNetBlockMap(0));
@@ -309,9 +358,9 @@
 
     @Test
     @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void testIsUidPresentInLocalNetBlockMapBeforeV() {
+    public void testIsUidBlockedFromUsingLocalNetworkBeforeV() {
         assertThrows(UnsupportedOperationException.class, () ->
-                mBpfNetMaps.getUidValueFromLocalNetBlockMap(0));
+                mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(0));
     }
 
     @Test
@@ -339,18 +388,18 @@
 
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void testIsUidPresentInLocalNetBlockMapAfterV() throws Exception {
+    public void testIsUidBlockedFromUsingLocalNetworkAfterV() throws Exception {
         final int uid0 = TEST_UIDS[0];
         final int uid1 = TEST_UIDS[1];
 
         assertTrue(mLocalNetAccessMap.isEmpty());
 
         mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
-        assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid0));
-        assertFalse(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+        assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid0));
+        assertFalse(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
 
         mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
-        assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+        assertTrue(mBpfNetMaps.isUidBlockedFromUsingLocalNetwork(uid1));
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index f7d7c87..1562dad 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -412,6 +412,7 @@
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
+import com.android.server.L2capNetworkProvider;
 import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
 import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
@@ -2381,6 +2382,11 @@
             // Needed to mock out the dependency on DeviceConfig
             return 15;
         }
+
+        @Override
+        public L2capNetworkProvider makeL2capNetworkProvider(Context context) {
+            return null;
+        }
     }
 
     private class AutomaticOnOffKeepaliveTrackerDependencies
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
deleted file mode 100644
index 49eb476..0000000
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ /dev/null
@@ -1,233 +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.
- */
-
-package com.android.server
-
-import android.bluetooth.BluetoothAdapter
-import android.bluetooth.BluetoothManager
-import android.bluetooth.BluetoothServerSocket
-import android.content.Context
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.TYPE_NONE
-import android.net.L2capNetworkSpecifier
-import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
-import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
-import android.net.L2capNetworkSpecifier.ROLE_SERVER
-import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
-import android.net.NetworkProvider
-import android.net.NetworkProvider.NetworkOfferCallback
-import android.net.NetworkRequest
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.waitForIdle
-import kotlin.test.assertTrue
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-private const val TAG = "L2capNetworkProviderTest"
-private const val TIMEOUT_MS = 1000
-
-private val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-    .addTransportType(TRANSPORT_BLUETOOTH)
-    .build()
-
-private val RESERVATION = NetworkRequest(
-        NetworkCapabilities(RESERVATION_CAPS),
-        TYPE_NONE,
-        42 /* rId */,
-        NetworkRequest.Type.RESERVATION
-)
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class L2capNetworkProviderTest {
-    @Mock private lateinit var context: Context
-    @Mock private lateinit var deps: L2capNetworkProvider.Dependencies
-    @Mock private lateinit var provider: NetworkProvider
-    @Mock private lateinit var cm: ConnectivityManager
-    @Mock private lateinit var pm: PackageManager
-    @Mock private lateinit var bm: BluetoothManager
-    @Mock private lateinit var adapter: BluetoothAdapter
-    @Mock private lateinit var serverSocket: BluetoothServerSocket
-
-    private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
-    private val handler = Handler(handlerThread.looper)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        doReturn(provider).`when`(deps).getNetworkProvider(any(), any())
-        doReturn(handlerThread).`when`(deps).getHandlerThread()
-        doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
-        doReturn(pm).`when`(context).getPackageManager()
-        doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
-
-        doReturn(bm).`when`(context).getSystemService(eq(BluetoothManager::class.java))
-        doReturn(adapter).`when`(bm).getAdapter()
-        doReturn(serverSocket).`when`(adapter).listenUsingInsecureL2capChannel()
-        doReturn(0x80).`when`(serverSocket).getPsm()
-    }
-
-    @After
-    fun tearDown() {
-        handlerThread.quitSafely()
-        handlerThread.join()
-    }
-
-    @Test
-    fun testNetworkProvider_registeredWhenSupported() {
-        L2capNetworkProvider(deps, context).start()
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        verify(cm).registerNetworkProvider(eq(provider))
-        verify(provider).registerNetworkOffer(any(), any(), any(), any())
-    }
-
-    @Test
-    fun testNetworkProvider_notRegisteredWhenNotSupported() {
-        doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
-        L2capNetworkProvider(deps, context).start()
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        verify(cm, never()).registerNetworkProvider(eq(provider))
-    }
-
-    fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
-        clearInvocations(provider)
-        L2capNetworkProvider(deps, context).start()
-        handlerThread.waitForIdle(TIMEOUT_MS)
-
-        val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
-        verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
-
-        blanketOfferCaptor.value.onNetworkNeeded(request)
-        verify(provider).registerNetworkOffer(any(), any(), any(), any())
-    }
-
-    fun doTestBlanketOfferCreatesReservation(
-            request: NetworkRequest,
-            reservation: NetworkCapabilities
-    ) {
-        clearInvocations(provider)
-        L2capNetworkProvider(deps, context).start()
-        handlerThread.waitForIdle(TIMEOUT_MS)
-
-        val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
-        verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
-
-        blanketOfferCaptor.value.onNetworkNeeded(request)
-
-        val capsCaptor = ArgumentCaptor.forClass(NetworkCapabilities::class.java)
-        verify(provider, times(2)).registerNetworkOffer(any(), capsCaptor.capture(), any(), any())
-
-        assertTrue(reservation.satisfiedByNetworkCapabilities(capsCaptor.value))
-    }
-
-    @Test
-    fun testBlanketOffer_reservationWithoutSpecifier() {
-        val caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .build()
-        val nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
-
-        doTestBlanketOfferIgnoresRequest(nr)
-    }
-
-    @Test
-    fun testBlanketOffer_reservationWithCorrectSpecifier() {
-        var specifier = L2capNetworkSpecifier.Builder()
-                .setRole(ROLE_SERVER)
-                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
-                .build()
-        var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferCreatesReservation(nr, caps)
-
-        specifier = L2capNetworkSpecifier.Builder()
-                .setRole(ROLE_SERVER)
-                .setHeaderCompression(HEADER_COMPRESSION_NONE)
-                .build()
-        caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        nr = NetworkRequest(caps, TYPE_NONE, 43 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferCreatesReservation(nr, caps)
-    }
-
-    @Test
-    fun testBlanketOffer_reservationWithIncorrectSpecifier() {
-        var specifier = L2capNetworkSpecifier.Builder().build()
-        var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferIgnoresRequest(nr)
-
-        specifier = L2capNetworkSpecifier.Builder()
-                .setRole(ROLE_SERVER)
-                .build()
-        caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        nr = NetworkRequest(caps, TYPE_NONE, 44 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferIgnoresRequest(nr)
-
-        specifier = L2capNetworkSpecifier.Builder()
-                .setRole(ROLE_SERVER)
-                .setHeaderCompression(HEADER_COMPRESSION_NONE)
-                .setPsm(0x81)
-                .build()
-        caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        nr = NetworkRequest(caps, TYPE_NONE, 45 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferIgnoresRequest(nr)
-
-        specifier = L2capNetworkSpecifier.Builder()
-                .setHeaderCompression(HEADER_COMPRESSION_NONE)
-                .build()
-        caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                .addTransportType(TRANSPORT_BLUETOOTH)
-                .setNetworkSpecifier(specifier)
-                .build()
-        nr = NetworkRequest(caps, TYPE_NONE, 47 /* rId */, NetworkRequest.Type.RESERVATION)
-        doTestBlanketOfferIgnoresRequest(nr)
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index 8155fd0..06cb7ee 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -21,7 +21,10 @@
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
 import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.net.InetAddresses
+import android.net.LinkProperties
 import android.os.Build
+import android.os.Build.VERSION_CODES
 import androidx.test.filters.SmallTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -33,11 +36,32 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.atLeastOnce
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
+internal val LOCAL_DNS = InetAddresses.parseNumericAddress("224.0.1.2")
+internal val NON_LOCAL_DNS = InetAddresses.parseNumericAddress("76.76.75.75")
+
+private const val IFNAME_1 = "wlan1"
+private const val IFNAME_2 = "wlan2"
+private const val PORT_53 = 53
+private const val PROTOCOL_TCP = 6
+private const val PROTOCOL_UDP = 17
+
+private val lpWithNoLocalDns = LinkProperties().apply {
+    addDnsServer(NON_LOCAL_DNS)
+    interfaceName = IFNAME_1
+}
+
+private val lpWithLocalDns = LinkProperties().apply {
+    addDnsServer(LOCAL_DNS)
+    interfaceName = IFNAME_2
+}
+
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
@@ -69,6 +93,81 @@
         }
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalPrefixesUpdatedInBpfMap() {
+        // Connect Wi-Fi network with non-local dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        wifiAgent.connect()
+
+        // Verify that block rule is added to BpfMap for local prefixes.
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(any(), eq(IFNAME_1),
+            any(), eq(0), eq(0), eq(false))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        cellAgent.connect()
+
+        // Verify that block rule is removed from BpfMap for local prefixes.
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(any(), eq(IFNAME_1),
+            any(), eq(0), eq(0))
+
+        cellAgent.disconnect()
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalDnsNotUpdatedInBpfMap() {
+        // Connect Wi-Fi network with non-local dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        wifiAgent.connect()
+
+        // Verify that No allow rule is added to BpfMap since there is no local dns.
+        verify(bpfNetMaps, never()).addLocalNetAccess(any(), any(), any(), any(), any(),
+            eq(true))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        cellAgent.connect()
+
+        // Verify that No allow rule from port 53 is removed on network change
+        // because no dns was added
+        verify(bpfNetMaps, never()).removeLocalNetAccess(eq(192), eq(IFNAME_1),
+            eq(NON_LOCAL_DNS), any(), eq(PORT_53))
+
+        cellAgent.disconnect()
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    fun testLocalDnsUpdatedInBpfMap() {
+        // Connect Wi-Fi network with one local Dns.
+        val wifiAgent = Agent(nc = defaultNc(), lp = lpWithLocalDns)
+        wifiAgent.connect()
+
+        // Verify that allow rule is added to BpfMap for local dns at port 53,
+        // for TCP(=6) protocol
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53), eq(true))
+        // And for UDP(=17) protocol
+        verify(bpfNetMaps, atLeastOnce()).addLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53), eq(true))
+
+        wifiAgent.disconnect()
+        val cellAgent = Agent(nc = defaultNc(), lp = lpWithNoLocalDns)
+        cellAgent.connect()
+
+        // Verify that allow rule is removed for local dns on network change,
+        // for TCP(=6) protocol
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_TCP), eq(PORT_53))
+        // And for UDP(=17) protocol
+        verify(bpfNetMaps, atLeastOnce()).removeLocalNetAccess(eq(192), eq(IFNAME_2),
+            eq(LOCAL_DNS), eq(PROTOCOL_UDP), eq(PORT_53))
+
+        cellAgent.disconnect()
+    }
+
     private fun mockDataSaverStatus(status: Int) {
         doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
         // While the production code dispatches the intent on the handler thread,
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
new file mode 100644
index 0000000..d44bd0a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSL2capProviderTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.server
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.content.Context
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkSpecifier
+import android.os.Build
+import android.os.HandlerThread
+import android.os.Looper
+import com.android.server.L2capNetworkProvider
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.TestableNetworkCallback
+import java.io.IOException
+import java.util.concurrent.Executor
+import java.util.concurrent.LinkedBlockingQueue
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+private const val PSM = 0x85
+private val REMOTE_MAC = byteArrayOf(1, 2, 3, 4, 5, 6)
+private val REQUEST = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_BLUETOOTH)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+        .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSL2capProviderTest : CSTest() {
+    private val btAdapter = mock<BluetoothAdapter>()
+    private val btServerSocket = mock<BluetoothServerSocket>()
+    private val btSocket = mock<BluetoothSocket>()
+    private val providerDeps = mock<L2capNetworkProvider.Dependencies>()
+    private val acceptQueue = LinkedBlockingQueue<BluetoothSocket>()
+
+    private val handlerThread = HandlerThread("CSL2capProviderTest thread").apply { start() }
+
+    // Requires Dependencies mock to be setup before creation.
+    private lateinit var provider: L2capNetworkProvider
+
+    @Before
+    fun innerSetUp() {
+        doReturn(btAdapter).`when`(bluetoothManager).getAdapter()
+        doReturn(btServerSocket).`when`(btAdapter).listenUsingInsecureL2capChannel()
+        doReturn(PSM).`when`(btServerSocket).getPsm();
+
+        doAnswer {
+            val sock = acceptQueue.take()
+            sock ?: throw IOException()
+        }.`when`(btServerSocket).accept()
+
+        doAnswer {
+            acceptQueue.put(null)
+        }.`when`(btServerSocket).close()
+
+        doReturn(handlerThread).`when`(providerDeps).getHandlerThread()
+        provider = L2capNetworkProvider(providerDeps, context)
+        provider.start()
+    }
+
+    @After
+    fun innerTearDown() {
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    private fun reserveNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+        cm.reserveNetwork(nr, csHandler, it)
+    }
+
+    private fun requestNetwork(nr: NetworkRequest) = TestableNetworkCallback().also {
+        cm.requestNetwork(nr, it, csHandler)
+    }
+
+    private fun NetworkRequest.copyWithSpecifier(specifier: NetworkSpecifier): NetworkRequest {
+        // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+        // changes the underlying request.
+        return NetworkRequest.Builder(NetworkRequest(this))
+                .setNetworkSpecifier(specifier)
+                .build()
+    }
+
+    @Test
+    fun testReservation() {
+        val l2capServerSpecifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        val l2capReservation = REQUEST.copyWithSpecifier(l2capServerSpecifier)
+        val reservationCb = reserveNetwork(l2capReservation)
+
+        val reservedCaps = reservationCb.expect<Reserved>().caps
+        val reservedSpec = reservedCaps.networkSpecifier as L2capNetworkSpecifier
+
+        assertEquals(PSM, reservedSpec.getPsm())
+        assertEquals(HEADER_COMPRESSION_6LOWPAN, reservedSpec.headerCompression)
+        assertNull(reservedSpec.remoteAddress)
+
+        reservationCb.assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithoutSpecifier() {
+        reserveNetwork(REQUEST).assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithCorrectSpecifier() {
+        var specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+                .build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).expect<Reserved>()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).expect<Reserved>()
+    }
+
+    @Test
+    fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+        var specifier = L2capNetworkSpecifier.Builder().build()
+        var nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setRole(ROLE_SERVER)
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .setPsm(0x81)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+
+        specifier = L2capNetworkSpecifier.Builder()
+                .setHeaderCompression(HEADER_COMPRESSION_NONE)
+                .build()
+        nr = REQUEST.copyWithSpecifier(specifier)
+        reserveNetwork(nr).assertNoCallback()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
new file mode 100644
index 0000000..5bf6e04
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalNetworkProtectionTest.kt
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val LONG_TIMEOUT_MS = 5_000
+private const val PREFIX_LENGTH_IPV4 = 32 + 96
+private const val PREFIX_LENGTH_IPV6 = 32
+private const val WIFI_IFNAME = "wlan0"
+private const val WIFI_IFNAME_2 = "wlan1"
+
+private val wifiNc = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+    interfaceName = iface
+    for (linkAddress in linkAddresses) {
+        addLinkAddress(linkAddress)
+    }
+}
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+        .clearCapabilities()
+        .addTransportType(transport).apply {
+            if (transport != TRANSPORT_VPN) {
+                addCapability(NET_CAPABILITY_NOT_VPN)
+            }
+        }.build()
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CSLocalNetworkProtectionTest : CSTest() {
+    private val LOCAL_IPV6_IP_ADDRESS_PREFIX = IpPrefix("fe80::1cf1:35ff:fe8c:db87/64")
+    private val LOCAL_IPV6_LINK_ADDRESS = LinkAddress(
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress(),
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()
+    )
+
+    private val LOCAL_IPV4_IP_ADDRESS_PREFIX_1 = IpPrefix("10.0.0.184/24")
+    private val LOCAL_IPV4_LINK_ADDRRESS_1 =
+        LinkAddress(
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress(),
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getPrefixLength()
+        )
+
+    private val LOCAL_IPV4_IP_ADDRESS_PREFIX_2 = IpPrefix("10.0.255.184/24")
+    private val LOCAL_IPV4_LINK_ADDRRESS_2 =
+        LinkAddress(
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getAddress(),
+            LOCAL_IPV4_IP_ADDRESS_PREFIX_2.getPrefixLength()
+        )
+
+    @Test
+    fun testNetworkWithIPv6LocalAddress_AddressAddedToBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        // Connecting to network with IPv6 local address in LinkProperties
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    @Test
+    fun testNetworkWithIPv4LocalAddress_AddressAddedToBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithDifferentLinkAddresses_AddressReplacedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property from IPv6 in Link Address to IPv4 in Link Address
+        val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    @Test
+    fun testStackedLinkPropertiesWithDifferentLinkAddresses_AddressAddedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        // Adding stacked link
+        wifiLp.addStackedLink(wifiLp2)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+                eq(WIFI_IFNAME),
+                eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+
+        // Multicast and Broadcast address should always be populated on stacked link
+        // in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+        // in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+        // As both addresses are in stacked links, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testRemovingStackedLinkProperties_AddressRemovedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        // populating stacked link
+        wifiLp.addStackedLink(wifiLp2)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+                eq(WIFI_IFNAME),
+                eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+
+        // Multicast and Broadcast address should always be populated on stacked link
+        // in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated as part of stacked link
+        // in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0),
+                eq(false)
+        )
+        // As both addresses are in stacked links, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+
+        // replacing link properties without stacked links
+        val wifiLp_3 = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        wifiAgent.sendLinkProperties(wifiLp_3)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // As both stacked links is removed, 10.0.0.0/8 should be removed from local_net_access map.
+        verify(bpfNetMaps).removeLocalNetAccess(
+                eq(PREFIX_LENGTH_IPV4 + 8),
+                eq(WIFI_IFNAME_2),
+                eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+                eq(0),
+                eq(0)
+        )
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithLinkAddressesInSameRange_AddressIntactInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV4_IP_ADDRESS_PREFIX_1.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property from one IPv4 to another IPv4 within same range(10.0.0.0/8)
+        val wifiLp2 = lp(WIFI_IFNAME, LOCAL_IPV4_LINK_ADDRRESS_2)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // As both addresses below to same range, so no address should be removed from the map.
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testChangeLinkPropertiesWithDifferentInterface_AddressReplacedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Updating Link Property by changing interface name which has IPv4 instead of IPv6
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        wifiAgent.sendLinkProperties(wifiLp2)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // Multicast and Broadcast address should be populated in local_net_access map for
+        // new interface
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME_2),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Multicast and Broadcast address should be removed in local_net_access map for
+        // old interface
+        verifyRemovalOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    @Test
+    fun testAddingAnotherNetwork_AllAddressesAddedInBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Adding another network with LinkProperty having IPv4 in LinkAddress
+        val wifiLp2 = lp(WIFI_IFNAME_2, LOCAL_IPV4_LINK_ADDRRESS_1)
+        val wifiAgent2 = Agent(nc = wifiNc, lp = wifiLp2)
+        wifiAgent2.connect()
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress(WIFI_IFNAME_2)
+        // Verifying IPv4 matching prefix(10.0.0.0/8) should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 8),
+            eq(WIFI_IFNAME_2),
+            eq(InetAddresses.parseNumericAddress("10.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        // Verifying nothing should be removed from local_net_access map
+        verify(bpfNetMaps, never()).removeLocalNetAccess(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testDestroyingNetwork_AddressesRemovedFromBpfMap() {
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // Multicast and Broadcast address should always be populated in local_net_access map
+        verifyPopulationOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be populated in local_net_access map
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq( PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+
+        // Unregistering the network
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        cb.expect<Lost>(wifiAgent.network)
+
+        // Multicast and Broadcast address should be removed in local_net_access map for
+        // old interface
+        verifyRemovalOfMulticastAndBroadcastAddress()
+        // Verifying IPv6 address should be removed from local_net_access map
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()),
+            eq(WIFI_IFNAME),
+            eq(LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress()),
+            eq(0),
+            eq(0)
+        )
+    }
+
+    // Verify if multicast and broadcast addresses have been added using addLocalNetAccess
+    fun verifyPopulationOfMulticastAndBroadcastAddress(
+        interfaceName: String = WIFI_IFNAME
+    ) {
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 4),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + 8),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("ff00::")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+        verify(bpfNetMaps).addLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 32),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+            eq(0),
+            eq(0),
+            eq(false)
+        )
+    }
+
+    // Verify if multicast and broadcast addresses have been removed using removeLocalNetAccess
+    fun verifyRemovalOfMulticastAndBroadcastAddress(
+        interfaceName: String = WIFI_IFNAME
+    ) {
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 4),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("224.0.0.0")),
+            eq(0),
+            eq(0)
+        )
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV6 + 8),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("ff00::")),
+            eq(0),
+            eq(0)
+        )
+        verify(bpfNetMaps).removeLocalNetAccess(
+            eq(PREFIX_LENGTH_IPV4 + 32),
+            eq(interfaceName),
+            eq(InetAddresses.parseNumericAddress("255.255.255.255")),
+            eq(0),
+            eq(0)
+        )
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index ae196a6..350a140 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.AlarmManager
 import android.app.AppOpsManager
+import android.bluetooth.BluetoothManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -209,6 +210,7 @@
         doReturn(true).`when`(it).isDataCapable()
     }
     val subscriptionManager = mock<SubscriptionManager>()
+    val bluetoothManager = mock<BluetoothManager>()
 
     val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
     val satelliteAccessController = mock<SatelliteAccessController>()
@@ -393,6 +395,8 @@
             // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
             destroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids)
         }
+
+        override fun makeL2capNetworkProvider(context: Context) = null
     }
 
     inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -503,6 +507,7 @@
             Context.BATTERY_STATS_SERVICE -> batteryManager
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
             Context.APP_OPS_SERVICE -> appOpsManager
+            Context.BLUETOOTH_SERVICE -> bluetoothManager
             else -> super.getSystemService(serviceName)
         }
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index 8ff790c..6b560de 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -23,6 +23,7 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
 import android.content.pm.PackageManager.FEATURE_ETHERNET
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
@@ -103,7 +104,13 @@
 }
 
 internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
-    val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+    val supported = listOf(
+            FEATURE_WIFI,
+            FEATURE_WIFI_DIRECT,
+            FEATURE_BLUETOOTH,
+            FEATURE_BLUETOOTH_LE,
+            FEATURE_ETHERNET
+    )
     doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
     val myPackageName = realContext.packageName
     val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
diff --git a/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
new file mode 100644
index 0000000..8431194
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/HeaderCompressionUtilsTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 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.server.net
+
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.internal.util.HexDump
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class HeaderCompressionUtilsTest {
+
+    private fun decompressHex(hex: String): ByteArray {
+        val bytes = HexDump.hexStringToByteArray(hex)
+        val buf = bytes.copyOf(1500)
+        val newLen = HeaderCompressionUtils.decompress6lowpan(buf, bytes.size)
+        return buf.copyOf(newLen)
+    }
+
+    private fun compressHex(hex: String): ByteArray {
+        val buf = HexDump.hexStringToByteArray(hex)
+        val newLen = HeaderCompressionUtils.compress6lowpan(buf, buf.size)
+        return buf.copyOf(newLen)
+    }
+
+    private fun String.decodeHex() = HexDump.hexStringToByteArray(this)
+
+    @Test
+    fun testHeaderDecompression() {
+        // TF: 00, NH: 0, HLIM: 00, CID: 0, SAC: 0, SAM: 00, M: 0, DAC: 0, DAM: 00
+        var input = "6000" +
+                    "ccf" +                               // ECN + DSCP + 4-bit Pad (here "f")
+                    "12345" +                             // flow label
+                    "11" +                                // next header
+                    "e7" +                                // hop limit
+                    "abcdef1234567890abcdef1234567890" +  // source
+                    "aaabbbcccdddeeefff00011122233344" +  // dest
+                    "abcd"                                // payload
+
+        var output = "6" +                                // version
+                     "cc" +                               // traffic class
+                     "12345" +                            // flow label
+                     "0002" +                             // payload length
+                     "11" +                               // next header
+                     "e7" +                               // hop limit
+                     "abcdef1234567890abcdef1234567890" + // source
+                     "aaabbbcccdddeeefff00011122233344" + // dest
+                     "abcd"                               // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 01, NH: 0, HLIM: 01, CID: 0, SAC: 0, SAM: 01, M: 0, DAC: 0, DAM: 01
+        input  = "6911" +
+                 "5" +                                // ECN + 2-bit pad (here "1")
+                 "f100e" +                            // flow label
+                 "42" +                               // next header
+                 "1102030405060708" +                 // source
+                 "aa0b0c0d0e0f1011" +                 // dest
+                 "abcd"                               // payload
+
+        output = "6" +                                // version
+                 "01" +                               // traffic class
+                 "f100e" +                            // flow label
+                 "0002" +                             // payload length
+                 "42" +                               // next header
+                 "01" +                               // hop limit
+                 "fe800000000000001102030405060708" + // source
+                 "fe80000000000000aa0b0c0d0e0f1011" + // dest
+                 "abcd"                               // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 10, NH: 0, HLIM: 10, CID: 0, SAC: 0, SAM: 10, M: 0, DAC: 0, DAM: 10
+        input  = "7222" +
+                 "cc" +                               // traffic class
+                 "43" +                               // next header
+                 "1234" +                             // source
+                 "abcd" +                             // dest
+                 "abcdef"                             // payload
+
+        output = "6" +                                // version
+                 "cc" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0003" +                             // payload length
+                 "43" +                               // next header
+                 "40" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "fe80000000000000000000fffe00abcd" + // dest
+                 "abcdef"                             // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 00
+        input  = "7b28" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "ff020000000000000000000000000001" + // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff020000000000000000000000000001" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 01
+        input  = "7b29" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "02abcdef1234" +                     // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff02000000000000000000abcdef1234" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 10
+        input  = "7b2a" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "ee123456" +                         // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ffee0000000000000000000000123456" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+
+        // TF: 11, NH: 0, HLIM: 11, CID: 0, SAC: 0, SAM: 10, M: 1, DAC: 0, DAM: 11
+        input  = "7b2b" +
+                 "44" +                               // next header
+                 "1234" +                             // source
+                 "89" +                               // dest
+                 "abcdef01"                           // payload
+
+        output = "6" +                                // version
+                 "00" +                               // traffic class
+                 "00000" +                            // flow label
+                 "0004" +                             // payload length
+                 "44" +                               // next header
+                 "ff" +                               // hop limit
+                 "fe80000000000000000000fffe001234" + // source
+                 "ff020000000000000000000000000089" + // dest
+                 "abcdef01"                           // payload
+        assertThat(decompressHex(input)).isEqualTo(output.decodeHex())
+    }
+
+    @Test
+    fun testHeaderCompression() {
+        val input  = "60120304000011fffe800000000000000000000000000001fe800000000000000000000000000002"
+        val output = "60000102030411fffe800000000000000000000000000001fe800000000000000000000000000002"
+        assertThat(compressHex(input)).isEqualTo(output.decodeHex())
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
index b3095ee..e261732 100644
--- a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -184,6 +184,7 @@
                 handler,
                 FdWrapper(ParcelFileDescriptor(tunFds[0])),
                 BluetoothSocketWrapper(bluetoothSocket),
+                false /* compressHeaders */,
                 callback
         )
     }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index b528480..581c709 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -168,7 +168,6 @@
 import com.android.net.module.util.Struct.U8;
 import com.android.net.module.util.bpf.CookieTagMapKey;
 import com.android.net.module.util.bpf.CookieTagMapValue;
-import com.android.server.BpfNetMaps;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.net.NetworkStatsService.AlertObserver;
 import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
@@ -286,8 +285,6 @@
     private LocationPermissionChecker mLocationPermissionChecker;
     private TestBpfMap<S32, U8> mUidCounterSetMap = spy(new TestBpfMap<>(S32.class, U8.class));
     @Mock
-    private BpfNetMaps mBpfNetMaps;
-    @Mock
     private SkDestroyListener mSkDestroyListener;
 
     private TestBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap = new TestBpfMap<>(
@@ -608,11 +605,6 @@
         }
 
         @Override
-        public BpfNetMaps makeBpfNetMaps(Context ctx) {
-            return mBpfNetMaps;
-        }
-
-        @Override
         public SkDestroyListener makeSkDestroyListener(
                 IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
             return mSkDestroyListener;
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index d41550b..7a5895f 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -130,6 +130,7 @@
     public void setUp() throws Exception {
         mExecutor = Executors.newSingleThreadExecutor();
         mOtCtl = new OtDaemonController();
+        mController.setEnabledAndWait(true);
         mController.leaveAndWait();
 
         // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network