Merge "Revert "Return default value when getModuleVersion throwed""
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index d8a397d..1fe6c2c 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -304,6 +304,30 @@
     lint: { strict_updatability_linting: true },
 }
 
+java_library {
+    name: "net-utils-device-common-async",
+    srcs: [
+        "device/com/android/net/module/util/async/*.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "29",
+    visibility: [
+        "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/libs/net/common/testutils:__subpackages__",
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        "framework-annotations-lib",
+    ],
+    static_libs: [
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: { strict_updatability_linting: true },
+}
+
 // Use a filegroup and not a library for telephony sources, as framework-annotations cannot be
 // included either (some annotations would be duplicated on the bootclasspath).
 filegroup {
diff --git a/staticlibs/device/com/android/net/module/util/async/Assertions.java b/staticlibs/device/com/android/net/module/util/async/Assertions.java
new file mode 100644
index 0000000..ce701d0
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/Assertions.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import android.os.Build;
+
+/**
+ * Implements basic assert functions for runtime error-checking.
+ *
+ * @hide
+ */
+public final class Assertions {
+    public static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
+
+    public static void throwsIfOutOfBounds(int totalLength, int pos, int len) {
+        if (!IS_USER_BUILD && ((totalLength | pos | len) < 0 || pos > totalLength - len)) {
+            throw new ArrayIndexOutOfBoundsException(
+                "length=" + totalLength + "; regionStart=" + pos + "; regionLength=" + len);
+        }
+    }
+
+    public static void throwsIfOutOfBounds(byte[] buffer, int pos, int len) {
+        throwsIfOutOfBounds(buffer != null ? buffer.length : 0, pos, len);
+    }
+
+    private Assertions() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/AsyncFile.java b/staticlibs/device/com/android/net/module/util/async/AsyncFile.java
new file mode 100644
index 0000000..2a3231b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/AsyncFile.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import java.io.IOException;
+
+/**
+ * Represents an EventManager-managed file with Async IO semantics.
+ *
+ * Implements level-based Asyn IO semantics. This means that:
+ *   - onReadReady() callback would keep happening as long as there's any remaining
+ *     data to read, or the user calls enableReadEvents(false)
+ *   - onWriteReady() callback would keep happening as long as there's remaining space
+ *     to write to, or the user calls enableWriteEvents(false)
+ *
+ * All operations except close() must be called on the EventManager thread.
+ *
+ * @hide
+ */
+public interface AsyncFile {
+    /**
+     * Receives notifications when file readability or writeability changes.
+     * @hide
+     */
+    public interface Listener {
+        /** Invoked after the underlying file has been closed. */
+        void onClosed(AsyncFile file);
+
+        /** Invoked while the file has readable data and read notifications are enabled. */
+        void onReadReady(AsyncFile file);
+
+        /** Invoked while the file has writeable space and write notifications are enabled. */
+        void onWriteReady(AsyncFile file);
+    }
+
+    /** Requests this file to be closed. */
+    void close();
+
+    /** Enables or disables onReadReady() events. */
+    void enableReadEvents(boolean enable);
+
+    /** Enables or disables onWriteReady() events. */
+    void enableWriteEvents(boolean enable);
+
+    /** Returns true if the input stream has reached its end, or has been closed. */
+    boolean reachedEndOfFile();
+
+    /**
+     * Reads available data from the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no data to read at this moment.
+     * Returns -1 if the file has reached its end or the input stream has been closed.
+     * Otherwise returns the number of bytes read.
+     */
+    int read(byte[] buffer, int pos, int len) throws IOException;
+
+    /**
+     * Writes data into the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no buffer space to write to at this moment.
+     * Otherwise returns the number of bytes written.
+     */
+    int write(byte[] buffer, int pos, int len) throws IOException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java b/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java
new file mode 100644
index 0000000..92daa08
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/CircularByteBuffer.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import java.util.Arrays;
+
+/**
+ * Implements a circular read-write byte buffer.
+ *
+ * @hide
+ */
+public final class CircularByteBuffer implements ReadableByteBuffer {
+    private final byte[] mBuffer;
+    private final int mCapacity;
+    private int mReadPos;
+    private int mWritePos;
+    private int mSize;
+
+    public CircularByteBuffer(int capacity) {
+        mCapacity = capacity;
+        mBuffer = new byte[mCapacity];
+    }
+
+    @Override
+    public void clear() {
+        mReadPos = 0;
+        mWritePos = 0;
+        mSize = 0;
+        Arrays.fill(mBuffer, (byte) 0);
+    }
+
+    @Override
+    public int capacity() {
+        return mCapacity;
+    }
+
+    @Override
+    public int size() {
+        return mSize;
+    }
+
+    /** Returns the amount of remaining writeable space in this buffer. */
+    public int freeSize() {
+        return mCapacity - mSize;
+    }
+
+    @Override
+    public byte peek(int offset) {
+        if (offset < 0 || offset >= size()) {
+            throw new IllegalArgumentException("Invalid offset=" + offset + ", size=" + size());
+        }
+
+        return mBuffer[(mReadPos + offset) % mCapacity];
+    }
+
+    @Override
+    public void readBytes(byte[] dst, int dstPos, int dstLen) {
+        if (dst != null) {
+            Assertions.throwsIfOutOfBounds(dst, dstPos, dstLen);
+        }
+        if (dstLen > size()) {
+            throw new IllegalArgumentException("Invalid len=" + dstLen + ", size=" + size());
+        }
+
+        while (dstLen > 0) {
+            final int copyLen = getCopyLen(mReadPos, mWritePos, dstLen);
+            if (dst != null) {
+                System.arraycopy(mBuffer, mReadPos, dst, dstPos, copyLen);
+            }
+            dstPos += copyLen;
+            dstLen -= copyLen;
+            mSize -= copyLen;
+            mReadPos = (mReadPos + copyLen) % mCapacity;
+        }
+
+        if (mSize == 0) {
+            // Reset to the beginning for better contiguous access.
+            mReadPos = 0;
+            mWritePos = 0;
+        }
+    }
+
+    @Override
+    public void peekBytes(int offset, byte[] dst, int dstPos, int dstLen) {
+        Assertions.throwsIfOutOfBounds(dst, dstPos, dstLen);
+        if (offset + dstLen > size()) {
+            throw new IllegalArgumentException("Invalid len=" + dstLen
+                    + ", offset=" + offset + ", size=" + size());
+        }
+
+        int tmpReadPos = (mReadPos + offset) % mCapacity;
+        while (dstLen > 0) {
+            final int copyLen = getCopyLen(tmpReadPos, mWritePos, dstLen);
+            System.arraycopy(mBuffer, tmpReadPos, dst, dstPos, copyLen);
+            dstPos += copyLen;
+            dstLen -= copyLen;
+            tmpReadPos = (tmpReadPos + copyLen) % mCapacity;
+        }
+    }
+
+    @Override
+    public int getDirectReadSize() {
+        if (size() == 0) {
+            return 0;
+        }
+        return (mReadPos < mWritePos ? (mWritePos - mReadPos) : (mCapacity - mReadPos));
+    }
+
+    @Override
+    public int getDirectReadPos() {
+        return mReadPos;
+    }
+
+    @Override
+    public byte[] getDirectReadBuffer() {
+        return mBuffer;
+    }
+
+    @Override
+    public void accountForDirectRead(int len) {
+        if (len < 0 || len > size()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + size());
+        }
+
+        mSize -= len;
+        mReadPos = (mReadPos + len) % mCapacity;
+    }
+
+    /** Copies given data to the end of the buffer. */
+    public void writeBytes(byte[] buffer, int pos, int len) {
+        Assertions.throwsIfOutOfBounds(buffer, pos, len);
+        if (len > freeSize()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + freeSize());
+        }
+
+        while (len > 0) {
+            final int copyLen = getCopyLen(mWritePos, mReadPos,len);
+            System.arraycopy(buffer, pos, mBuffer, mWritePos, copyLen);
+            pos += copyLen;
+            len -= copyLen;
+            mSize += copyLen;
+            mWritePos = (mWritePos + copyLen) % mCapacity;
+        }
+    }
+
+    private int getCopyLen(int startPos, int endPos, int len) {
+        if (startPos < endPos) {
+            return Math.min(len, endPos - startPos);
+        } else {
+            return Math.min(len, mCapacity - startPos);
+        }
+    }
+
+    /** Returns the amount of contiguous writeable space. */
+    public int getDirectWriteSize() {
+        if (freeSize() == 0) {
+            return 0;  // Return zero in case buffer is full.
+        }
+        return (mWritePos < mReadPos ? (mReadPos - mWritePos) : (mCapacity - mWritePos));
+    }
+
+    /** Returns the position of contiguous writeable space. */
+    public int getDirectWritePos() {
+        return mWritePos;
+    }
+
+    /** Returns the buffer reference for direct write operation. */
+    public byte[] getDirectWriteBuffer() {
+        return mBuffer;
+    }
+
+    /** Must be called after performing a direct write using getDirectWriteBuffer(). */
+    public void accountForDirectWrite(int len) {
+        if (len < 0 || len > freeSize()) {
+            throw new IllegalArgumentException("Invalid len=" + len + ", size=" + freeSize());
+        }
+
+        mSize += len;
+        mWritePos = (mWritePos + len) % mCapacity;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("CircularByteBuffer{c=");
+        sb.append(mCapacity);
+        sb.append(",s=");
+        sb.append(mSize);
+        sb.append(",r=");
+        sb.append(mReadPos);
+        sb.append(",w=");
+        sb.append(mWritePos);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/EventManager.java b/staticlibs/device/com/android/net/module/util/async/EventManager.java
new file mode 100644
index 0000000..4ed4a70
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/EventManager.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages Async IO files and scheduled alarms, and executes all related callbacks
+ * in its own thread.
+ *
+ * All callbacks of AsyncFile, Alarm and EventManager will execute on EventManager's thread.
+ *
+ * Methods of this interface can be called from any thread.
+ *
+ * @hide
+ */
+public interface EventManager extends Executor {
+    /**
+     * Represents a scheduled alarm, allowing caller to attempt to cancel that alarm
+     * before it executes.
+     *
+     * @hide
+     */
+    public interface Alarm {
+        /** @hide */
+        public interface Listener {
+            void onAlarm(Alarm alarm, long elapsedTimeMs);
+            void onAlarmCancelled(Alarm alarm);
+        }
+
+        /**
+         * Attempts to cancel this alarm. Note that this request is inherently
+         * racy if executed close to the alarm's expiration time.
+         */
+        void cancel();
+    }
+
+    /**
+     * Requests EventManager to manage the given file.
+     *
+     * The file descriptors are not cloned, and EventManager takes ownership of all files passed.
+     *
+     * No event callbacks are enabled by this method.
+     */
+    AsyncFile registerFile(FileHandle fileHandle, AsyncFile.Listener listener) throws IOException;
+
+    /**
+     * Schedules Alarm with the given timeout.
+     *
+     * Timeout of zero can be used for immediate execution.
+     */
+    Alarm scheduleAlarm(long timeout, Alarm.Listener callback);
+
+    /** Schedules Runnable for immediate execution. */
+    @Override
+    void execute(Runnable callback);
+
+    /** Throws a runtime exception if the caller is not executing on this EventManager's thread. */
+    void assertInThread();
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/FileHandle.java b/staticlibs/device/com/android/net/module/util/async/FileHandle.java
new file mode 100644
index 0000000..9f7942d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/FileHandle.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.Closeable;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Represents an file descriptor or another way to access a file.
+ *
+ * @hide
+ */
+public final class FileHandle {
+    private final ParcelFileDescriptor mFd;
+    private final Closeable mCloseable;
+    private final InputStream mInputStream;
+    private final OutputStream mOutputStream;
+
+    public static FileHandle fromFileDescriptor(ParcelFileDescriptor fd) {
+        if (fd == null) {
+            throw new NullPointerException();
+        }
+        return new FileHandle(fd, null, null, null);
+    }
+
+    public static FileHandle fromBlockingStream(
+            Closeable closeable, InputStream is, OutputStream os) {
+        if (closeable == null || is == null || os == null) {
+            throw new NullPointerException();
+        }
+        return new FileHandle(null, closeable, is, os);
+    }
+
+    private FileHandle(ParcelFileDescriptor fd, Closeable closeable,
+            InputStream is, OutputStream os) {
+        mFd = fd;
+        mCloseable = closeable;
+        mInputStream = is;
+        mOutputStream = os;
+    }
+
+    ParcelFileDescriptor getFileDescriptor() {
+        return mFd;
+    }
+
+    Closeable getCloseable() {
+        return mCloseable;
+    }
+
+    InputStream getInputStream() {
+        return mInputStream;
+    }
+
+    OutputStream getOutputStream() {
+        return mOutputStream;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/OsAccess.java b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
new file mode 100644
index 0000000..df0ded2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
@@ -0,0 +1,66 @@
+/*
+ * 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.net.module.util.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides access to all relevant OS functions..
+ *
+ * @hide
+ */
+public abstract class OsAccess {
+    /** Closes the given file, suppressing IO exceptions. */
+    public abstract void close(ParcelFileDescriptor fd);
+
+    /** Returns file name for debugging purposes. */
+    public abstract String getFileDebugName(ParcelFileDescriptor fd);
+
+    /** Returns inner FileDescriptor instance. */
+    public abstract FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd);
+
+    /**
+     * Reads available data from the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no data to read at this moment.
+     * Returns -1 if the file has reached its end or the input stream has been closed.
+     * Otherwise returns the number of bytes read.
+     */
+    public abstract int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException;
+
+    /**
+     * Writes data into the given non-blocking file descriptor.
+     *
+     * Returns zero if there's no buffer space to write to at this moment.
+     * Otherwise returns the number of bytes written.
+     */
+    public abstract int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException;
+
+    public abstract long monotonicTimeMillis();
+    public abstract void setNonBlocking(FileDescriptor fd) throws IOException;
+    public abstract ParcelFileDescriptor[] pipe() throws IOException;
+
+    public abstract int poll(StructPollfd[] fds, int timeoutMs) throws IOException;
+    public abstract short getPollInMask();
+    public abstract short getPollOutMask();
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java b/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java
new file mode 100644
index 0000000..7f82404
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/ReadableByteBuffer.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+/**
+ * Allows reading from a buffer of bytes. The data can be read and thus removed,
+ * or peeked at without disturbing the buffer state.
+ *
+ * @hide
+ */
+public interface ReadableByteBuffer {
+    /** Returns the size of the buffered data. */
+    int size();
+
+    /**
+     * Returns the maximum amount of the buffered data.
+     *
+     * The caller may use this method in combination with peekBytes()
+     * to estimate when the buffer needs to be emptied using readData().
+     */
+    int capacity();
+
+    /** Clears all buffered data. */
+    void clear();
+
+    /** Returns a single byte at the given offset. */
+    byte peek(int offset);
+
+    /** Copies an array of bytes from the given offset to "dst". */
+    void peekBytes(int offset, byte[] dst, int dstPos, int dstLen);
+
+    /** Reads and removes an array of bytes from the head of the buffer. */
+    void readBytes(byte[] dst, int dstPos, int dstLen);
+
+    /** Returns the amount of contiguous readable data. */
+    int getDirectReadSize();
+
+    /** Returns the position of contiguous readable data. */
+    int getDirectReadPos();
+
+    /** Returns the buffer reference for direct read operation. */
+    byte[] getDirectReadBuffer();
+
+    /** Must be called after performing a direct read using getDirectReadBuffer(). */
+    void accountForDirectRead(int len);
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
new file mode 100644
index 0000000..5a09c40
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
@@ -0,0 +1,81 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_PD;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA_PD option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         OPTION_IA_PD          |           option-len          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         IAID (4 octets)                       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                              T1                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                              T2                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * .                                                               .
+ * .                          IA_PD-options                        .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class IaPdOption extends Struct {
+    public static final int LENGTH = 12; // option length excluding IA_PD options
+
+    @Field(order = 0, type = Type.S16)
+    public short code;
+    @Field(order = 1, type = Type.S16)
+    public short length;
+    @Field(order = 2, type = Type.U32)
+    public long id;
+    @Field(order = 3, type = Type.U32)
+    public long t1;
+    @Field(order = 4, type = Type.U32)
+    public long t2;
+
+    IaPdOption(final short code, final short length, final long id, final long t1,
+            final long t2) {
+        this.code = code;
+        this.length = length;
+        this.id = id;
+        this.t1 = t1;
+        this.t2 = t2;
+    }
+
+    /**
+     * Build an IA_PD option from the required specific parameters.
+     */
+    public static ByteBuffer build(final short length, final long id, final long t1,
+            final long t2) {
+        final IaPdOption option = new IaPdOption((short) DHCP6_OPTION_IA_PD,
+                length /* 12 + IA_PD options length */, id, t1, t2);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
new file mode 100644
index 0000000..1ac21ff
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -0,0 +1,89 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA Prefix Option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |        OPTION_IAPREFIX        |           option-len          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                      preferred-lifetime                       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        valid-lifetime                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | prefix-length |                                               |
+ * +-+-+-+-+-+-+-+-+          IPv6-prefix                          |
+ * |                           (16 octets)                         |
+ * |                                                               |
+ * |                                                               |
+ * |                                                               |
+ * |               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |               |                                               .
+ * +-+-+-+-+-+-+-+-+                                               .
+ * .                       IAprefix-options                        .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class IaPrefixOption extends Struct {
+    public static final int LENGTH = 25; // option length excluding IAprefix-options
+
+    @Field(order = 0, type = Type.S16)
+    public short code;
+    @Field(order = 1, type = Type.S16)
+    public short length;
+    @Field(order = 2, type = Type.U32)
+    public long preferred;
+    @Field(order = 3, type = Type.U32)
+    public long valid;
+    @Field(order = 4, type = Type.U8)
+    public short prefixLen;
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public byte[] prefix;
+
+    IaPrefixOption(final short code, final short length, final long preferred,
+            final long valid, final short prefixLen, final byte[] prefix) {
+        this.code = code;
+        this.length = length;
+        this.preferred = preferred;
+        this.valid = valid;
+        this.prefixLen = prefixLen;
+        this.prefix = prefix.clone();
+    }
+
+    /**
+     * Build an IA_PD prefix option with given specific parameters.
+     */
+    public static ByteBuffer build(final short length, final long preferred, final long valid,
+            final short prefixLen, final byte[] prefix) {
+        final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX,
+                length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BitUtils.java b/staticlibs/framework/com/android/net/module/util/BitUtils.java
index 2b32e86..3062d8c 100644
--- a/staticlibs/framework/com/android/net/module/util/BitUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/BitUtils.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 
 /**
  * @hide
@@ -107,4 +108,33 @@
             ++bitPos;
         }
     }
+
+    /**
+     * Returns a short but human-readable string of updates between an old and a new bit fields.
+     *
+     * @param oldVal the old bit field to diff from
+     * @param newVal the new bit field to diff to
+     * @return a string fit for logging differences, or null if no differences.
+     *         this method cannot return the empty string.
+     */
+    @Nullable
+    public static String describeDifferences(final long oldVal, final long newVal,
+            @NonNull final NameOf nameFetcher) {
+        final long changed = oldVal ^ newVal;
+        if (0 == changed) return null;
+        // If the control reaches here, there are changes (additions, removals, or both) so
+        // the code below is guaranteed to add something to the string and can't return "".
+        final long removed = oldVal & changed;
+        final long added = newVal & changed;
+        final StringBuilder sb = new StringBuilder();
+        if (0 != removed) {
+            sb.append("-");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, removed, nameFetcher, "-");
+        }
+        if (0 != added) {
+            sb.append("+");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, added, nameFetcher, "+");
+        }
+        return sb.toString();
+    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java b/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java
index b7eb70b..f4856b3 100644
--- a/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/ConnectivitySettingsUtils.java
@@ -60,6 +60,10 @@
     }
 
     private static int getPrivateDnsModeAsInt(String mode) {
+        // If both PRIVATE_DNS_MODE and PRIVATE_DNS_DEFAULT_MODE are not set, choose
+        // PRIVATE_DNS_MODE_OPPORTUNISTIC as default mode.
+        if (TextUtils.isEmpty(mode))
+            return PRIVATE_DNS_MODE_OPPORTUNISTIC;
         switch (mode) {
             case "off":
                 return PRIVATE_DNS_MODE_OFF;
@@ -68,7 +72,10 @@
             case "opportunistic":
                 return PRIVATE_DNS_MODE_OPPORTUNISTIC;
             default:
-                throw new IllegalArgumentException("Invalid private dns mode: " + mode);
+                // b/260211513: adb shell settings put global private_dns_mode foo
+                // can result in arbitrary strings - treat any unknown value as empty string.
+                // throw new IllegalArgumentException("Invalid private dns mode: " + mode);
+                return PRIVATE_DNS_MODE_OPPORTUNISTIC;
         }
     }
 
@@ -82,9 +89,6 @@
         final ContentResolver cr = context.getContentResolver();
         String mode = Settings.Global.getString(cr, PRIVATE_DNS_MODE);
         if (TextUtils.isEmpty(mode)) mode = Settings.Global.getString(cr, PRIVATE_DNS_DEFAULT_MODE);
-        // If both PRIVATE_DNS_MODE and PRIVATE_DNS_DEFAULT_MODE are not set, choose
-        // PRIVATE_DNS_MODE_OPPORTUNISTIC as default mode.
-        if (TextUtils.isEmpty(mode)) return PRIVATE_DNS_MODE_OPPORTUNISTIC;
         return getPrivateDnsModeAsInt(mode);
     }
 
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 7b3943c..1d88d6e 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -214,11 +214,14 @@
      *
      * See also:
      *     - https://datatracker.ietf.org/doc/html/rfc8415
+     *     - https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml
      */
     public static final int DHCP6_CLIENT_PORT = 546;
     public static final int DHCP6_SERVER_PORT = 547;
     public static final Inet6Address ALL_DHCP_RELAY_AGENTS_AND_SERVERS =
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::1:2");
+    public static final int DHCP6_OPTION_IA_PD = 25;
+    public static final int DHCP6_OPTION_IAPREFIX = 26;
 
     /**
      * IEEE802.11 standard constants.
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
index 4a45a93..d23afae 100644
--- a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -23,14 +23,20 @@
 #include <unistd.h>
 
 #include "BpfSyscallWrappers.h"
+#include "bpf/BpfRingbuf.h"
 #include "bpf/BpfUtils.h"
 
+#define TEST_RINGBUF_MAGIC_NUM 12345
+
 namespace android {
 namespace bpf {
 using ::android::base::testing::HasError;
 using ::android::base::testing::HasValue;
-using ::android::base::testing::WithMessage;
+using ::android::base::testing::WithCode;
+using ::testing::AllOf;
+using ::testing::Gt;
 using ::testing::HasSubstr;
+using ::testing::Lt;
 
 class BpfRingbufTest : public ::testing::Test {
  protected:
@@ -40,8 +46,11 @@
 
   void SetUp() {
     if (!android::bpf::isAtLeastKernelVersion(5, 8, 0)) {
-      GTEST_SKIP() << "BPF ring buffers not supported";
-      return;
+      GTEST_SKIP() << "BPF ring buffers not supported below 5.8";
+    }
+
+    if (sizeof(unsigned long) != 8) {
+      GTEST_SKIP() << "BPF ring buffers not supported on 32 bit arch";
     }
 
     errno = 0;
@@ -51,12 +60,82 @@
         << mProgPath << " was either not found or inaccessible.";
   }
 
+  void RunProgram() {
+    char fake_skb[128] = {};
+    EXPECT_EQ(runProgram(mProgram, fake_skb, sizeof(fake_skb)), 0);
+  }
+
+  void RunTestN(int n) {
+    int run_count = 0;
+    uint64_t output = 0;
+    auto callback = [&](const uint64_t& value) {
+      output = value;
+      run_count++;
+    };
+
+    auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+    ASSERT_RESULT_OK(result);
+
+    for (int i = 0; i < n; i++) {
+      RunProgram();
+    }
+
+    EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
+    EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
+    EXPECT_EQ(run_count, n);
+  }
+
   std::string mProgPath;
   std::string mRingbufPath;
   android::base::unique_fd mProgram;
 };
 
-TEST_F(BpfRingbufTest, CheckSetUp) {}
+TEST_F(BpfRingbufTest, ConsumeSingle) { RunTestN(1); }
+TEST_F(BpfRingbufTest, ConsumeMultiple) { RunTestN(3); }
+
+TEST_F(BpfRingbufTest, FillAndWrap) {
+  int run_count = 0;
+  auto callback = [&](const uint64_t&) { run_count++; };
+
+  auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+  ASSERT_RESULT_OK(result);
+
+  // 4kb buffer with 16 byte payloads (8 byte data, 8 byte header) should fill
+  // after 255 iterations. Exceed that so that some events are dropped.
+  constexpr int iterations = 300;
+  for (int i = 0; i < iterations; i++) {
+    RunProgram();
+  }
+
+  // Some events were dropped, but consume all that succeeded.
+  EXPECT_THAT(result.value()->ConsumeAll(callback),
+              HasValue(AllOf(Gt(250), Lt(260))));
+  EXPECT_THAT(run_count, AllOf(Gt(250), Lt(260)));
+
+  // After consuming everything, we should be able to use the ring buffer again.
+  run_count = 0;
+  RunProgram();
+  EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(1));
+  EXPECT_EQ(run_count, 1);
+}
+
+TEST_F(BpfRingbufTest, WrongTypeSize) {
+  // The program under test writes 8-byte uint64_t values so a ringbuffer for
+  // 1-byte uint8_t values will fail to read from it. Note that the map_def does
+  // not specify the value size, so we fail on read, not creation.
+  auto result = BpfRingbuf<uint8_t>::Create(mRingbufPath.c_str());
+  ASSERT_RESULT_OK(result);
+
+  RunProgram();
+
+  EXPECT_THAT(result.value()->ConsumeAll([](const uint8_t&) {}),
+              HasError(WithCode(EMSGSIZE)));
+}
+
+TEST_F(BpfRingbufTest, InvalidPath) {
+  EXPECT_THAT(BpfRingbuf<int>::Create("/sys/fs/bpf/bad_path"),
+              HasError(WithCode(ENOENT)));
+}
 
 }  // namespace bpf
 }  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
new file mode 100644
index 0000000..cac1e43
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <linux/bpf.h>
+#include <sys/mman.h>
+#include <utils/Log.h>
+
+#include "bpf/BpfUtils.h"
+
+namespace android {
+namespace bpf {
+
+// BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
+class BpfRingbufBase {
+ public:
+  ~BpfRingbufBase() {
+    if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
+    if (mProducerPos) munmap(mProducerPos, mProducerSize);
+    mConsumerPos = nullptr;
+    mProducerPos = nullptr;
+  }
+
+ protected:
+  // Non-initializing constructor, used by Create.
+  BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
+
+  // Full construction that aborts on error (use Create/Init to handle errors).
+  BpfRingbufBase(const char* path, size_t value_size) : mValueSize(value_size) {
+    if (auto status = Init(path); !status.ok()) {
+      ALOGE("BpfRingbuf init failed: %s", status.error().message().c_str());
+      abort();
+    }
+  }
+
+  // Delete copy constructor (class owns raw pointers).
+  BpfRingbufBase(const BpfRingbufBase&) = delete;
+
+  // Initialize the base ringbuffer components. Must be called exactly once.
+  base::Result<void> Init(const char* path);
+
+  // Consumes all messages from the ring buffer, passing them to the callback.
+  base::Result<int> ConsumeAll(
+      const std::function<void(const void*)>& callback);
+
+  // Replicates c-style void* "byte-wise" pointer addition.
+  template <typename Ptr>
+  static Ptr pointerAddBytes(void* base, ssize_t offset_bytes) {
+    return reinterpret_cast<Ptr>(reinterpret_cast<char*>(base) + offset_bytes);
+  }
+
+  // Rounds len by clearing bitmask, adding header, and aligning to 8 bytes.
+  static uint32_t roundLength(uint32_t len) {
+    len &= ~(BPF_RINGBUF_BUSY_BIT | BPF_RINGBUF_DISCARD_BIT);
+    len += BPF_RINGBUF_HDR_SZ;
+    return (len + 7) & ~7;
+  }
+
+  const size_t mValueSize;
+
+  size_t mConsumerSize;
+  size_t mProducerSize;
+  unsigned long mPosMask;
+  android::base::unique_fd mRingFd;
+
+  void* mDataPos = nullptr;
+  unsigned long* mConsumerPos = nullptr;
+  unsigned long* mProducerPos = nullptr;
+};
+
+// This is a class wrapper for eBPF ring buffers. An eBPF ring buffer is a
+// special type of eBPF map used for sending messages from eBPF to userspace.
+// The implementation relies on fast shared memory and atomics for the producer
+// and consumer management. Ring buffers are a faster alternative to eBPF perf
+// buffers.
+//
+// This class is thread compatible, but not thread safe.
+//
+// Note: A kernel eBPF ring buffer may be accessed by both kernel and userspace
+// processes at the same time. However, the userspace consumers of a given ring
+// buffer all share a single read pointer. There is no guarantee which readers
+// will read which messages.
+template <typename Value>
+class BpfRingbuf : public BpfRingbufBase {
+ public:
+  using MessageCallback = std::function<void(const Value&)>;
+
+  // Creates a ringbuffer wrapper from a pinned path. This initialization will
+  // abort on error. To handle errors, initialize with Create instead.
+  BpfRingbuf(const char* path) : BpfRingbufBase(path, sizeof(Value)) {}
+
+  // Creates a ringbuffer wrapper from a pinned path. There are no guarantees
+  // that the ringbuf outputs messaged of type `Value`, only that they are the
+  // same size. Size is only checked in ConsumeAll.
+  static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
+      const char* path);
+
+  // Consumes all messages from the ring buffer, passing them to the callback.
+  // Returns the number of messages consumed or a non-ok result on error. If the
+  // ring buffer has no pending messages an OK result with count 0 is returned.
+  base::Result<int> ConsumeAll(const MessageCallback& callback);
+
+ private:
+  // Empty ctor for use by Create.
+  BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
+};
+
+#define ACCESS_ONCE(x) (*(volatile typeof(x)*)&(x))
+
+#if defined(__i386__) || defined(__x86_64__)
+#define smp_sync() asm volatile("" ::: "memory")
+#elif defined(__aarch64__)
+#define smp_sync() asm volatile("dmb ish" ::: "memory")
+#else
+#define smp_sync() __sync_synchronize()
+#endif
+
+#define smp_store_release(p, v) \
+  do {                          \
+    smp_sync();                 \
+    ACCESS_ONCE(*(p)) = (v);    \
+  } while (0)
+
+#define smp_load_acquire(p)        \
+  ({                               \
+    auto ___p = ACCESS_ONCE(*(p)); \
+    smp_sync();                    \
+    ___p;                          \
+  })
+
+inline base::Result<void> BpfRingbufBase::Init(const char* path) {
+  if (sizeof(unsigned long) != 8) {
+    return android::base::Error()
+           << "BpfRingbuf does not support 32 bit architectures";
+  }
+  mRingFd.reset(mapRetrieveRW(path));
+  if (!mRingFd.ok()) {
+    return android::base::ErrnoError()
+           << "failed to retrieve ringbuffer at " << path;
+  }
+
+  int map_type = android::bpf::bpfGetFdMapType(mRingFd);
+  if (map_type != BPF_MAP_TYPE_RINGBUF) {
+    errno = EINVAL;
+    return android::base::ErrnoError()
+           << "bpf map has wrong type: want BPF_MAP_TYPE_RINGBUF ("
+           << BPF_MAP_TYPE_RINGBUF << ") got " << map_type;
+  }
+
+  int max_entries = android::bpf::bpfGetFdMaxEntries(mRingFd);
+  if (max_entries < 0) {
+    return android::base::ErrnoError()
+           << "failed to read max_entries from ringbuf";
+  }
+  if (max_entries == 0) {
+    errno = EINVAL;
+    return android::base::ErrnoError() << "max_entries must be non-zero";
+  }
+
+  mPosMask = max_entries - 1;
+  mConsumerSize = getpagesize();
+  mProducerSize = getpagesize() + 2 * max_entries;
+
+  {
+    void* ptr = mmap(NULL, mConsumerSize, PROT_READ | PROT_WRITE, MAP_SHARED,
+                     mRingFd, 0);
+    if (ptr == MAP_FAILED) {
+      return android::base::ErrnoError()
+             << "failed to mmap ringbuf consumer pages";
+    }
+    mConsumerPos = reinterpret_cast<unsigned long*>(ptr);
+  }
+
+  {
+    void* ptr = mmap(NULL, mProducerSize, PROT_READ, MAP_SHARED, mRingFd,
+                     mConsumerSize);
+    if (ptr == MAP_FAILED) {
+      return android::base::ErrnoError()
+             << "failed to mmap ringbuf producer page";
+    }
+    mProducerPos = reinterpret_cast<unsigned long*>(ptr);
+  }
+
+  mDataPos = pointerAddBytes<void*>(mProducerPos, getpagesize());
+  return {};
+}
+
+inline base::Result<int> BpfRingbufBase::ConsumeAll(
+    const std::function<void(const void*)>& callback) {
+  int64_t count = 0;
+  unsigned long cons_pos = smp_load_acquire(mConsumerPos);
+  unsigned long prod_pos = smp_load_acquire(mProducerPos);
+  while (cons_pos < prod_pos) {
+    // Find the start of the entry for this read (wrapping is done here).
+    void* start_ptr = pointerAddBytes<void*>(mDataPos, cons_pos & mPosMask);
+
+    // The entry has an 8 byte header containing the sample length.
+    uint32_t length = smp_load_acquire(reinterpret_cast<uint32_t*>(start_ptr));
+
+    // If the sample isn't committed, we're caught up with the producer.
+    if (length & BPF_RINGBUF_BUSY_BIT) return count;
+
+    cons_pos += roundLength(length);
+
+    if ((length & BPF_RINGBUF_DISCARD_BIT) == 0) {
+      if (length != mValueSize) {
+        smp_store_release(mConsumerPos, cons_pos);
+        errno = EMSGSIZE;
+        return android::base::ErrnoError()
+               << "BPF ring buffer message has unexpected size (want "
+               << mValueSize << " bytes, got " << length << " bytes)";
+      }
+      callback(pointerAddBytes<const void*>(start_ptr, BPF_RINGBUF_HDR_SZ));
+      count++;
+    }
+
+    smp_store_release(mConsumerPos, cons_pos);
+  }
+
+  return count;
+}
+
+template <typename Value>
+inline base::Result<std::unique_ptr<BpfRingbuf<Value>>>
+BpfRingbuf<Value>::Create(const char* path) {
+  auto rb = std::unique_ptr<BpfRingbuf>(new BpfRingbuf);
+  if (auto status = rb->Init(path); !status.ok()) return status.error();
+  return rb;
+}
+
+template <typename Value>
+inline base::Result<int> BpfRingbuf<Value>::ConsumeAll(
+    const MessageCallback& callback) {
+  return BpfRingbufBase::ConsumeAll([&](const void* value) {
+    callback(*reinterpret_cast<const Value*>(value));
+  });
+}
+
+#undef ACCESS_ONCE
+#undef smp_sync
+#undef smp_store_release
+#undef smp_load_acquire
+
+}  // namespace bpf
+}  // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelVersion.h b/staticlibs/native/bpf_headers/include/bpf/KernelVersion.h
index e0e53a9..29a36e6 100644
--- a/staticlibs/native/bpf_headers/include/bpf/KernelVersion.h
+++ b/staticlibs/native/bpf_headers/include/bpf/KernelVersion.h
@@ -16,8 +16,7 @@
 
 #pragma once
 
-#include <stdlib.h>
-#include <string.h>
+#include <stdio.h>
 #include <sys/utsname.h>
 
 namespace android {
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 4d48720..36865f3 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -40,6 +40,9 @@
 // BpfLoader v0.25+ support obj@ver.o files
 #define BPFLOADER_OBJ_AT_VER_VERSION 25u
 
+// Bpfloader v0.33+ supports {map,prog}.ignore_on_{eng,user,userdebug}
+#define BPFLOADER_IGNORED_ON_VERSION 33u
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -155,26 +158,37 @@
         __attribute__ ((section(".maps." #name), used)) \
                 ____btf_map_##name = { }
 
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug)  \
+    _Static_assert(                                                                       \
+        (min_loader) >= BPFLOADER_IGNORED_ON_VERSION ||                                   \
+            !((ignore_eng) || (ignore_user) || (ignore_userdebug)),                       \
+        "bpfloader min version must be >= 0.33 in order to use ignored_on");
+
 #define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
                             usr, grp, md, selinux, pindir, share, minkver,  \
-                            maxkver)                                        \
-  const struct bpf_map_def SECTION("maps") the_map = {                      \
-      .type = BPF_MAP_TYPE_##TYPE,                                          \
-      .key_size = (keysize),                                                \
-      .value_size = (valuesize),                                            \
-      .max_entries = (num_entries),                                         \
-      .map_flags = 0,                                                       \
-      .uid = (usr),                                                         \
-      .gid = (grp),                                                         \
-      .mode = (md),                                                         \
-      .bpfloader_min_ver = DEFAULT_BPFLOADER_MIN_VER,                       \
-      .bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER,                       \
-      .min_kver = (minkver),                                                \
-      .max_kver = (maxkver),                                                \
-      .selinux_context = (selinux),                                         \
-      .pin_subdir = (pindir),                                               \
-      .shared = (share),                                                    \
-  };
+                            maxkver, minloader, maxloader, ignore_eng,      \
+                            ignore_user, ignore_userdebug)                  \
+    const struct bpf_map_def SECTION("maps") the_map = {                    \
+        .type = BPF_MAP_TYPE_##TYPE,                                        \
+        .key_size = (keysize),                                              \
+        .value_size = (valuesize),                                          \
+        .max_entries = (num_entries),                                       \
+        .map_flags = 0,                                                     \
+        .uid = (usr),                                                       \
+        .gid = (grp),                                                       \
+        .mode = (md),                                                       \
+        .bpfloader_min_ver = (minloader),                                   \
+        .bpfloader_max_ver = (maxloader),                                   \
+        .min_kver = (minkver),                                              \
+        .max_kver = (maxkver),                                              \
+        .selinux_context = (selinux),                                       \
+        .pin_subdir = (pindir),                                             \
+        .shared = (share),                                                  \
+        .ignore_on_eng = (ignore_eng),                                      \
+        .ignore_on_user = (ignore_user),                                    \
+        .ignore_on_userdebug = (ignore_userdebug),                          \
+    };                                                                      \
+    BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
 
 // Type safe macro to declare a ring buffer and related output functions.
 // Compatibility:
@@ -182,22 +196,25 @@
 //   accessing the ring buffer should set a program level min_kver >= 5.8.
 // * The definition below sets a map min_kver of 5.8 which requires targeting
 //   a BPFLOADER_MIN_VER >= BPFLOADER_S_VERSION.
-#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md, \
-                               selinux, pindir, share)                       \
-  DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md,      \
-                      selinux, pindir, share, KVER(5, 8, 0), KVER_INF);      \
-  static inline __always_inline __unused int bpf_##the_map##_output(         \
-      const ValueType* v) {                                                  \
-    return bpf_ringbuf_output_unsafe(&the_map, v, sizeof(*v), 0);            \
-  }                                                                          \
-  static inline __always_inline __unused                                     \
-      ValueType* bpf_##the_map##_reserve() {                                 \
-    return bpf_ringbuf_reserve_unsafe(&the_map, sizeof(ValueType), 0);       \
-  }                                                                          \
-  static inline __always_inline __unused void bpf_##the_map##_submit(        \
-      const ValueType* v) {                                                  \
-    bpf_ringbuf_submit_unsafe(v, 0);                                         \
-  }
+#define DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md,   \
+                               selinux, pindir, share, min_loader, max_loader, \
+                               ignore_eng, ignore_user, ignore_userdebug)      \
+    DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md,      \
+                        selinux, pindir, share, KVER(5, 8, 0), KVER_INF,       \
+                        min_loader, max_loader, ignore_eng, ignore_user,       \
+                        ignore_userdebug);                                     \
+    static inline __always_inline __unused int bpf_##the_map##_output(         \
+            const ValueType* v) {                                              \
+        return bpf_ringbuf_output_unsafe(&the_map, v, sizeof(*v), 0);          \
+    }                                                                          \
+    static inline __always_inline __unused                                     \
+            ValueType* bpf_##the_map##_reserve() {                             \
+        return bpf_ringbuf_reserve_unsafe(&the_map, sizeof(ValueType), 0);     \
+    }                                                                          \
+    static inline __always_inline __unused void bpf_##the_map##_submit(        \
+            const ValueType* v) {                                              \
+        bpf_ringbuf_submit_unsafe(v, 0);                                       \
+    }
 
 /* There exist buggy kernels with pre-T OS, that due to
  * kernel patch "[ALPS05162612] bpf: fix ubsan error"
@@ -218,10 +235,12 @@
 
 /* type safe macro to declare a map and related accessor functions */
 #define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
-                           selinux, pindir, share)                                               \
+                           selinux, pindir, share, min_loader, max_loader, ignore_eng,           \
+                           ignore_user, ignore_userdebug)                                        \
   DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType),                         \
                       num_entries, usr, grp, md, selinux, pindir, share,                         \
-                      KVER_NONE, KVER_INF);                                                      \
+                      KVER_NONE, KVER_INF, min_loader, max_loader,                               \
+                      ignore_eng, ignore_user, ignore_userdebug);                                \
     BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md));                                 \
     BPF_ANNOTATE_KV_PAIR(the_map, KeyType, ValueType);                                           \
                                                                                                  \
@@ -253,9 +272,11 @@
 #error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
 #endif
 
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
-                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md)   \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,       \
+                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
+                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,       \
+                       /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
 
 #define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -277,6 +298,11 @@
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
                        DEFAULT_BPF_MAP_UID, gid, 0660)
 
+// LLVM eBPF builtins: they directly generate BPF_LD_ABS/BPF_LD_IND (skb may be ignored?)
+unsigned long long load_byte(void* skb, unsigned long long off) asm("llvm.bpf.load.byte");
+unsigned long long load_half(void* skb, unsigned long long off) asm("llvm.bpf.load.half");
+unsigned long long load_word(void* skb, unsigned long long off) asm("llvm.bpf.load.word");
+
 static int (*bpf_probe_read)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read;
 static int (*bpf_probe_read_str)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_str;
 static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
@@ -288,20 +314,24 @@
 static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
 static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
 
-#define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, opt,       \
-                            selinux, pindir)                                                       \
-    const struct bpf_prog_def SECTION("progs") the_prog##_def = {                                  \
-            .uid = (prog_uid),                                                                     \
-            .gid = (prog_gid),                                                                     \
-            .min_kver = (min_kv),                                                                  \
-            .max_kver = (max_kv),                                                                  \
-            .optional = (opt),                                                                     \
-            .bpfloader_min_ver = DEFAULT_BPFLOADER_MIN_VER,                                        \
-            .bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER,                                        \
-            .selinux_context = selinux,                                                            \
-            .pin_subdir = pindir,                                                                  \
-    };                                                                                             \
-    SECTION(SECTION_NAME)                                                                          \
+#define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,  \
+                            min_loader, max_loader, opt, selinux, pindir, ignore_eng,    \
+                            ignore_user, ignore_userdebug)                               \
+    const struct bpf_prog_def SECTION("progs") the_prog##_def = {                        \
+        .uid = (prog_uid),                                                               \
+        .gid = (prog_gid),                                                               \
+        .min_kver = (min_kv),                                                            \
+        .max_kver = (max_kv),                                                            \
+        .optional = (opt),                                                               \
+        .bpfloader_min_ver = (min_loader),                                               \
+        .bpfloader_max_ver = (max_loader),                                               \
+        .selinux_context = (selinux),                                                    \
+        .pin_subdir = (pindir),                                                          \
+        .ignore_on_eng = (ignore_eng),                                                   \
+        .ignore_on_user = (ignore_user),                                                 \
+        .ignore_on_userdebug = (ignore_userdebug),                                       \
+    };                                                                                   \
+    SECTION(SECTION_NAME)                                                                \
     int the_prog
 
 #ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
