Merge "Set restrict local network aconfig flag for 26q2 dates" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 1249e85..c0192dd 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -132,7 +132,6 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Pair;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -292,7 +291,7 @@
     private SettingsObserver mSettingsObserver;
     private BluetoothPan mBluetoothPan;
     private PanServiceListener mBluetoothPanListener;
-    private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+    private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
     // AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
     // TetheringManager, TetheringManager would convert it to a set of Integer types.
     // mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -312,7 +311,7 @@
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
         // list and handle them as soon as onServiceConnected is called.
-        mPendingPanRequests = new ArrayList<>();
+        mPendingPanRequestListeners = new ArrayList<>();
 
         mTetherStates = new ArrayMap<>();
         mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -862,12 +861,19 @@
             return;
         }
 
-        // The reference of IIntResultListener should only exist when application want to start
-        // tethering but tethering is not bound to pan service yet. Even if the calling process
-        // dies, the referenice of IIntResultListener would still keep in mPendingPanRequests. Once
-        // tethering bound to pan service (onServiceConnected) or bluetooth just crash
-        // (onServiceDisconnected), all the references from mPendingPanRequests would be cleared.
-        mPendingPanRequests.add(new Pair(enable, listener));
+        if (!enable) {
+            // The service is not connected. If disabling tethering, there's no point starting
+            // the service just to stop tethering since tethering is not started. Just remove
+            // any pending requests to enable tethering, and notify them that they have failed.
+            for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
+                        TETHERING_BLUETOOTH);
+            }
+            mPendingPanRequestListeners.clear();
+            sendTetherResult(listener, TETHER_ERROR_NO_ERROR, TETHERING_BLUETOOTH);
+            return;
+        }
+        mPendingPanRequestListeners.add(listener);
 
         // Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
         // the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -893,10 +899,11 @@
                 mBluetoothPan = (BluetoothPan) proxy;
                 mIsConnected = true;
 
-                for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
-                    setBluetoothTetheringSettings(mBluetoothPan, request.first, request.second);
+                for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                    setBluetoothTetheringSettings(mBluetoothPan, true /* enable */,
+                            pendingListener);
                 }
-                mPendingPanRequests.clear();
+                mPendingPanRequestListeners.clear();
             });
         }
 
@@ -907,11 +914,11 @@
                 // reachable before next onServiceConnected.
                 mIsConnected = false;
 
-                for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
-                    sendTetherResult(request.second, TETHER_ERROR_SERVICE_UNAVAIL,
+                for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+                    sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
                             TETHERING_BLUETOOTH);
                 }
-                mPendingPanRequests.clear();
+                mPendingPanRequestListeners.clear();
                 mBluetoothIfaceRequest = null;
                 mBluetoothCallback = null;
                 maybeDisableBluetoothIpServing();
@@ -1730,27 +1737,7 @@
                 break;
             case IFACE_IP_MODE_LOCAL_ONLY:
                 type = maybeInferWifiTetheringType(ifname);
-                // BUG: this request is incorrect - instead of LOHS, it will reflect whatever
-                // request (if any) is being processed for TETHERING_WIFI. However, this is the
-                // historical behaviour. It mostly works because a) most of the time there is no
-                // such request b) tetherinternal doesn't look at the connectivity scope of the
-                // request, it takes the scope from requestedState.
-                request = getPendingTetheringRequest(type);
-                if (request == null) {
-                    request = createImplicitLocalOnlyTetheringRequest(TETHERING_WIFI);
-                } else {
-                    // If we've taken this request from the pending requests, then force the
-                    // connectivity scope to local so we start IpServer in local-only mode (this
-                    // matches historical behavior). This should be OK since the connectivity scope
-                    // is only used to start IpServer in the correct mode.
-                    // TODO: This will break fuzzy-matching logic for start/stop tethering in the
-                    //       future. Figure out how to reconcile that with this forced scope.
-                    //       Possibly ignore the connectivity scope for wifi if both requests are
-                    //       explicit, since explicit Wifi requests may only have
-                    //       CONNECTIVITY_SCOPE_GLOBAL. Or possibly, don't add any edge case and
-                    //       treat it as a different request entirely.
-                    request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
-                }
+                request = createImplicitLocalOnlyTetheringRequest(type);
                 break;
             default:
                 mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index f7a44f1..e1c2db9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -93,7 +93,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
