Add L2capPacketForwarder

L2capPacketForwarder forwards packets between an L2CAP BluetoothSocket
and a tun fd.

It uses blocking i/o to do so, since the BluetoothSocket API does not
expose a file descriptor to poll() on.

It remains to be seen whether shutdown() on a tun interface fd is
actually implemented. Currently, the test uses a pair of SOCK_SEQPACKET
sockets which require to be shutdown to interrupt the blocking read.

Test: L2capPacketForwarderTest
Change-Id: I03d2a2dd76ec2b71fdb9e3ea7008daa6d1d124fe
diff --git a/service/Android.bp b/service/Android.bp
index c4e2ef0..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -161,6 +161,7 @@
     ],
     libs: [
         "framework-annotations-lib",
+        "framework-bluetooth.stubs.module_lib",
         "framework-configinfrastructure.stubs.module_lib",
         "framework-connectivity-pre-jarjar",
         // The framework-connectivity-t library is only available on T+ platforms
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..cef351c
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,322 @@
+/*
+ * 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.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+    private static final String TAG = "L2capPacketForwarder";
+
+    // DCT specifies an MTU of 1500.
+    // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+    // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+    // MTU determines the value of the read buffers, so use the larger of the two.
+    @VisibleForTesting
+    public static final int MTU = 1528;
+    private final Handler mHandler;
+    private final IReadWriteFd mTunFd;
+    private final IReadWriteFd mL2capFd;
+    private final L2capThread mIngressThread;
+    private final L2capThread mEgressThread;
+    private final ICallback mCallback;
+
+    public interface ICallback {
+        /** Called when an error is encountered; should tear down forwarding. */
+        void onError();
+    }
+
+    private interface IReadWriteFd {
+        /**
+         * Read up to len bytes into bytes[off] and return bytes actually read.
+         *
+         * bytes[] must be of size >= off + len.
+         */
+        int read(byte[] bytes, int off, int len) throws IOException;
+        /**
+         * Write len bytes starting from bytes[off]
+         *
+         * bytes[] must be of size >= off + len.
+         */
+        void write(byte[] bytes, int off, int len) throws IOException;
+        /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+        void shutdownRead();
+        /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+        void shutdownWrite();
+        /** Close the fd */
+        void close();
+    }
+
+    @VisibleForTesting
+    public static class BluetoothSocketWrapper implements IReadWriteFd {
+        private final BluetoothSocket mSocket;
+        private final InputStream mInputStream;
+        private final OutputStream mOutputStream;
+
+        public BluetoothSocketWrapper(BluetoothSocket socket) {
+            // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+            // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+            // offload.
+            mSocket = socket;
+            try {
+                mInputStream = socket.getInputStream();
+                mOutputStream = socket.getOutputStream();
+            } catch (IOException e) {
+                // Per the API docs, this should not actually be possible.
+                Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+                // Fail hard.
+                throw new IllegalStateException("Failed to get Input/OutputStream");
+            }
+        }
+
+        /** Read from the BluetoothSocket. */
+        @Override
+        public int read(byte[] bytes, int off, int len) throws IOException {
+            // Note: EINTR is handled internally and automatically triggers a retry loop.
+            int bytesRead = mInputStream.read(bytes, off, len);
+            if (bytesRead > MTU) {
+                // Don't try to recover, just trigger network teardown. This might indicate a bug in
+                // the Bluetooth stack.
+                throw new IOException("Packet exceeds MTU");
+            }
+            return bytesRead;
+        }
+
+        /** Write to the BluetoothSocket. */
+        @Override
+        public void write(byte[] bytes, int off, int len) throws IOException {
+            // Note: EINTR is handled internally and automatically triggers a retry loop.
+            mOutputStream.write(bytes, off, len);
+        }
+
+        @Override
+        public void shutdownRead() {
+            // BluetoothSocket does not expose methods to shutdown read / write individually;
+            // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+            // called multiple times from any thread.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+            }
+        }
+
+        @Override
+        public void shutdownWrite() {
+            // BluetoothSocket does not expose methods to shutdown read / write individually;
+            // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+            // called multiple times from any thread.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+            }
+        }
+
+        @Override
+        public void close() {
+            // BluetoothSocket#close() is safe to be called multiple times.
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public static class FdWrapper implements IReadWriteFd {
+        private final ParcelFileDescriptor mFd;
+
+        public FdWrapper(ParcelFileDescriptor fd) {
+            mFd = fd;
+        }
+
+        @Override
+        public int read(byte[] bytes, int off, int len) throws IOException {
+            try {
+                // Note: EINTR is handled internally and automatically triggers a retry loop.
+                return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+            } catch (ErrnoException e) {
+                throw new IOException(e);
+            }
+        }
+
+        /**
+         * Write to BluetoothSocket.
+         */
+        @Override
+        public void write(byte[] bytes, int off, int len) throws IOException {
+            try {
+                // Note: EINTR is handled internally and automatically triggers a retry loop.
+                Os.write(mFd.getFileDescriptor(), bytes, off, len);
+            } catch (ErrnoException e) {
+                throw new IOException(e);
+            }
+        }
+
+        @Override
+        public void shutdownRead() {
+            try {
+                Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+            } catch (ErrnoException e) {
+                Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+            }
+        }
+
+        @Override
+        public void shutdownWrite() {
+            try {
+                Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+            } catch (ErrnoException e) {
+                Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+            }
+        }
+
+        @Override
+        public void close() {
+            try {
+                // Safe to call multiple times. Both Os.close(FileDescriptor) and
+                // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+                mFd.close();
+            } catch (IOException e) {
+                Log.w(TAG, "close: Failed to close fd", e);
+            }
+        }
+    }
+
+    private class L2capThread extends Thread {
+        // Set mBuffer length to MTU + 1 to catch read() overflows.
+        private final byte[] mBuffer = new byte[MTU + 1];
+        private volatile boolean mIsRunning = true;
+
+        private final String mLogTag;
+        private IReadWriteFd mReadFd;
+        private IReadWriteFd mWriteFd;
+
+        L2capThread(String logTag, IReadWriteFd readFd, IReadWriteFd writeFd) {
+            mLogTag = logTag;
+            mReadFd = readFd;
+            mWriteFd = writeFd;
+        }
+
+        private void postOnError() {
+            mHandler.post(() -> {
+                // All callbacks must be called on handler thread.
+                mCallback.onError();
+            });
+        }
+
+        @Override
+        public void run() {
+            while (mIsRunning) {
+                try {
+                    final 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.
+                    // Note that a large read on BluetoothSocket throws an IOException to tear down
+                    // the network.
+                    if (readBytes > MTU) continue;
+
+                    mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+                } catch (IOException e) {
+                    Log.e(mLogTag, "L2capThread exception", e);
+                    // Tear down the network on any error.
+                    mIsRunning = false;
+                    // Notify provider that forwarding has stopped.
+                    postOnError();
+                }
+            }
+        }
+
+        public void tearDown() {
+            mIsRunning = false;
+            mReadFd.shutdownRead();
+            mWriteFd.shutdownWrite();
+        }
+    }
+
+    public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+            ICallback cb) {
+        this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), cb);
+    }
+
+    @VisibleForTesting
+    L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd, 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.start();
+        mEgressThread.start();
+    }
+
+    /**
+     * Tear down the L2capPacketForwarder.
+     *
+     * This operation closes both the passed tun fd and BluetoothSocket.
+     **/
+    public void tearDown() {
+        // Destroying both threads first guarantees that both read and write side of FD have been
+        // shutdown.
+        mIngressThread.tearDown();
+        mEgressThread.tearDown();
+
+        // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+        // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+        // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+        mTunFd.close();
+        mL2capFd.close();
+
+        try {
+            mIngressThread.join();
+        } catch (InterruptedException e) {
+            // join() interrupted in tearDown path, do nothing.
+        }
+        try {
+            mEgressThread.join();
+        } catch (InterruptedException e) {
+            // join() interrupted in tearDown path, do nothing.
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
new file mode 100644
index 0000000..b3095ee
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+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.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+    private lateinit var forwarder: L2capPacketForwarder
+    private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+    private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+    private lateinit var l2capInputStream: BluetoothL2capInputStream
+    private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+    @Mock private lateinit var bluetoothSocket: BluetoothSocket
+    @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+    private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+
+    /** Imitates the behavior of an L2CAP BluetoothSocket */
+    private class BluetoothL2capInputStream(
+        val fd: FileDescriptor,
+    ) : InputStream() {
+        val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+            limit(0)
+        }
+
+        override fun read(): Int {
+            throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+        }
+
+        /** See BluetoothSocket#read(buf, off, len) */
+        override fun read(b: ByteArray, off: Int, len: Int): Int {
+            // If no more bytes are remaining, read from the fd into the intermediate buffer.
+            if (l2capBuffer.remaining() == 0) {
+                // fillL2capRxBuffer()
+                // refill buffer and return - 1
+                val backingArray = l2capBuffer.array()
+                var bytesRead = 0
+                try {
+                    bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+                } catch (e: Exception) {
+                    // read failed, timed out, or was interrupted
+                    // InputStream throws IOException
+                    throw IOException(e)
+                }
+                l2capBuffer.rewind()
+                l2capBuffer.limit(bytesRead)
+            }
+
+            val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+            l2capBuffer.get(b, off, bytesToRead)
+            return bytesToRead
+        }
+
+        override fun available(): Int {
+            throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+        }
+
+        override fun close() {
+            try {
+                Os.shutdown(fd, SHUT_RD)
+            } catch (e: Exception) {
+                // InputStream throws IOException
+                throw IOException(e)
+            }
+        }
+    }
+
+    /** Imitates the behavior of an L2CAP BluetoothSocket */
+    private class BluetoothL2capOutputStream(
+        val fd: FileDescriptor,
+    ) : OutputStream() {
+
+        override fun write(b: Int) {
+            throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+        }
+
+        /** See BluetoothSocket#write(buf, off, len) */
+        override fun write(b: ByteArray, off: Int, len: Int) {
+            try {
+                Os.write(fd, b, off, len)
+            } catch (e: Exception) {
+                // OutputStream throws IOException
+                throw IOException(e)
+            }
+        }
+
+        override fun close() {
+            try {
+                Os.shutdown(fd, SHUT_WR)
+            } catch (e: Exception) {
+                // OutputStream throws IOException
+                throw IOException(e)
+            }
+        }
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+        Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+        // Set socket i/o timeout for test end.
+        Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+        Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+        l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+        l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+        doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+        doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+        doAnswer({
+            l2capInputStream.close()
+            l2capOutputStream.close()
+            try {
+                // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+                // close multiple times.
+                Os.close(l2capFds[0])
+            } catch (e: Exception) {
+                // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+                // invocations.
+            }
+        }).`when`(bluetoothSocket).close()
+
+        forwarder = L2capPacketForwarder(
+                handler,
+                FdWrapper(ParcelFileDescriptor(tunFds[0])),
+                BluetoothSocketWrapper(bluetoothSocket),
+                callback
+        )
+    }
+
+    @After
+    fun tearDown() {
+        if (::forwarder.isInitialized) {
+            // forwarder closes tunFds[0] and l2capFds[0]
+            forwarder.tearDown()
+        } else {
+            Os.close(tunFds[0])
+            Os.close(l2capFds[0])
+        }
+        Os.close(tunFds[1])
+        Os.close(l2capFds[1])
+
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+        val packet = ByteArray(size)
+        Random.nextBytes(packet)
+        Os.write(fd, packet, 0 /*off*/, packet.size)
+        return packet
+    }
+
+    fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+        val readBuffer = ByteArray(expected.size)
+        Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+        assertThat(readBuffer).isEqualTo(expected)
+    }
+
+    @Test
+    fun testForwarding_withoutHeaderCompression() {
+        var packet = sendPacket(l2capFds[1])
+        var packet2 = sendPacket(l2capFds[1])
+        assertPacketReceived(tunFds[1], packet)
+        assertPacketReceived(tunFds[1], packet2)
+
+        packet = sendPacket(tunFds[1])
+        packet2 = sendPacket(tunFds[1])
+        assertPacketReceived(l2capFds[1], packet)
+        assertPacketReceived(l2capFds[1], packet2)
+    }
+
+    @Test
+    fun testForwarding_packetExceedsMtu() {
+        // Reading from tun drops packets that exceed MTU.
+        // drop
+        sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+        // drop
+        sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+        var packet = sendPacket(l2capFds[1], 1280)
+        assertPacketReceived(tunFds[1], packet)
+
+        // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+        sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+        verify(callback, timeout(TIMEOUT)).onError()
+    }
+}