@@ -313,9 +343,11 @@
 #endif
 
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
-                                       opt) \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, opt, \
-                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR)
+                                       opt)                                                        \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,                \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt,                                 \
+                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR,             \
+                        false, false, false)
 
 // Programs (here used in the sense of functions/sections) marked optional are allowed to fail
 // to load (for example due to missing kernel patches).
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index f17cf3a..d286eba 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -204,8 +204,8 @@
 
     bool optional;  // program section (ie. function) may fail to load, continue onto next func.
 
-    // The following 3 ignore_on_* fields were added in version 0.32 (U). These are ignored in
-    // older bpfloader versions, and zero in programs compiled before 0.32.
+    // The following 3 ignore_on_* fields were added in version 0.33 (U). These are ignored in
+    // older bpfloader versions, and zero in programs compiled before 0.33.
     bool ignore_on_eng:1;
     bool ignore_on_user:1;
     bool ignore_on_userdebug:1;
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index f7d6a38..8502961 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -150,6 +150,18 @@
                                 });
 }
 
+// Available in 4.12 and later kernels.
+inline int runProgram(const BPF_FD_TYPE prog_fd, const void* data,
+                      const uint32_t data_size) {
+    return bpf(BPF_PROG_RUN, {
+                                     .test = {
+                                             .prog_fd = BPF_FD_TO_U32(prog_fd),
+                                             .data_in = ptr_to_u64(data),
+                                             .data_size_in = data_size,
+                                     },
+                             });
+}
+
 // BPF_OBJ_GET_INFO_BY_FD requires 4.14+ kernel
 //
 // Note: some fields are only defined in newer kernels (ie. the map_info struct grows
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
index 88a2683..84a32ed 100644
--- a/staticlibs/native/tcutils/Android.bp
+++ b/staticlibs/native/tcutils/Android.bp
@@ -20,7 +20,7 @@
     name: "libtcutils",
     srcs: ["tcutils.cpp"],
     export_include_dirs: ["include"],