@@ -2787,6 +2786,10 @@
         public void assertHasResult() {
             if (!mHasResult) fail("No callback result");
         }
+
+        public void assertDoesNotHaveResult() {
+            if (mHasResult) fail("Has callback result");
+        }
     }
 
     @Test
@@ -3395,10 +3398,9 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testBluetoothTethering() throws Exception {
         initTetheringOnTestThread();
-        // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
-        assumeTrue(isAtLeastT());
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3432,10 +3434,9 @@
     }
 
     @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
     public void testBluetoothTetheringBeforeT() throws Exception {
         initTetheringOnTestThread();
-        // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
-        assumeFalse(isAtLeastT());
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3515,6 +3516,23 @@
         verifyNetdCommandForBtTearDown();
     }
 
+    @Test
+    public void testPendingPanEnableRequestFailedUponDisableRequest() throws Exception {
+        initTetheringOnTestThread();
+
+        mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+        final ResultListener failedEnable = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+                TEST_CALLER_PKG, failedEnable);
+        mLooper.dispatchAll();
+        failedEnable.assertDoesNotHaveResult();
+
+        // Stop tethering before the pan service connects. This should fail the enable request.
+        mTethering.stopTethering(TETHERING_BLUETOOTH);
+        mLooper.dispatchAll();
+        failedEnable.assertHasResult();
+    }
+
     private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
         when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
         when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index d67da48..e95ca5f 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -106,8 +106,12 @@
 // Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
 _Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
 _Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
-// BPF wants 8, but 32-bit x86 wants 4
-//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+// BPF & everyone else wants 8, but 32-bit x86 wants 4
+#if defined(__i386__)
+_Static_assert(_Alignof(unsigned long long) == 4, "x86-32 _Alignof unsigned long long != 4");
+#else
+_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+#endif
 
 
 // for maps:
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 04d7492..1e088a6 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1490,6 +1490,11 @@
         if (!isTV()) return 1;
     }
 
+    if (isKernel32Bit() && isAtLeast25Q2) {
+        ALOGE("Android 25Q2 requires 64 bit kernel.");
+        return 1;
+    }
+
     // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
     if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
         ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 6af7228..125f26b 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -318,7 +318,6 @@
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
     // initialized last so mCookieTagMap.isValid() implies everything else is valid too
     RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
-    ALOGI("%s successfully", __func__);
 
     return netdutils::status::ok;
 }
diff --git a/framework/Android.bp b/framework/Android.bp
index a1c6a15..d6ee759 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -160,7 +160,10 @@
 java_defaults {
     name: "CronetJavaDefaults",
     srcs: [":httpclient_api_sources"],
-    static_libs: ["com.android.net.http.flags-aconfig-java"],
+    static_libs: [
+        "com.android.net.http.flags-aconfig-java",
+        "modules-utils-expresslog",
+    ],
     libs: [
         "androidx.annotation_annotation",
     ],
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 00cf2ba..c7ad738 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -24,9 +24,6 @@
         "libasync_trait",
     ],
     prefer_rlib: true,
-    apex_available: [
-        "com.android.tethering",
-    ],
     host_supported: true,
 }
 
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index c5ec9ee..15c860b 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -30,8 +30,15 @@
 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.BluetoothManager;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
@@ -45,23 +52,49 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.util.ArrayMap;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.ServiceConnectivityJni;
+import com.android.server.net.L2capNetwork;
 