-    header_libs: ["bpf_syscall_wrappers"],
+    header_libs: ["bpf_headers"],
     shared_libs: [
         "liblog",
     ],
@@ -53,7 +53,7 @@
         "-Werror",
         "-Wno-error=unused-variable",
     ],
-    header_libs: ["bpf_syscall_wrappers"],
+    header_libs: ["bpf_headers"],
     static_libs: [
         "libgmock",
         "libtcutils",
diff --git a/staticlibs/native/tcutils/kernelversion.h b/staticlibs/native/tcutils/kernelversion.h
deleted file mode 100644
index 9aab31d..0000000
--- a/staticlibs/native/tcutils/kernelversion.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-// -----------------------------------------------------------------------------
-// TODO - This should be replaced with BpfUtils in bpf_headers.
-// Currently, bpf_headers contains a bunch requirements it doesn't actually provide, such as a
-// non-ndk liblog version, and some version of libbase. libtcutils does not have access to either of
-// these, so I think this will have to wait until we figure out a way around this.
-//
-// In the mean time copying verbatim from:
-//   frameworks/libs/net/common/native/bpf_headers
-
-#pragma once
-
-#include <stdio.h>
-#include <sys/utsname.h>
-
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-
-namespace android {
-
-static inline unsigned uncachedKernelVersion() {
-  struct utsname buf;
-  if (uname(&buf)) return 0;
-
-  unsigned kver_major = 0;
-  unsigned kver_minor = 0;
-  unsigned kver_sub = 0;
-  (void)sscanf(buf.release, "%u.%u.%u", &kver_major, &kver_minor, &kver_sub);
-  return KVER(kver_major, kver_minor, kver_sub);
-}
-
-static unsigned kernelVersion() {
-  static unsigned kver = uncachedKernelVersion();
-  return kver;
-}
-
-static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor,
-                                          unsigned sub) {
-  return kernelVersion() >= KVER(major, minor, sub);
-}
-
-} // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 4101885..37a7ec8 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -19,7 +19,7 @@
 #include "tcutils/tcutils.h"
 
 #include "logging.h"
-#include "kernelversion.h"
+#include "bpf/KernelVersion.h"
 #include "scopeguard.h"
 
 #include <arpa/inet.h>
@@ -504,7 +504,7 @@
     // shipped with Android S, so (for safety) let's limit ourselves to
     // >5.10, ie. 5.11+ as a guarantee we're on Android T+ and thus no
     // longer need this non-upstream compatibility logic
-    static bool is_pre_5_11_kernel = !isAtLeastKernelVersion(5, 11, 0);
+    static bool is_pre_5_11_kernel = !bpf::isAtLeastKernelVersion(5, 11, 0);
     if (is_pre_5_11_kernel)
       return false;
   }
diff --git a/staticlibs/native/tcutils/tests/tcutils_test.cpp b/staticlibs/native/tcutils/tests/tcutils_test.cpp
index 3a89696..53835d7 100644
--- a/staticlibs/native/tcutils/tests/tcutils_test.cpp
+++ b/staticlibs/native/tcutils/tests/tcutils_test.cpp
@@ -18,7 +18,7 @@
 
 #include <gtest/gtest.h>
 
-#include "kernelversion.h"
+#include "bpf/KernelVersion.h"
 #include <tcutils/tcutils.h>
 
 #include <BpfSyscallWrappers.h>
@@ -82,7 +82,7 @@
   // TODO: this should likely be in the tethering module, where using netd.h would be ok
   static constexpr char bpfProgPath[] =
       "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether";