-import java.util.Map;
+import java.io.IOException;
+import java.util.Set;
 
 
 public class L2capNetworkProvider {
     private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+    private static final NetworkCapabilities COMMON_CAPABILITIES =
+            // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+            // BLUETOOTH_CONNECT permission.
+            NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                    .addTransportType(TRANSPORT_BLUETOOTH)
+                    // TODO: remove NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
+                    .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                    .addCapability(NET_CAPABILITY_NOT_METERED)
+                    .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                    .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                    .addCapability(NET_CAPABILITY_NOT_VPN)
+                    .build();
     private final Dependencies mDeps;
     private final Context mContext;
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
     private final NetworkProvider mProvider;
     private final BlanketReservationOffer mBlanketOffer;
-    private final Map<Integer, ReservedServerOffer> mReservedServerOffers = new ArrayMap<>();
+    private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+    // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
+    @Nullable
+    private BluetoothManager mBluetoothManager;
+
+    // Note: IFNAMSIZ is 16.
+    private static final String TUN_IFNAME = "l2cap-tun";
+    private static int sTunIndex = 0;
 
     /**
      * The blanket reservation offer is used to create an L2CAP server network, i.e. a network
@@ -71,28 +104,24 @@
      * requests that do not have a NetworkSpecifier set.
      */
     private class BlanketReservationOffer implements NetworkOfferCallback {
-        // TODO: ensure that once the incoming request is satisfied, the blanket offer does not get
-        // unneeded. This means the blanket offer must always outscore the reserved offer. This
-        // might require setting the blanket offer as setTransportPrimary().
-        public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+        // 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();
         // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
         public static final NetworkCapabilities CAPABILITIES;
         static {
+            // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+            // that specifies ROLE_SERVER or without a NetworkSpecifier.
             final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
                     .setRole(ROLE_SERVER)
                     .build();
-            NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                    .addTransportType(TRANSPORT_BLUETOOTH)
-                    // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
-                    .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
-                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
-                    .addCapability(NET_CAPABILITY_NOT_METERED)
-                    .addCapability(NET_CAPABILITY_NOT_ROAMING)
-                    .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-                    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                    .addCapability(NET_CAPABILITY_NOT_VPN)
+            NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
                     .setNetworkSpecifier(l2capNetworkSpecifier)
                     .build();
+            // TODO: add #setReservationId() to NetworkCapabilities.Builder
             caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
             CAPABILITIES = caps;
         }
@@ -127,51 +156,129 @@
                 return;
             }
 
-            final NetworkCapabilities reservationCaps = request.networkCapabilities;
-            final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+            final ReservedServerOffer reservedOffer = createReservedServerOffer(request);
+            if (reservedOffer == null) {
+                // Something went wrong when creating the offer. Send onUnavailable() to the app.
+                Log.e(TAG, "Failed to create L2cap server offer");
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
 
             final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
             mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
-            mReservedServerOffers.put(request.requestId, reservedOffer);
+            mReservedServerOffers.add(reservedOffer);
+        }
+
+        @Nullable
+        private ReservedServerOffer createReservedServerOffer(NetworkRequest reservation) {
+            final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+            if (bluetoothAdapter == null) {
+                Log.w(TAG, "Failed to get BluetoothAdapter");
+                return null;
+            }
+            final BluetoothServerSocket serverSocket;
+            try {
+                serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel();
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to open BluetoothServerSocket");
+                return null;
+            }
+
+            // Create the reserved capabilities partially from the reservation itself (non-reserved
+            // parts of the L2capNetworkSpecifier), the COMMON_CAPABILITIES, and the reserved data
+            // (BLE L2CAP PSM from the BluetoothServerSocket).
+            final NetworkCapabilities reservationNc = reservation.networkCapabilities;
+            final L2capNetworkSpecifier reservationSpec =
+                    (L2capNetworkSpecifier) reservationNc.getNetworkSpecifier();
+            // Note: the RemoteAddress is unspecified for server networks.
+            final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_SERVER)
+                    .setHeaderCompression(reservationSpec.getHeaderCompression())
+                    .setPsm(serverSocket.getPsm())
+                    .build();
+            NetworkCapabilities reservedNc =
+                    new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+                            .setNetworkSpecifier(reservedSpec)
+                            .build();
+            reservedNc.setReservationId(reservationNc.getReservationId());
+            return new ReservedServerOffer(reservedNc, serverSocket);
+        }
+
+        @Nullable
+        private ReservedServerOffer getReservedOfferForRequest(NetworkRequest request) {
+            final int rId = request.networkCapabilities.getReservationId();
+            for (ReservedServerOffer offer : mReservedServerOffers) {
+                // Comparing by reservationId is more explicit then using canBeSatisfiedBy() or the
+                // requestId.
+                if (offer.getReservedCapabilities().getReservationId() != rId) continue;
+                return offer;
+            }
+            return null;
         }
 
         @Override
         public void onNetworkUnneeded(NetworkRequest request) {
-            if (!mReservedServerOffers.containsKey(request.requestId)) {
-                return;
-            }
+            final ReservedServerOffer reservedOffer = getReservedOfferForRequest(request);
+            if (reservedOffer == null) return;
 
-            final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
             // Note that the reserved offer gets torn down when the reservation goes away, even if
-            // there are lingering requests.
-            reservedOffer.tearDown();
-            mProvider.unregisterNetworkOffer(reservedOffer);
+            // there are active (non-reservation) requests for said offer.
+            destroyAndUnregisterReservedOffer(reservedOffer);
         }
     }
 