-  const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+  const int errNOENT = bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
 
   // static test values
   static constexpr bool ingress = true;
@@ -118,7 +118,7 @@
   ASSERT_LE(3, fd);
   close(fd);
 
-  const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+  const int errNOENT = bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
 
   // static test values
   static constexpr unsigned rateInBytesPerSec =
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 21e8c64..6e223bd 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -12,14 +12,15 @@
     min_sdk_version: "29",
     defaults: ["framework-connectivity-test-defaults"],
     static_libs: [
-        "net-utils-framework-common",
-        "net-utils-device-common-ip",
         "androidx.test.rules",
         "mockito-target-extended-minus-junit4",
-        "net-utils-device-common",
-        "net-utils-device-common-bpf",
-        "net-tests-utils",
         "netd-client",
+        "net-tests-utils",
+        "net-utils-framework-common",
+        "net-utils-device-common",
+        "net-utils-device-common-async",
+        "net-utils-device-common-bpf",
+        "net-utils-device-common-ip",
     ],
     libs: [
         "android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
index 0236716..49940ea 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
@@ -17,11 +17,13 @@
 package com.android.net.module.util
 
 import com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder
+import com.android.net.module.util.BitUtils.describeDifferences
 import com.android.net.module.util.BitUtils.packBits
 import com.android.net.module.util.BitUtils.unpackBits
-import org.junit.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
+import org.junit.Test
 
 class BitUtilsTests {
     @Test
@@ -58,4 +60,23 @@
             assertEquals(expected, it.toString())
         }
     }
+
+    @Test
+    fun testDescribeDifferences() {
+        fun describe(a: Long, b: Long) = describeDifferences(a, b, Integer::toString)
+        assertNull(describe(0, 0))
+        assertNull(describe(5, 5))
+        assertNull(describe(Long.MAX_VALUE, Long.MAX_VALUE))
+
+        assertEquals("+0", describe(0, 1))
+        assertEquals("-0", describe(1, 0))
+
+        assertEquals("+0+2", describe(0, 5))
+        assertEquals("+2", describe(1, 5))
+        assertEquals("-0+2", describe(1, 4))
+
+        fun makeField(vararg i: Int) = i.sumOf { 1L shl it }
+        assertEquals("-0-4-6-9+1+3+11", describe(makeField(0, 4, 6, 9), makeField(1, 3, 11)))
+        assertEquals("-1-5-9+6+8", describe(makeField(0, 1, 3, 4, 5, 9), makeField(0, 3, 4, 6, 8)))
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java
new file mode 100644
index 0000000..01abee2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/async/CircularByteBufferTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2022 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.net.module.util.async;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CircularByteBufferTest {
+    @Test
+    public void writeBytes() {
+        final int capacity = 23;
+        CircularByteBuffer buffer = new CircularByteBuffer(capacity);
+        assertEquals(0, buffer.size());
+        assertEquals(0, buffer.getDirectReadSize());
+        assertEquals(capacity, buffer.freeSize());
+        assertEquals(capacity, buffer.getDirectWriteSize());
+
+        final byte[] writeBuffer = new byte[15];
+        buffer.writeBytes(writeBuffer, 0, writeBuffer.length);
+
+        assertEquals(writeBuffer.length, buffer.size());
+        assertEquals(writeBuffer.length, buffer.getDirectReadSize());
+        assertEquals(capacity - writeBuffer.length, buffer.freeSize());
+        assertEquals(capacity - writeBuffer.length, buffer.getDirectWriteSize());
+
+        buffer.clear();
+        assertEquals(0, buffer.size());
+        assertEquals(0, buffer.getDirectReadSize());
+        assertEquals(capacity, buffer.freeSize());
+        assertEquals(capacity, buffer.getDirectWriteSize());
+    }
+
+    @Test
+    public void writeBytes_withRollover() {
+        doTestReadWriteWithRollover(new BufferAccessor(false, false));
+    }
+
+    @Test
+    public void writeBytes_withFullBuffer() {
+        doTestReadWriteWithFullBuffer(new BufferAccessor(false, false));
+    }
+
+    @Test
+    public void directWriteBytes_withRollover() {
+        doTestReadWriteWithRollover(new BufferAccessor(true, true));
+    }
+
+    @Test
+    public void directWriteBytes_withFullBuffer() {
+        doTestReadWriteWithFullBuffer(new BufferAccessor(true, true));
+    }
+
+    private void doTestReadWriteWithFullBuffer(BufferAccessor accessor) {
+        CircularByteBuffer buffer = doTestReadWrite(accessor, 20, 5, 4);
+
+        assertEquals(0, buffer.size());
+        assertEquals(20, buffer.freeSize());
+    }
+
+    private void doTestReadWriteWithRollover(BufferAccessor accessor) {
+        // All buffer sizes are prime numbers to ensure that some read or write
+        // operations will roll over the end of the internal buffer.
+        CircularByteBuffer buffer = doTestReadWrite(accessor, 31, 13, 7);
+
+        assertNotEquals(0, buffer.size());
+    }
+
+    private CircularByteBuffer doTestReadWrite(BufferAccessor accessor,
+            final int capacity, final int writeLen, final int readLen) {
+        CircularByteBuffer buffer = new CircularByteBuffer(capacity);
+
+        final byte[] writeBuffer = new byte[writeLen + 2];
+        final byte[] peekBuffer = new byte[readLen + 2];
+        final byte[] readBuffer = new byte[readLen + 2];
+
+        final int numIterations = 1011;
+        final int maxRemaining = readLen - 1;
+
+        int currentWriteSymbol = 0;
+        int expectedReadSymbol = 0;
+        int expectedSize = 0;
+        int totalWritten = 0;
+        int totalRead = 0;
+
+        for (int i = 0; i < numIterations; i++) {
+            // Fill in with write buffers as much as possible.
+            while (buffer.freeSize() >= writeLen) {
+                currentWriteSymbol = fillTestBytes(writeBuffer, 1, writeLen, currentWriteSymbol);
+                accessor.writeBytes(buffer, writeBuffer, 1, writeLen);
+
+                expectedSize += writeLen;
+                totalWritten += writeLen;
+                assertEquals(expectedSize, buffer.size());
+                assertEquals(capacity - expectedSize, buffer.freeSize());
+            }
+
+            // Keep reading into read buffers while there's still data.
+            while (buffer.size() >= readLen) {
+                peekBuffer[1] = 0;
+                peekBuffer[2] = 0;
+                buffer.peekBytes(2, peekBuffer, 3, readLen - 2);
+                assertEquals(0, peekBuffer[1]);
+                assertEquals(0, peekBuffer[2]);
+
+                peekBuffer[2] = buffer.peek(1);
+
+                accessor.readBytes(buffer, readBuffer, 1, readLen);
+                peekBuffer[1] = readBuffer[1];
+
+                expectedReadSymbol = checkTestBytes(
+                    readBuffer, 1, readLen, expectedReadSymbol, totalRead);
+
+                assertArrayEquals(peekBuffer, readBuffer);
+
+                expectedSize -= readLen;
+                totalRead += readLen;
+                assertEquals(expectedSize, buffer.size());
+                assertEquals(capacity - expectedSize, buffer.freeSize());
+            }
+
+            if (buffer.size() > maxRemaining) {
+                fail("Too much data remaining: " + buffer.size());
+            }
+        }
+
+        final int maxWritten = capacity * numIterations;
+        final int minWritten = maxWritten / 2;
+        if (totalWritten < minWritten || totalWritten > maxWritten
+                || (totalWritten - totalRead) > maxRemaining) {
+            fail("Unexpected counts: read=" + totalRead + ", written=" + totalWritten
+                    + ", minWritten=" + minWritten + ", maxWritten=" + maxWritten);
+        }
+
+        return buffer;
+    }
+
+    @Test
+    public void readBytes_overflow() {
+        CircularByteBuffer buffer = new CircularByteBuffer(23);
+
+        final byte[] dataBuffer = new byte[15];
+        buffer.writeBytes(dataBuffer, 0, dataBuffer.length - 2);
+
+        try {
+            buffer.readBytes(dataBuffer, 0, dataBuffer.length);
+            assertTrue(false);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        assertEquals(13, buffer.size());
+        assertEquals(10, buffer.freeSize());
+    }
+
+    @Test
+    public void writeBytes_overflow() {
+        CircularByteBuffer buffer = new CircularByteBuffer(23);
+
+        final byte[] dataBuffer = new byte[15];
+        buffer.writeBytes(dataBuffer, 0, dataBuffer.length);
+
+        try {
+            buffer.writeBytes(dataBuffer, 0, dataBuffer.length);
+            assertTrue(false);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        assertEquals(15, buffer.size());
+        assertEquals(8, buffer.freeSize());
+    }
+
+    private static int fillTestBytes(byte[] buffer, int pos, int len, int startValue) {
+        for (int i = 0; i < len; i++) {
+            buffer[pos + i] = (byte) (startValue & 0xFF);
+            startValue = (startValue + 1) % 256;
+        }
+        return startValue;
+    }
+
+    private static int checkTestBytes(
+            byte[] buffer, int pos, int len, int startValue, int totalRead) {
+        for (int i = 0; i < len; i++) {
+            byte expectedValue = (byte) (startValue & 0xFF);
+            if (expectedValue != buffer[pos + i]) {
+                fail("Unexpected byte=" + (((int) buffer[pos + i]) & 0xFF)
+                        + ", expected=" + (((int) expectedValue) & 0xFF)
+                        + ", pos=" + (totalRead + i));
+            }
+            startValue = (startValue + 1) % 256;
+        }
+        return startValue;
+    }
+
+    private static final class BufferAccessor {
+        private final boolean mDirectRead;
+        private final boolean mDirectWrite;
+
+        BufferAccessor(boolean directRead, boolean directWrite) {
+            mDirectRead = directRead;
+            mDirectWrite = directWrite;
+        }
+
+        void writeBytes(CircularByteBuffer buffer, byte[] src, int pos, int len) {
+            if (mDirectWrite) {
+                while (len > 0) {
+                    if (buffer.getDirectWriteSize() == 0) {
+                        fail("Direct write size is zero: free=" + buffer.freeSize()
+                                + ", size=" + buffer.size());
+                    }
+                    int copyLen = Math.min(len, buffer.getDirectWriteSize());
+                    System.arraycopy(src, pos, buffer.getDirectWriteBuffer(),
+                        buffer.getDirectWritePos(), copyLen);
+                    buffer.accountForDirectWrite(copyLen);
+                    len -= copyLen;
+                    pos += copyLen;
+                }
+            } else {
+                buffer.writeBytes(src, pos, len);
+            }
+        }
+
+        void readBytes(CircularByteBuffer buffer, byte[] dst, int pos, int len) {
+            if (mDirectRead) {
+                while (len > 0) {
+                    if (buffer.getDirectReadSize() == 0) {
+                        fail("Direct read size is zero: free=" + buffer.freeSize()
+                                + ", size=" + buffer.size());
+                    }
+                    int copyLen = Math.min(len, buffer.getDirectReadSize());
+                    System.arraycopy(
+                        buffer.getDirectReadBuffer(), buffer.getDirectReadPos(), dst, pos, copyLen);
+                    buffer.accountForDirectRead(copyLen);
+                    len -= copyLen;
+                    pos += copyLen;
+                }
+            } else {
+                buffer.readBytes(dst, pos, len);
+            }
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
new file mode 100644
index 0000000..30e0daf
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.os.Handler
+import android.os.HandlerThread
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val ATTEMPTS = 50 // Causes testWaitForIdle to take about 150ms on aosp_crosshatch-eng
+private const val TIMEOUT_MS = 200
+
+@RunWith(JUnit4::class)
+class HandlerUtilsTest {
+    @Test
+    fun testWaitForIdle() {
+        val handlerThread = HandlerThread("testHandler").apply { start() }
+
+        // Tests that waitForIdle can be called many times without ill impact if the service is
+        // already idle.
+        repeat(ATTEMPTS) {
+            handlerThread.waitForIdle(TIMEOUT_MS)
+        }
+
+        // Tests that calling waitForIdle waits for messages to be processed. Use both an
+        // inline runnable that's instantiated at each loop run and a runnable that's instantiated
+        // once for all.
+        val tempRunnable = object : Runnable {
+            // Use StringBuilder preferentially to StringBuffer because StringBuilder is NOT
+            // thread-safe. It's part of the point that both runnables run on the same thread
+            // so if anything is wrong in that space it's better to opportunistically use a class
+            // where things might go wrong, even if there is no guarantee of failure.
+            var memory = StringBuilder()
+            override fun run() {
+                memory.append("b")
+            }
+        }
+        repeat(ATTEMPTS) { i ->
+            handlerThread.threadHandler.post { tempRunnable.memory.append("a"); }
+            handlerThread.threadHandler.post(tempRunnable)
+            handlerThread.waitForIdle(TIMEOUT_MS)
+            assertEquals(tempRunnable.memory.toString(), "ab".repeat(i + 1))
+        }
+    }
+
+    // Statistical test : even if visibleOnHandlerThread doesn't work this is likely to succeed,
+    // but it will be at least flaky.
+    @Test
+    fun testVisibleOnHandlerThread() {
+        val handlerThread = HandlerThread("testHandler").apply { start() }
+        val handler = Handler(handlerThread.looper)
+
+        repeat(ATTEMPTS) { attempt ->
+            var x = -10
+            visibleOnHandlerThread(handler) { x = attempt }
+            assertEquals(attempt, x)
+            handler.post { assertEquals(attempt, x) }
+        }
+
+        assertFailsWith<IllegalArgumentException> {
+            visibleOnHandlerThread(handler) { throw IllegalArgumentException() }
+        }
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
index f8f2da0..ec7cdbd 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
@@ -22,29 +22,30 @@
 import android.net.Network
 import android.net.NetworkCapabilities
 import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.AVAILABLE
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.BLOCKED_STATUS
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LINK_PROPERTIES_CHANGED
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOSING
-import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.LOST
+import com.android.testutils.RecorderCallback.CallbackEntry.Companion.NETWORK_CAPS_UPDATED
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
 import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
-import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
-import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.junit.Assume.assumeTrue
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import kotlin.reflect.KClass
 import kotlin.test.assertEquals
 import kotlin.test.assertFails
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 
 const val SHORT_TIMEOUT_MS = 20L
 const val DEFAULT_LINGER_DELAY_MS = 30000
@@ -121,20 +122,16 @@
         mCallback.assertNoCallback(SHORT_TIMEOUT_MS)
         mCallback.onAvailable(Network(100))
         assertFails { mCallback.assertNoCallback(SHORT_TIMEOUT_MS) }
-    }
-
-    @Test
-    fun testAssertNoCallbackThat() {
         val net = Network(101)
-        mCallback.assertNoCallbackThat { it is Available }
+        mCallback.assertNoCallback { it is Available }
         mCallback.onAvailable(net)
         // Expect no blocked status change. Receive other callback does not fail the test.
-        mCallback.assertNoCallbackThat { it is BlockedStatus }
+        mCallback.assertNoCallback { it is BlockedStatus }
         mCallback.onBlockedStatusChanged(net, true)
-        assertFails { mCallback.assertNoCallbackThat { it is BlockedStatus } }
+        assertFails { mCallback.assertNoCallback { it is BlockedStatus } }
         mCallback.onBlockedStatusChanged(net, false)
         mCallback.onCapabilitiesChanged(net, NetworkCapabilities())
-        assertFails { mCallback.assertNoCallbackThat { it is CapabilitiesChanged } }
+        assertFails { mCallback.assertNoCallback { it is CapabilitiesChanged } }
     }
 
     @Test
@@ -144,20 +141,28 @@
         val meteredNc = NetworkCapabilities()
         val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED)
         // Check that expecting caps (with or without) fails when no callback has been received.
-        assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
-        assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+        }
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+        }
 
         // Add NOT_METERED and check that With succeeds and Without fails.
         mCallback.onCapabilitiesChanged(net, unmeteredNc)
-        mCallback.expectCapabilitiesWith(NOT_METERED, matcher)
+        mCallback.expectCaps(matcher) { it.hasCapability(NOT_METERED) }
         mCallback.onCapabilitiesChanged(net, unmeteredNc)
-        assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+        }
 
         // Don't add NOT_METERED and check that With fails and Without succeeds.
         mCallback.onCapabilitiesChanged(net, meteredNc)
-        assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+        assertFails {
+            mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+        }
         mCallback.onCapabilitiesChanged(net, meteredNc)
-        mCallback.expectCapabilitiesWithout(NOT_METERED, matcher)
+        mCallback.expectCaps(matcher) { !it.hasCapability(NOT_METERED) }
     }
 
     @Test
@@ -183,37 +188,35 @@
     }
 
     @Test
-    fun testCapabilitiesThat() {
+    fun testExpectCaps() {
         val net = Network(101)
         val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI)
         // Check that expecting capabilitiesThat anything fails when no callback has been received.
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } }
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { true } }
 
         // Basic test for true and false
         mCallback.onCapabilitiesChanged(net, netCaps)
-        mCallback.expectCapabilitiesThat(net) { true }
+        mCallback.expectCaps(net) { true }
         mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } }
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { false } }
 
         // Try a positive and a negative case
         mCallback.onCapabilitiesChanged(net, netCaps)
-        mCallback.expectCapabilitiesThat(net) { caps ->
-            caps.hasCapability(NOT_METERED) &&
-                    caps.hasTransport(WIFI) &&
-                    !caps.hasTransport(CELLULAR)
+        mCallback.expectCaps(net) {
+            it.hasCapability(NOT_METERED) && it.hasTransport(WIFI) && !it.hasTransport(CELLULAR)
         }
         mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps ->
-            caps.hasTransport(CELLULAR)
-        } }
+        assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { it.hasTransport(CELLULAR) } }
 
         // Try a matching callback on the wrong network
         mCallback.onCapabilitiesChanged(net, netCaps)
-        assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } }
+        assertFails {
+            mCallback.expectCaps(Network(100), SHORT_TIMEOUT_MS) { true }
+        }
     }
 
     @Test
-    fun testLinkPropertiesThat() {
+    fun testLinkPropertiesCallbacks() {
         val net = Network(112)
         val linkAddress = LinkAddress("fe80::ace:d00d/64")
         val mtu = 1984
@@ -224,30 +227,30 @@
         }
 
         // Check that expecting linkPropsThat anything fails when no callback has been received.
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } }
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { true } }
 
         // Basic test for true and false
         mCallback.onLinkPropertiesChanged(net, linkProps)
-        mCallback.expectLinkPropertiesThat(net) { true }
+        mCallback.expect<LinkPropertiesChanged>(net) { true }
         mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } }
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { false } }
 
         // Try a positive and negative case
         mCallback.onLinkPropertiesChanged(net, linkProps)
-        mCallback.expectLinkPropertiesThat(net) { lp ->
-            lp.interfaceName == TEST_INTERFACE_NAME &&
-                    lp.linkAddresses.contains(linkAddress) &&
-                    lp.mtu == mtu
+        mCallback.expect<LinkPropertiesChanged>(net) {
+            it.lp.interfaceName == TEST_INTERFACE_NAME &&
+                    it.lp.linkAddresses.contains(linkAddress) &&
+                    it.lp.mtu == mtu
         }
         mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp ->
-            lp.interfaceName != TEST_INTERFACE_NAME
+        assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) {
+            it.lp.interfaceName != TEST_INTERFACE_NAME
         } }
 
         // Try a matching callback on the wrong network
         mCallback.onLinkPropertiesChanged(net, linkProps)
-        assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp ->
-            lp.interfaceName == TEST_INTERFACE_NAME
+        assertFails { mCallback.expect<LinkPropertiesChanged>(Network(114), SHORT_TIMEOUT_MS) {
+            it.lp.interfaceName == TEST_INTERFACE_NAME
         } }
     }
 
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index c9b3d07..bcf89b3 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -36,6 +36,7 @@
         "libnanohttpd",
         "net-tests-utils-host-device-common",
         "net-utils-device-common",
+        "net-utils-device-common-async",
         "net-utils-device-common-netlink",
         "modules-utils-build_system",
     ],
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
index 861f45e..aa252a5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -21,9 +21,14 @@
 import android.os.ConditionVariable
 import android.os.Handler
 import android.os.HandlerThread
+import android.util.Log
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import java.lang.Exception
 import java.util.concurrent.Executor
 import kotlin.test.fail
 
+private const val TAG = "HandlerUtils"
+
 /**
  * Block until the specified Handler or HandlerThread becomes idle, or until timeoutMs has passed.
  */
@@ -48,3 +53,31 @@
         fail("Executor did not become idle after ${timeoutMs}ms")
     }
 }