+    private void destroyAndUnregisterReservedOffer(ReservedServerOffer reservedOffer) {
+        // Ensure the offer still exists if this was posted on the handler.
+        if (!mReservedServerOffers.contains(reservedOffer)) return;
+        mReservedServerOffers.remove(reservedOffer);
+
+        reservedOffer.tearDown();
+        mProvider.unregisterNetworkOffer(reservedOffer);
+    }
+
+    @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);
+        if (tunFd == null) {
+            return null;
+        }
+
+        return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
+    }
+
+
     private class ReservedServerOffer implements NetworkOfferCallback {
-        private final boolean mUseHeaderCompression;
-        private final int mPsm;
         private final NetworkCapabilities mReservedCapabilities;
+        private final BluetoothServerSocket mServerSocket;
 
-        public ReservedServerOffer(NetworkCapabilities reservationCaps) {
-            // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
-            final L2capNetworkSpecifier reservationSpec =
-                    (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
-            mUseHeaderCompression =
-                    reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
-
-            // TODO: open BluetoothServerSocket and allocate a PSM.
-            mPsm = 0x80;
-
-            final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
-                    .setRole(ROLE_SERVER)
-                    .setHeaderCompression(reservationSpec.getHeaderCompression())
-                    .setPsm(mPsm)
-                    .build();
-            mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
-                    .setNetworkSpecifier(reservedSpec)
-                    .build();
+        public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
+                BluetoothServerSocket serverSocket) {
+            mReservedCapabilities = reservedCapabilities;
+            // TODO: ServerSocket will be managed by an AcceptThread.
+            mServerSocket = serverSocket;
         }
 
         public NetworkCapabilities getReservedCapabilities() {
@@ -194,8 +301,11 @@
          * This method can be called multiple times.
          */
         public void tearDown() {
-            // TODO: implement.
-            // This method can be called multiple times.
+            try {
+                mServerSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+            }
         }
     }
 
@@ -234,11 +344,20 @@
      * Called on CS Handler thread.
      */
     public void start() {
-        final PackageManager pm = mContext.getPackageManager();
-        if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+        mHandler.post(() -> {
+            final PackageManager pm = mContext.getPackageManager();
+            if (!pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+                return;
+            }
+            mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
+            if (mBluetoothManager == null) {
+                // Can this ever happen?
+                Log.wtf(TAG, "BluetoothManager not found");
+                return;
+            }
             mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
             mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
                     BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
-        }
+        });
     }
 }
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 2686e4a..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1128,6 +1128,7 @@
         int delta = add ? +1 : -1;
         switch (request.type) {
             case REQUEST:
+            case RESERVATION:
                 mNumRequestNetworkRequests += delta;
                 break;
 
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
new file mode 100644
index 0000000..b9d5f13
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,141 @@
+/*
+ * 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.annotation.Nullable;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+public class L2capNetwork {
+    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.
+     *
+     * Note that the IpClient does not need to be stopped.
+     */
+    private static class L2capIpClient extends IpClientCallbacks {
+        private final String mLogTag;
+        private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+        private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+        @Nullable
+        private IpClientManager mIpClient;
+        @Nullable
+        private LinkProperties mLinkProperties;
+
+        L2capIpClient(String logTag, Context context, String ifname) {
+            mLogTag = logTag;
+            IpClientUtil.makeIpClient(context, ifname, this);
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            mIpClient = new IpClientManager(ipClient, mLogTag);
+            mOnIpClientCreatedCv.open();
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties lp) {
+            Log.d(mLogTag, "Successfully provisionined l2cap tun: " + lp);
+            mLinkProperties = lp;
+            mOnProvisioningSuccessCv.open();
+        }
+
+        public LinkProperties start() {
+            mOnIpClientCreatedCv.block();
+            // mIpClient guaranteed non-null.
+            final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                    .withoutIPv4()
+                    .withIpv6LinkLocalOnly()
+                    .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+                    .build();
+            mIpClient.startProvisioning(config);
+            // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+            // require any provisioning.
+            mOnProvisioningSuccessCv.block();
+            return mLinkProperties;
+        }
+    }
+
+    public interface ICallback {
+        /** Called when an error is encountered */
+        void onError(L2capNetwork network);
+        /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+        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);
+        mHandler = handler;
+        mIfname = ifname;
+        mForwarder = new L2capPacketForwarder(handler, tunFd, socket, () -> {
+            // 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) {
+            @Override
+            public void onNetworkUnwanted() {
+                Log.i(mLogTag, mIfname + ": Network is unwanted");
+                // TODO: add a check that this callback is invoked on the handler thread.
+                cb.onNetworkUnwanted(L2capNetwork.this);
+            }
+        };
+        mNetworkAgent.register();
+        mNetworkAgent.markConnected();
+    }
+
+    /** Get the NetworkCapabilities used for this Network */
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities;
+    }
+
+    /** Tear down the network and associated resources */
+    public void tearDown() {
+        mNetworkAgent.unregister();
+        mForwarder.tearDown();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
index 4a5dd4f..1d3561a 100644
--- a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -17,8 +17,7 @@
 package com.android.net.module.util;
 
 import android.annotation.NonNull;
-
-import java.io.IOException;
+import android.system.ErrnoException;
 
 /**
  * Contains JNI functions for use in service-connectivity
@@ -38,17 +37,17 @@
     /**
      * Create a timerfd.
      *
-     * @throws IOException if the timerfd creation is failed.
+     * @throws ErrnoException if the timerfd creation is failed.
      */
-    public static native int createTimerFd() throws IOException;
+    public static native int createTimerFd() throws ErrnoException;
 
     /**
      * Set given time to the timerfd.
      *
      * @param timeMs target time
-     * @throws IOException if setting expiration time is failed.
+     * @throws ErrnoException if setting expiration time is failed.
      */
-    public static native void setTimerFdTime(int fd, long timeMs) throws IOException;
+    public static native void setTimerFdTime(int fd, long timeMs) throws ErrnoException;
 
     /** Create tun/tap interface */
     public static native int createTunTap(boolean isTun, boolean hasCarrier,
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 10bc595..cce7efd 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -16,11 +16,9 @@
 
 package com.android.net.module.util;
 
-import android.os.Process;
+import android.system.ErrnoException;
 import android.util.Log;
 
-import java.io.IOException;
-
 /**
  * Contains mostly timerfd functionality.
  */
@@ -33,7 +31,7 @@
     static int createTimerFileDescriptor() {
         try {
             return ServiceConnectivityJni.createTimerFd();
-        } catch (IOException e) {
+        } catch (ErrnoException e) {
             Log.e(TAG, "createTimerFd failed", e);
             return -1;
         }
@@ -45,7 +43,7 @@
     static boolean setExpirationTime(int fd, long expirationTimeMs) {
         try {
             ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
-        } catch (IOException e) {
+        } catch (ErrnoException e) {
             Log.e(TAG, "setExpirationTime failed", e);
             return false;
         }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 0d96fc4..2420e7a 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -497,4 +497,33 @@
             return false;
         }
     }
+
+    /**
+     * Sends a netlink request to set MTU for given interface
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param mtu MTU value to set for the interface.
+     * @return true if the request finished successfully, otherwise false.
+     */
+    public static boolean setInterfaceMtu(@NonNull String interfaceName, int mtu) {
+        if (mtu < 68) {
+            Log.e(TAG, "Invalid mtu: " + mtu + ", mtu should be greater than 68 referring RFC791");
+            return false;
+        }
+        final RtNetlinkLinkMessage ntMsg =
+                RtNetlinkLinkMessage.createSetMtuMessage(interfaceName, /*seqNo*/ 0, mtu);
+        if (ntMsg == null) {
+            Log.e(TAG, "Failed to create message to set MTU to " + mtu
+                    + "for interface " + interfaceName);
+            return false;
+        }
+        final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+            return true;
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set MTU to " + mtu + " for: " + interfaceName, e);
+            return false;
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index f17a7ec..c19a124 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -364,6 +364,37 @@
                 DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
     }
 
+    /**
+     * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the MTU of a
+     * network interface.
+     *
+     * @param interfaceName The name of the network interface to query.
+     * @param sequenceNumber The sequence number for the Netlink message.
+     * @param mtu MTU value to set for the interface.
+     * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+     */
+    @Nullable
+    public static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+            int sequenceNumber, int mtu) {
+        return createSetMtuMessage(
+            interfaceName, sequenceNumber, mtu, new OsAccess());
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+            int sequenceNumber, int mtu, @NonNull OsAccess osAccess) {
+        final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+        if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+            return null;
+        }
+        return RtNetlinkLinkMessage.build(
+            new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK , sequenceNumber),
+            new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+                /*flags*/ 0, /*change*/ 0),
+            mtu, /*hardwareAddress*/ null, /*interfaceName*/ null);
+    }
+
     @Override
     public String toString() {
         return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index b29fc73..13710b1 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -328,6 +328,28 @@
     }
 
     @Test