+
+/**
+ * Executes a block of code, making its side effects visible on the caller and the handler thread
+ *
+ * After this function returns, the side effects of the passed block of code are guaranteed to be
+ * observed both on the thread running the handler and on the thread running this method.
+ * To achieve this, this method runs the passed block on the handler and blocks this thread
+ * until it's executed, so keep in mind this method will block, (including, if the handler isn't
+ * running, blocking forever).
+ */
+fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) {
+    val cv = ConditionVariable()
+    var e: Exception? = null
+    handler.post {
+        try {
+            r.run()
+        } catch (exception: Exception) {
+            Log.e(TAG, "visibleOnHandlerThread caught exception", exception)
+            e = exception
+        }
+        cv.open()
+    }
+    // After block() returns, the handler thread has seen the change (since it ran it)
+    // and this thread also has seen the change (since cv.open() happens-before cv.block()
+    // returns).
+    cv.block()
+    e?.let { throw it }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 124d134..485799c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -83,7 +83,7 @@
         ) : CallbackEntry()
         data class BlockedStatusInt(
             override val network: Network,
-            val blocked: Int
+            val reason: Int
         ) : CallbackEntry()
         // Convenience constants for expecting a type
         companion object {
@@ -167,20 +167,43 @@
 
 private const val DEFAULT_TIMEOUT = 30_000L // ms
 private const val DEFAULT_NO_CALLBACK_TIMEOUT = 200L // ms
+private val NOOP = Runnable {}
 
+/**
+ * See comments on the public constructor below for a description of the arguments.
+ */
 open class TestableNetworkCallback private constructor(
     src: TestableNetworkCallback?,
     val defaultTimeoutMs: Long = DEFAULT_TIMEOUT,
-    val defaultNoCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT
+    val defaultNoCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
+    val waiterFunc: Runnable = NOOP // "() -> Unit" would forbid calling with a void func from Java
 ) : RecorderCallback(src) {
+    /**
+     * Construct a testable network callback.
+     * @param timeoutMs the default timeout for expecting a callback. Default 30 seconds. This
+     *                  should be long in most cases, because the success case doesn't incur
+     *                  the wait.
+     * @param noCallbackTimeoutMs the timeout for expecting that no callback is received. Default
+     *                            200ms. Because the success case does incur the timeout, this
+     *                            should be short in most cases, but not so short as to frequently
+     *                            time out before an incorrect callback is received.
+     * @param waiterFunc a function to use before asserting no callback. For some specific tests,
+     *                   it is useful to run test-specific code before asserting no callback to
+     *                   increase the likelihood that a spurious callback is correctly detected.
+     *                   As an example, a unit test using mock loopers may want to use this to
+     *                   make sure the loopers are drained before asserting no callback, since
+     *                   one of them may cause a callback to be called. @see ConnectivityServiceTest
+     *                   for such an example.
+     */
     @JvmOverloads
     constructor(
         timeoutMs: Long = DEFAULT_TIMEOUT,
-        noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT
-    ) : this(null, timeoutMs, noCallbackTimeoutMs)
+        noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
+        waiterFunc: Runnable = NOOP
+    ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc)
 
     fun createLinkedCopy() = TestableNetworkCallback(
-            this, defaultTimeoutMs, defaultNoCallbackTimeoutMs)
+            this, defaultTimeoutMs, defaultNoCallbackTimeoutMs, waiterFunc)
 
     // The last available network, or null if any network was lost since the last call to
     // onAvailable. TODO : fix this by fixing the tests that rely on this behavior
@@ -343,25 +366,18 @@
         test: (T) -> Boolean = { true }
     ) = expect(network.network, timeoutMs, errorMsg, test)
 
-    // Make open for use in ConnectivityServiceTest which is the only one knowing its handlers.
-    // TODO : remove the necessity to overload this, remove the open qualifier, and give a
-    // default argument to assertNoCallback instead, possibly with @JvmOverloads if necessary.
-    open fun assertNoCallback() = assertNoCallback(defaultNoCallbackTimeoutMs)
-
-    fun assertNoCallback(timeoutMs: Long) {
-        val cb = history.poll(timeoutMs)
-        if (null != cb) fail("Expected no callback but got $cb")
-    }
-
-    fun assertNoCallbackThat(
+    @JvmOverloads
+    fun assertNoCallback(
         timeoutMs: Long = defaultNoCallbackTimeoutMs,
-        valid: (CallbackEntry) -> Boolean
+        valid: (CallbackEntry) -> Boolean = { true }
     ) {
-        val cb = history.poll(timeoutMs) { valid(it) }.let {
-            if (null != it) fail("Expected no callback but got $it")
-        }
+        waiterFunc.run()
+        history.poll(timeoutMs) { valid(it) }?.let { fail("Expected no callback but got $it") }
     }
 
+    fun assertNoCallback(valid: (CallbackEntry) -> Boolean) =
+            assertNoCallback(defaultNoCallbackTimeoutMs, valid)
+
     // Expects a callback of the specified type matching the predicate within the timeout.
     // Any callback that doesn't match the predicate will be skipped. Fails only if
     // no matching callback is received within the timeout.
@@ -398,20 +414,6 @@
         crossinline predicate: (T) -> Boolean = { true }
     ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
 
-    inline fun expectCapabilitiesThat(
-        net: Network,
-        tmt: Long = defaultTimeoutMs,
-        valid: (NetworkCapabilities) -> Boolean
-    ): CapabilitiesChanged =
-            expect(net, tmt, "Capabilities don't match expectations") { valid(it.caps) }
-
-    inline fun expectLinkPropertiesThat(
-        net: Network,
-        tmt: Long = defaultTimeoutMs,
-        valid: (LinkProperties) -> Boolean
-    ): LinkPropertiesChanged =
-            expect(net, tmt, "LinkProperties don't match expectations") { valid(it.lp) }
-
     // Expects onAvailable and the callbacks that follow it. These are:
     // - onSuspended, iff the network was suspended when the callbacks fire.
     // - onCapabilitiesChanged.
@@ -432,18 +434,18 @@
         tmt: Long = defaultTimeoutMs
     ) {
         expectAvailableCallbacksCommon(net, suspended, validated, tmt)
-        expectBlockedStatusCallback(blocked, net, tmt)
+        expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
     }
 
     fun expectAvailableCallbacks(
         net: Network,
         suspended: Boolean,
         validated: Boolean,
-        blockedStatus: Int,
+        blockedReason: Int,
         tmt: Long
     ) {
         expectAvailableCallbacksCommon(net, suspended, validated, tmt)
-        expectBlockedStatusCallback(blockedStatus, net)
+        expect<BlockedStatusInt>(net) { it.reason == blockedReason }
     }
 
     private fun expectAvailableCallbacksCommon(
@@ -456,10 +458,8 @@
         if (suspended) {
             expect<Suspended>(net, tmt)
         }
-        expectCapabilitiesThat(net, tmt) {
-            validated == null || validated == it.hasCapability(
-                NET_CAPABILITY_VALIDATED
-            )
+        expect<CapabilitiesChanged>(net, tmt) {
+            validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
         }
         expect<LinkPropertiesChanged>(net, tmt)
     }
@@ -472,16 +472,6 @@
         tmt: Long = defaultTimeoutMs
     ) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
 
-    fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) =
-            expect<BlockedStatus>(net, tmt, "Unexpected blocked status") {
-                it.blocked == blocked
-            }
-
-    fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) =
-            expect<BlockedStatusInt>(net, tmt, "Unexpected blocked status") {
-                it.blocked == blocked
-            }
-
     // Expects the available callbacks (where the onCapabilitiesChanged must contain the
     // VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
     // one we just sent.
@@ -498,17 +488,17 @@
     // when a network connects and satisfies a callback, and then immediately validates.
     fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
         expectAvailableCallbacks(net, validated = false, tmt = tmt)
-        expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+        expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
     }
 
     fun expectAvailableThenValidatedCallbacks(
         net: Network,
-        blockedStatus: Int,
+        blockedReason: Int,
         tmt: Long = defaultTimeoutMs
     ) {
         expectAvailableCallbacks(net, validated = false, suspended = false,
-                blockedStatus = blockedStatus, tmt = tmt)
-        expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+                blockedReason = blockedReason, tmt = tmt)
+        expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
     }
 
     // Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
@@ -555,38 +545,26 @@
     }
 
     @JvmOverloads