+    public void testCreateSetInterfaceMtuMessage() {
+        final String expectedHexBytes =
+                "280000001000050068240000000000000000000008000000"   // struct nlmsghdr
+                        + "000000000000000008000400DC050000"; // struct ifinfomsg
+        final String interfaceName = "wlan0";
+        final int interfaceIndex = 8;
+        final int sequenceNumber = 0x2468;
+        final int mtu = 1500;
+
+        when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+        final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetMtuMessage(
+                interfaceName,
+                sequenceNumber,
+                mtu,
+                mOsAccess);
+        assertNotNull(msg);
+        final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+    }
+
+    @Test
     public void testToString() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
index 5a7515e..49eb476 100644
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -16,6 +16,9 @@
 
 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
@@ -35,6 +38,7 @@
 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
@@ -52,13 +56,14 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-const val TAG = "L2capNetworkProviderTest"
+private const val TAG = "L2capNetworkProviderTest"
+private const val TIMEOUT_MS = 1000
 
-val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+private val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
     .addTransportType(TRANSPORT_BLUETOOTH)
     .build()
 
-val RESERVATION = NetworkRequest(
+private val RESERVATION = NetworkRequest(
         NetworkCapabilities(RESERVATION_CAPS),
         TYPE_NONE,
         42 /* rId */,
@@ -73,6 +78,9 @@
     @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)
@@ -85,6 +93,11 @@
         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
@@ -96,6 +109,7 @@
     @Test
     fun testNetworkProvider_registeredWhenSupported() {
         L2capNetworkProvider(deps, context).start()
+        handlerThread.waitForIdle(TIMEOUT_MS)
         verify(cm).registerNetworkProvider(eq(provider))
         verify(provider).registerNetworkOffer(any(), any(), any(), any())
     }
@@ -104,12 +118,14 @@
     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())
@@ -124,6 +140,7 @@
     ) {
         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())