-    fun expectLinkPropertiesThat(
+    fun expectCaps(
         n: HasNetwork,
         tmt: Long = defaultTimeoutMs,
-        valid: (LinkProperties) -> Boolean
-    ) = expectLinkPropertiesThat(n.network, tmt, valid)
+        valid: (NetworkCapabilities) -> Boolean = { true }
+    ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
 
     @JvmOverloads
-    fun expectCapabilitiesThat(
-        n: HasNetwork,
+    fun expectCaps(
+        n: Network,
         tmt: Long = defaultTimeoutMs,
         valid: (NetworkCapabilities) -> Boolean
-    ) = expectCapabilitiesThat(n.network, tmt, valid)
+    ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
 
-    @JvmOverloads
-    fun expectCapabilitiesWith(
-        capability: Int,
+    fun expectCaps(
         n: HasNetwork,
-        timeoutMs: Long = defaultTimeoutMs
-    ): NetworkCapabilities {
-        return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps
-    }
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
 
-    @JvmOverloads
-    fun expectCapabilitiesWithout(
-        capability: Int,
-        n: HasNetwork,
-        timeoutMs: Long = defaultTimeoutMs
-    ): NetworkCapabilities {
-        return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps
-    }
-
-    fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
-        expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
-    }
+    fun expectCaps(
+        tmt: Long,
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 0000000..1b8e26b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * 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.testutils;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+    public static final boolean ENABLE_FINE_DEBUG = true;
+
+    public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+    private enum FileType { PAIR, PIPE }
+
+    // Common poll() constants:
+    private static final short POLLIN  = 0x0001;
+    private static final short POLLOUT = 0x0004;
+    private static final short POLLERR = 0x0008;
+    private static final short POLLHUP = 0x0010;
+
+    private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+    private static final Field FD_FIELD_DESCRIPTOR;
+    private static final Field PFD_FIELD_DESCRIPTOR;
+    private static final Field PFD_FIELD_GUARD;
+    private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+    private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+    private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+    private final HashMap<Integer, File> mFiles = new HashMap<>();
+    private final byte[] mTmpBuffer = new byte[1024];
+    private final long mStartTime;
+    private final String mLogTag;
+    private int mFileNumberGen = 3;
+    private boolean mHasRateLimitedData;
+
+    public FakeOsAccess(String logTag) {
+        mLogTag = logTag;
+        mStartTime = monotonicTimeMillis();
+    }
+
+    @Override
+    public long monotonicTimeMillis() {
+        return System.nanoTime() / 1000000;
+    }
+
+    @Override
+    public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+        try {
+            return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void close(ParcelFileDescriptor fd) {
+        if (fd != null) {
+            close(getInnerFileDescriptor(fd));
+
+            try {
+                // Reduce CloseGuard warnings.
+                Object guard = PFD_FIELD_GUARD.get(fd);
+                CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public synchronized void close(FileDescriptor fd) {
+        if (fd != null) {
+            File file = getFileOrNull(fd);
+            if (file != null) {
+                file.decreaseRefCount();
+                mFiles.remove(getFileDescriptorNumber(fd));
+                setFileDescriptorNumber(fd, -1);
+                notifyAll();
+            }
+        }
+    }
+
+    private File getFile(String func, FileDescriptor fd) throws IOException {
+        File file = getFileOrNull(fd);
+        if (file == null) {
+            throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+        }
+        return file;
+    }
+
+    private File getFileOrNull(FileDescriptor fd) {
+        return mFiles.get(getFileDescriptorNumber(fd));
+    }
+
+    @Override
+    public String getFileDebugName(ParcelFileDescriptor fd) {
+        return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+    }
+
+    public String getFileDebugName(FileDescriptor fd) {
+        if (fd == null) {
+            return "null";
+        }
+
+        final int fdNumber = getFileDescriptorNumber(fd);
+        File file = mFiles.get(fdNumber);
+
+        StringBuilder sb = new StringBuilder();
+        if (file != null) {
+            if (file.name != null) {
+                sb.append(file.name);
+                sb.append("/");
+            }
+            sb.append(file.type);
+            sb.append("/");
+        } else {
+            sb.append("BADFD/");
+        }
+        sb.append(fdNumber);
+        return sb.toString();
+    }
+
+    public synchronized void setFileName(FileDescriptor fd, String name) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.name = name;
+        }
+    }
+
+    @Override
+    public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+        File file = getFile("fcntl", fd);
+        file.isBlocking = false;
+    }
+
+    @Override
+    public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException {
+        checkBoundaries("read", buffer, pos, len);
+
+        File file = getFile("read", fd);
+        if (file.readQueue == null) {
+            throw newIOException("read", "File not readable");
+        }
+        file.checkNonBlocking("read");
+
+        if (len == 0) {
+            return 0;
+        }
+
+        final int availSize = file.readQueue.size();
+        if (availSize == 0) {
+            if (file.isEndOfStream) {
+                // Java convention uses -1 to indicate end of stream.
+                return -1;
+            }
+            return 0;  // EAGAIN
+        }
+
+        final int readCount = Math.min(len, availSize);
+        file.readQueue.readBytes(buffer, pos, readCount);
+        maybeTransferData(file);
+        return readCount;
+    }
+
+    @Override
+    public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+            throws IOException {
+        checkBoundaries("write", buffer, pos, len);
+
+        File file = getFile("write", fd);
+        if (file.writeQueue == null) {
+            throw newIOException("read", "File not writable");
+        }
+        if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+            throw newIOException("write", "The other end of pipe is closed");
+        }
+        file.checkNonBlocking("write");
+
+        if (len == 0) {
+            return 0;
+        }
+
+        final int originalFreeSize = file.writeQueue.freeSize();
+        if (originalFreeSize == 0) {
+            return 0;  // EAGAIN
+        }
+
+        final int writeCount = Math.min(len, originalFreeSize);
+        file.writeQueue.writeBytes(buffer, pos, writeCount);
+        maybeTransferData(file);
+
+        if (file.writeQueue.freeSize() < originalFreeSize) {
+            final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+            Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+                + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+                + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+        }
+
+        return writeCount;
+    }
+
+    private void maybeTransferData(File file) {
+        boolean hasChanges = copyFileBuffers(file, file.sink);
+        hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+        if (hasChanges) {
+            // TODO(b/245971639): Avoid notifying if no-one is polling.
+            notifyAll();
+        }
+    }
+
+    private boolean copyFileBuffers(File src, File dst) {
+        if (src.writeQueue == null || dst.readQueue == null) {
+            return false;
+        }
+
+        final int originalCopyCount = Math.min(mTmpBuffer.length,
+            Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+        final int allowedCopyCount = RateLimiter.limit(
+            src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+        if (allowedCopyCount < originalCopyCount) {
+            if (ENABLE_FINE_DEBUG) {
+                Log.i(mLogTag, logStr("Delaying transfer of "
+                    + (originalCopyCount - allowedCopyCount) + " bytes, original="
+                    + originalCopyCount + ", allowed=" + allowedCopyCount
+                    + ", type=" + src.type));
+            }
+            if (originalCopyCount > 0) {
+                mHasRateLimitedData = true;
+            }
+            if (allowedCopyCount == 0) {
+                return false;
+            }
+        }
+
+        boolean hasChanges = false;
+        if (allowedCopyCount > 0) {
+            if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+                hasChanges = true;  // Read queue had no data, or write queue was full.
+            }
+            src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+            dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+        }
+
+        if (!dst.isEndOfStream && src.openCount == 0
+                && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+            dst.isEndOfStream = true;
+            hasChanges = true;
+        }
+
+        return hasChanges;
+    }
+
+    public void clearInboundRateLimit(FileDescriptor fd) {
+        setInboundRateLimit(fd, Integer.MAX_VALUE);
+    }
+
+    public void clearOutboundRateLimit(FileDescriptor fd) {
+        setOutboundRateLimit(fd, Integer.MAX_VALUE);
+    }
+
+    public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+            maybeTransferData(file);
+        }
+    }
+
+    public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+        File file = getFileOrNull(fd);
+        if (file != null) {
+            file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+            maybeTransferData(file);
+        }
+    }
+
+    public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+        int fdNumber1 = getNextFd("socketpair");
+        int fdNumber2 = getNextFd("socketpair");
+
+        File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+        File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+        return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+    }
+
+    @Override
+    public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+        int fdNumber1 = getNextFd("pipe");
+        int fdNumber2 = getNextFd("pipe");
+
+        File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+        File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+        return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+    }
+
+    private ParcelFileDescriptor[] registerFilePair(
+            int fdNumber1, File file1, int fdNumber2, File file2) {
+        file1.sink = file2;
+        file1.source = file2;
+        file2.sink = file1;
+        file2.source = file1;
+
+        mFiles.put(fdNumber1, file1);
+        mFiles.put(fdNumber2, file2);
+        return new ParcelFileDescriptor[] {
+            newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+    }
+
+    @Override
+    public short getPollInMask() {
+        return POLLIN;
+    }
+
+    @Override
+    public short getPollOutMask() {
+        return POLLOUT;
+    }
+
+    @Override
+    public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+        if (timeoutMs < 0) {
+            timeoutMs = (int) TimeUnit.HOURS.toMillis(1);  // Make "infinite" equal to 1 hour.
+        }
+
+        if (fds == null || fds.length > 1000) {
+            throw newIOException("poll", "Invalid fds param");
+        }
+        for (StructPollfd pollFd : fds) {
+            getFile("poll", pollFd.fd);
+        }
+
+        int waitCallCount = 0;
+        final long deadline = monotonicTimeMillis() + timeoutMs;
+        while (true) {
+            if (mHasRateLimitedData) {
+                mHasRateLimitedData = false;
+                for (File file : mFiles.values()) {
+                    if (file.inboundLimiter.getLastRequestReduction() != 0) {
+                        copyFileBuffers(file.source, file);
+                    }
+                    if (file.outboundLimiter.getLastRequestReduction() != 0) {
+                        copyFileBuffers(file, file.sink);
+                    }
+                }
+            }
+
+            final int readyCount = calculateReadyCount(fds);
+            if (readyCount > 0) {
+                if (ENABLE_FINE_DEBUG) {
+                    Log.v(mLogTag, logStr("Poll returns " + readyCount
+                            + " after " + waitCallCount + " wait calls"));
+                }
+                return readyCount;
+            }
+
+            long remainingTimeoutMs = deadline - monotonicTimeMillis();
+            if (remainingTimeoutMs <= 0) {
+                if (ENABLE_FINE_DEBUG) {
+                    Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+                            + "ms after " + waitCallCount + " wait calls"));
+                }
+                return 0;
+            }
+
+            if (mHasRateLimitedData) {
+                remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+            }
+
+            try {
+                wait(remainingTimeoutMs);
+            } catch (InterruptedException e) {
+                // Ignore and retry
+            }
+            waitCallCount++;
+        }
+    }
+
+    private int calculateReadyCount(StructPollfd[] fds) {
+        int fdCount = 0;
+        for (StructPollfd pollFd : fds) {
+            pollFd.revents = 0;
+
+            File file = getFileOrNull(pollFd.fd);
+            if (file == null) {
+                Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+                        + getFileDebugName(pollFd.fd)));
+                continue;
+            }
+
+            if (ENABLE_FINE_DEBUG) {
+                Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+                        + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+                        + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+                        + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+            }
+
+            if ((pollFd.events & POLLIN) != 0) {
+                if (file.readQueue != null && file.readQueue.size() != 0) {
+                    pollFd.revents |= POLLIN;
+                }
+                if (file.isEndOfStream) {
+                    pollFd.revents |= POLLHUP;
+                }
+            }
+
+            if ((pollFd.events & POLLOUT) != 0) {
+                if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+                    pollFd.revents |= POLLERR;
+                }
+                if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+                    pollFd.revents |= POLLOUT;
+                }
+            }
+
+            if (pollFd.revents != 0) {
+                fdCount++;
+            }
+        }
+        return fdCount;
+    }
+
+    private int getNextFd(String func) throws IOException {
+        if (mFileNumberGen > 100000) {
+            throw newIOException(func, "Too many files open");
+        }
+
+        return mFileNumberGen++;
+    }
+
+    private static IOException newIOException(String func, String message) {
+        return new IOException(message + ", func=" + func);
+    }
+
+    public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+            throws IOException {
+        if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+            throw newIOException(func, "Invalid array bounds");
+        }
+    }
+
+    private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+        try {
+            return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private FileDescriptor newFileDescriptor(int fdNumber) {
+        try {
+            return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public int getFileDescriptorNumber(FileDescriptor fd) {
+        try {
+            return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+        try {
+            FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private String logStr(String message) {
+        return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+    }
+
+    private class File {
+        final FileType type;
+        final CircularByteBuffer readQueue;
+        final CircularByteBuffer writeQueue;
+        final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+        final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+        String name;
+        int openCount = 1;
+        boolean isBlocking = true;
+        File sink;
+        File source;
+        boolean isEndOfStream;
+
+        File(FileType type, int readQueueSize, int writeQueueSize) {
+            this.type = type;
+            readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+            writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+        }
+
+        void decreaseRefCount() {
+            if (openCount <= 0) {
+                throw new IllegalStateException();
+            }
+            openCount--;
+        }
+
+        void checkNonBlocking(String func) throws IOException {
+            if (isBlocking) {
+                throw newIOException(func, "File in blocking mode");
+            }
+        }
+    }
+
+    static {
+        try {
+            FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+            FD_CONSTRUCTOR.setAccessible(true);
+
+            Field descriptorIntField;
+            try {
+                descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+            } catch (NoSuchFieldException e) {
+                descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+            }
+            FD_FIELD_DESCRIPTOR = descriptorIntField;
+            FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+            PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+            PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+            PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+            PFD_FIELD_GUARD.setAccessible(true);
+
+            CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+                .getDeclaredMethod("close");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 0000000..137873d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * 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.testutils;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+    private static final int PERIOD_DURATION_MS = 1000;
+    private static final int BUCKET_COUNT = 100;
+
+    public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+    private final OsAccess mOsAccess;
+    private final int[] mStatBuckets = new int[BUCKET_COUNT];
+    private int mMaxPerPeriodBytes;
+    private int mMaxPerBucketBytes;
+    private int mRecordedPeriodBytes;
+    private long mLastLimitTimestamp;
+    private int mLastRequestReduction;
+
+    public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+        mOsAccess = osAccess;
+        setBytesPerSecond(bytesPerSecond);
+        clear();
+    }
+
+    public int getBytesPerSecond() {
+        return mMaxPerPeriodBytes;
+    }
+
+    public void setBytesPerSecond(int bytesPerSecond) {
+        mMaxPerPeriodBytes = bytesPerSecond;
+        mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+    }
+
+    public void clear() {
+        mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+        mRecordedPeriodBytes = 0;
+        Arrays.fill(mStatBuckets, 0);
+    }
+
+    public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+        final long now = limiter1.mOsAccess.monotonicTimeMillis();
+        final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+            limiter2.calculateLimit(now, requestedBytes));
+        limiter1.recordBytes(now, requestedBytes, allowedCount);
+        limiter2.recordBytes(now, requestedBytes, allowedCount);
+        return allowedCount;
+    }
+
+    public int limit(int requestedBytes) {
+        final long now = mOsAccess.monotonicTimeMillis();
+        final int allowedCount = calculateLimit(now, requestedBytes);
+        recordBytes(now, requestedBytes, allowedCount);
+        return allowedCount;
+    }
+
+    public int getLastRequestReduction() {
+        return mLastRequestReduction;
+    }
+
+    public boolean acceptAllOrNone(int requestedBytes) {
+        final long now = mOsAccess.monotonicTimeMillis();
+        final int allowedCount = calculateLimit(now, requestedBytes);
+        if (allowedCount < requestedBytes) {
+            return false;
+        }
+        recordBytes(now, requestedBytes, allowedCount);
+        return true;
+    }
+
+    private int calculateLimit(long now, int requestedBytes) {
+        // First remove all stale bucket data and adjust the total.
+        final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+        final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+        for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+            final int idx = (int) (i % BUCKET_COUNT);
+            mRecordedPeriodBytes -= mStatBuckets[idx];
+            mStatBuckets[idx] = 0;
+        }
+
+        final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+        final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+            Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+        return Math.max(0, maxAllowed);
+    }
+
+    private void recordBytes(long now, int requestedBytes, int actualBytes) {
+        mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+        mRecordedPeriodBytes += actualBytes;
+        mLastRequestReduction = requestedBytes - actualBytes;
+        mLastLimitTimestamp = now;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("{max=");
+        sb.append(mMaxPerPeriodBytes);
+        sb.append(",max_bucket=");
+        sb.append(mMaxPerBucketBytes);
+        sb.append(",total=");
+        sb.append(mRecordedPeriodBytes);
+        sb.append(",last_red=");
+        sb.append(mLastRequestReduction);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
new file mode 100644
index 0000000..5af890f
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String)