Merge "Add packet filters for IPv6 UDP"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 1fe6c2c..ff65228 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -36,7 +36,6 @@
   srcs: [
       "device/com/android/net/module/util/DeviceConfigUtils.java",
       "device/com/android/net/module/util/FdEventsReader.java",
-      "device/com/android/net/module/util/HexDump.java",
       "device/com/android/net/module/util/NetworkMonitorUtils.java",
       "device/com/android/net/module/util/PacketReader.java",
       "device/com/android/net/module/util/SharedLog.java",
@@ -121,11 +120,11 @@
         "device/com/android/net/module/util/BpfDump.java",
         "device/com/android/net/module/util/BpfMap.java",
         "device/com/android/net/module/util/BpfUtils.java",
-        "device/com/android/net/module/util/HexDump.java",
         "device/com/android/net/module/util/IBpfMap.java",
         "device/com/android/net/module/util/JniUtil.java",
         "device/com/android/net/module/util/Struct.java",
         "device/com/android/net/module/util/TcUtils.java",
+        "framework/com/android/net/module/util/HexDump.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "29",
@@ -149,7 +148,6 @@
 java_library {
     name: "net-utils-device-common-struct",
     srcs: [
-        "device/com/android/net/module/util/HexDump.java",
         "device/com/android/net/module/util/Ipv6Utils.java",
         "device/com/android/net/module/util/PacketBuilder.java",
         "device/com/android/net/module/util/Struct.java",
@@ -267,6 +265,14 @@
         "//packages/apps/Settings",
     ],
     lint: { strict_updatability_linting: true },
+    errorprone: {
+        enabled: true,
+        // Error-prone checking only warns of problems when building. To make the build fail with
+        // these errors, list the specific error-prone problems below.
+        javacflags: [
+            "-Xep:NullablePrimitive:ERROR",
+        ],
+    },
 }
 
 java_library {
@@ -328,6 +334,63 @@
     lint: { strict_updatability_linting: true },
 }
 
+java_library {
+    name: "net-utils-device-common-wear",
+    srcs: [
+        "device/com/android/net/module/util/wear/*.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: [
+        "net-utils-device-common-async",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    lint: { strict_updatability_linting: true },
+}
+
+// Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
+// the mDNS code can build with only system APIs.
+// The mDNS code is platform code so it should use framework-annotations-lib, contrary to apps that
+// should use sdk_version: "system_current" and only androidx.annotation_annotation. But this build
+// rule verifies that the mDNS code can be built into apps, if code transformations are applied to
+// the annotations.
+// When using "system_current", framework annotations are not available; they would appear as
+// package-private as they are marked as such in the system_current stubs. So build against
+// core_platform and add the stubs manually in "libs". See http://b/147773144#comment7.
+java_library {
+    name: "net-utils-device-common-mdns-standalone-build-test",
+    // Build against core_platform and add the stub libraries manually in "libs", as annotations
+    // are already included in android_system_stubs_current but package-private, so
+    // "framework-annotations-lib" needs to be manually included before
+    // "android_system_stubs_current" (b/272392042)
+    sdk_version: "core_platform",
+    srcs: [
+        "device/com/android/net/module/util/FdEventsReader.java",
+        "device/com/android/net/module/util/SharedLog.java",
+        "framework/com/android/net/module/util/ByteUtils.java",
+        "framework/com/android/net/module/util/CollectionUtils.java",
+        "framework/com/android/net/module/util/HexDump.java",
+        "framework/com/android/net/module/util/LinkPropertiesUtils.java",
+    ],
+    libs: [
+        "framework-annotations-lib",
+        "android_system_stubs_current",
+        "androidx.annotation_annotation",
+    ],
+    visibility: ["//packages/modules/Connectivity/service-t"],
+}
+
 // 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/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 9042085..9df2b03 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -70,8 +70,11 @@
     private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode,
                                                        int keySize, int valueSize)
             throws ErrnoException, NullPointerException {
-        // TODO: key should include keySize & valueSize, but really we should match specific types
-        Pair<String, Integer> key = Pair.create(path, mode);
+        // Supports up to 1023 byte key and 65535 byte values
+        // Creating a BpfMap with larger keys/values seems like a bad idea any way...
+        keySize &= 1023; // 10-bits
+        valueSize &= 65535; // 16-bits
+        var key = Pair.create(path, (mode << 26) ^ (keySize << 16) ^ valueSize);
         // unlocked fetch is safe: map is concurrent read capable, and only inserted into
         ParcelFileDescriptor fd = sFdCache.get(key);
         if (fd != null) return fd;
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 1225aa7..dae4eb9 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -16,9 +16,12 @@
 
 package com.android.net.module.util;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
 import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
 import android.util.Log;
@@ -28,6 +31,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Utilities for modules to query {@link DeviceConfig} and flags.
  */
@@ -43,6 +49,11 @@
     public static final String TETHERING_MODULE_NAME = "com.android.tethering";
 
     @VisibleForTesting
+    public static final String RESOURCES_APK_INTENT =
+            "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
+    private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/";
+
+    @VisibleForTesting
     public static void resetPackageVersionCacheForTest() {
         sPackageVersion = -1;
         sModuleVersion = -1;
@@ -189,23 +200,20 @@
      */
     public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
             @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+        // TODO: migrate callers to a non-generic isTetheringFeatureEnabled method.
+        if (!TETHERING_MODULE_NAME.equals(moduleName)) {
+            throw new IllegalArgumentException(
+                    "This method is only usable by the tethering module");
+        }
         try {
-            final long packageVersion = getModuleVersion(context, moduleName);
+            final long packageVersion = getTetheringModuleVersion(context);
             return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled);
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Could not find the module name", e);
-            return defaultEnabled;
+            return false;
         }
     }
 
-    private static boolean maybeUseFixedPackageVersion(@NonNull Context context) {
-        final String packageName = context.getPackageName();
-        if (packageName == null) return false;
-
-        return packageName.equals("com.android.networkstack.tethering")
-                || packageName.equals("com.android.networkstack.tethering.inprocess");
-    }
-
     private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion,
             @NonNull String namespace, String name, boolean defaultEnabled)
             throws PackageManager.NameNotFoundException {
@@ -215,36 +223,40 @@
                 || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
     }
 
+    // Guess the tethering module name based on the package prefix of the connectivity resources
+    // Take the resource package name, cut it before "connectivity" and append "tethering".
+    // Then resolve that package version number with packageManager.
+    // If that fails retry by appending "go.tethering" instead
+    private static long resolveTetheringModuleVersion(@NonNull Context context)
+            throws PackageManager.NameNotFoundException {
+        final String connResourcesPackage = getConnectivityResourcesPackageName(context);
+        final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity");
+        if (pkgPrefixLen < 0) {
+            throw new IllegalStateException(
+                    "Invalid connectivity resources package: " + connResourcesPackage);
+        }
+
+        final String pkgPrefix = connResourcesPackage.substring(0, pkgPrefixLen);
+        final PackageManager packageManager = context.getPackageManager();
+        try {
+            return packageManager.getPackageInfo(pkgPrefix + "tethering",
+                    PackageManager.MATCH_APEX).getLongVersionCode();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Device is using go modules");
+            // fall through
+        }
+
+        return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+                PackageManager.MATCH_APEX).getLongVersionCode();
+    }
+
     private static volatile long sModuleVersion = -1;
-    @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10;
-    private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName)
+    private static long getTetheringModuleVersion(@NonNull Context context)
             throws PackageManager.NameNotFoundException {
         if (sModuleVersion >= 0) return sModuleVersion;
 
-        final PackageManager packageManager = context.getPackageManager();
-        ModuleInfo module;
-        try {
-            module = packageManager.getModuleInfo(
-                    moduleName, PackageManager.MODULE_APEX_NAME);
-        } catch (PackageManager.NameNotFoundException e) {
-            // The error may happen if mainline module meta data is not installed e.g. there are
-            // no meta data configuration in AOSP build. To be able to enable a feature in AOSP
-            // by setting a flag via ADB for example. set a small non-zero fixed number for
-            // comparing.
-            if (maybeUseFixedPackageVersion(context)) {
-                sModuleVersion = FIXED_PACKAGE_VERSION;
-                return FIXED_PACKAGE_VERSION;
-            } else {
-                throw e;
-            }
-        }
-        String modulePackageName = module.getPackageName();
-        if (modulePackageName == null) throw new PackageManager.NameNotFoundException(moduleName);
-        final long version = packageManager.getPackageInfo(modulePackageName,
-                PackageManager.MATCH_APEX).getLongVersionCode();
-        sModuleVersion = version;
-
-        return version;
+        sModuleVersion = resolveTetheringModuleVersion(context);
+        return sModuleVersion;
     }
 
     /**
@@ -272,4 +284,24 @@
             return defaultValue;
         }
     }
+
+    /**
+     * Get the package name of the ServiceConnectivityResources package, used to provide resources
+     * for service-connectivity.
+     */
+    @NonNull
+    public static String getConnectivityResourcesPackageName(@NonNull Context context) {
+        final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager()
+                .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY));
+        pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(
+                CONNECTIVITY_RES_PKG_DIR));
+        if (pkgs.size() > 1) {
+            Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs);
+        }
+        if (pkgs.isEmpty()) {
+            throw new IllegalStateException("No connectivity resource package found");
+        }
+
+        return pkgs.get(0).activityInfo.applicationInfo.packageName;
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/async/BufferedFile.java b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
new file mode 100644
index 0000000..bb5736b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
@@ -0,0 +1,292 @@
+/*
+ * 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 java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Buffers inbound and outbound file data within given strict limits.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ *   - When read buffer has more space - asks EventManager to notify on more data
+ *   - When write buffer has more space - asks the user to provide more data
+ *   - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public final class BufferedFile implements AsyncFile.Listener {
+    /**
+     * Receives notifications when new data or output space is available.
+     * @hide
+     */
+    public interface Listener {
+        /** Invoked after the underlying file has been closed. */
+        void onBufferedFileClosed();
+
+        /** Invoked when there's new data in the inbound buffer. */
+        void onBufferedFileInboundData(int readByteCount);
+
+        /** Notifies on data being flushed from output buffer. */
+        void onBufferedFileOutboundSpace();
+
+        /** Notifies on unrecoverable error in file access. */
+        void onBufferedFileIoError(String message);
+    }
+
+    private final Listener mListener;
+    private final EventManager mEventManager;
+    private AsyncFile mFile;
+
+    private final CircularByteBuffer mInboundBuffer;
+    private final AtomicLong mTotalBytesRead = new AtomicLong();
+    private boolean mIsReadingShutdown;
+
+    private final CircularByteBuffer mOutboundBuffer;
+    private final AtomicLong mTotalBytesWritten = new AtomicLong();
+
+    /** Creates BufferedFile based on the given file descriptor. */
+    public static BufferedFile create(
+            EventManager eventManager,
+            FileHandle fileHandle,
+            Listener listener,
+            int inboundBufferSize,
+            int outboundBufferSize) throws IOException {
+        if (fileHandle == null) {
+            throw new NullPointerException();
+        }
+        BufferedFile file = new BufferedFile(
+            eventManager, listener, inboundBufferSize, outboundBufferSize);
+        file.mFile = eventManager.registerFile(fileHandle, file);
+        return file;
+    }
+
+    private BufferedFile(
+            EventManager eventManager,
+            Listener listener,
+            int inboundBufferSize,
+            int outboundBufferSize) {
+        if (eventManager == null || listener == null) {
+            throw new NullPointerException();
+        }
+        mEventManager = eventManager;
+        mListener = listener;
+
+        mInboundBuffer = new CircularByteBuffer(inboundBufferSize);
+        mOutboundBuffer = new CircularByteBuffer(outboundBufferSize);
+    }
+
+    /** Requests this file to be closed. */
+    public void close() {
+        mFile.close();
+    }
+
+    @Override
+    public void onClosed(AsyncFile file) {
+        mListener.onBufferedFileClosed();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // READ PATH
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** Returns buffer that is automatically filled with inbound data. */
+    public ReadableByteBuffer getInboundBuffer() {
+        return mInboundBuffer;
+    }
+
+    public int getInboundBufferFreeSizeForTest() {
+        return mInboundBuffer.freeSize();
+    }
+
+    /** Permanently disables reading of this file, and clears all buffered data. */
+    public void shutdownReading() {
+        mIsReadingShutdown = true;
+        mInboundBuffer.clear();
+        mFile.enableReadEvents(false);
+    }
+
+    /** Returns true after shutdownReading() has been called. */
+    public boolean isReadingShutdown() {
+        return mIsReadingShutdown;
+    }
+
+    /** Starts or resumes async read operations on this file. */
+    public void continueReading() {
+        if (!mIsReadingShutdown && mInboundBuffer.freeSize() > 0) {
+            mFile.enableReadEvents(true);
+        }
+    }
+
+    @Override
+    public void onReadReady(AsyncFile file) {
+        if (mIsReadingShutdown) {
+            return;
+        }
+
+        int readByteCount;
+        try {
+            readByteCount = bufferInputData();
+        } catch (IOException e) {
+            mListener.onBufferedFileIoError("IOException while reading: " + e.toString());
+            return;
+        }
+
+        if (readByteCount > 0) {
+            mListener.onBufferedFileInboundData(readByteCount);
+        }
+
+        continueReading();
+    }
+
+    private int bufferInputData() throws IOException {
+        int totalReadCount = 0;
+        while (true) {
+            final int maxReadCount = mInboundBuffer.getDirectWriteSize();
+            if (maxReadCount == 0) {
+                mFile.enableReadEvents(false);
+                break;
+            }
+
+            final int bufferOffset = mInboundBuffer.getDirectWritePos();
+            final byte[] buffer = mInboundBuffer.getDirectWriteBuffer();
+
+            final int readCount = mFile.read(buffer, bufferOffset, maxReadCount);
+            if (readCount <= 0) {
+                break;
+            }
+
+            mInboundBuffer.accountForDirectWrite(readCount);
+            totalReadCount += readCount;
+        }
+
+        mTotalBytesRead.addAndGet(totalReadCount);
+        return totalReadCount;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // WRITE PATH
+    ///////////////////////////////////////////////////////////////////////////
+
+    /** Returns the number of bytes currently buffered for output. */
+    public int getOutboundBufferSize() {
+        return mOutboundBuffer.size();
+    }
+
+    /** Returns the number of bytes currently available for buffering for output. */
+    public int getOutboundBufferFreeSize() {
+        return mOutboundBuffer.freeSize();
+    }
+
+    /**
+     * Queues the given data for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    public boolean enqueueOutboundData(byte[] data, int pos, int len) {
+        return enqueueOutboundData(data, pos, len, null, 0, 0);
+    }
+
+    /**
+     * Queues data1, then data2 for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    public boolean enqueueOutboundData(
+            byte[] data1, int pos1, int len1,
+            byte[] buffer2, int pos2, int len2) {
+        Assertions.throwsIfOutOfBounds(data1, pos1, len1);
+        Assertions.throwsIfOutOfBounds(buffer2, pos2, len2);
+
+        final int totalLen = len1 + len2;
+
+        if (totalLen > mOutboundBuffer.freeSize()) {
+            flushOutboundBuffer();
+
+            if (totalLen > mOutboundBuffer.freeSize()) {
+                return false;
+            }
+        }
+
+        mOutboundBuffer.writeBytes(data1, pos1, len1);
+
+        if (buffer2 != null) {
+            mOutboundBuffer.writeBytes(buffer2, pos2, len2);
+        }
+
+        flushOutboundBuffer();
+
+        return true;
+    }
+
+    private void flushOutboundBuffer() {
+        try {
+            while (mOutboundBuffer.getDirectReadSize() > 0) {
+                final int maxReadSize = mOutboundBuffer.getDirectReadSize();
+                final int writeCount = mFile.write(
+                    mOutboundBuffer.getDirectReadBuffer(),
+                    mOutboundBuffer.getDirectReadPos(),
+                    maxReadSize);
+
+                if (writeCount == 0) {
+                    mFile.enableWriteEvents(true);
+                    break;
+                }
+
+                if (writeCount > maxReadSize) {
+                    throw new IllegalArgumentException(
+                        "Write count " + writeCount + " above max " + maxReadSize);
+                }
+
+                mOutboundBuffer.accountForDirectRead(writeCount);
+            }
+        } catch (IOException e) {
+            scheduleOnIoError("IOException while writing: " + e.toString());
+        }
+    }
+
+    private void scheduleOnIoError(String message) {
+        mEventManager.execute(() -> {
+            mListener.onBufferedFileIoError(message);
+        });
+    }
+
+    @Override
+    public void onWriteReady(AsyncFile file) {
+        mFile.enableWriteEvents(false);
+        flushOutboundBuffer();
+        mListener.onBufferedFileOutboundSpace();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("file={");
+        sb.append(mFile);
+        sb.append("}");
+        if (mIsReadingShutdown) {
+            sb.append(", readingShutdown");
+        }
+        sb.append("}, inboundBuffer={");
+        sb.append(mInboundBuffer);
+        sb.append("}, outboundBuffer={");
+        sb.append(mOutboundBuffer);
+        sb.append("}, totalBytesRead=");
+        sb.append(mTotalBytesRead);
+        sb.append(", totalBytesWritten=");
+        sb.append(mTotalBytesWritten);
+        return sb.toString();
+    }
+}
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/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 5a180e7..d462c53 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -19,32 +19,48 @@
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.NETLINK_INET_DIAG;
 
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
 import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
 import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
+import static com.android.net.module.util.netlink.NetlinkUtils.connectSocketToNetlink;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
 import android.net.util.SocketUtils;
+import android.os.Process;
 import android.system.ErrnoException;
 import android.util.Log;
+import android.util.Range;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InterruptedIOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * A NetlinkMessage subclass for netlink inet_diag messages.
@@ -58,8 +74,8 @@
     private static final int TIMEOUT_MS = 500;
 
     /**
-     * Construct an inet_diag_req_v2 message. This method will throw {@code NullPointerException}
-     * if local and remote are not both null or both non-null.
+     * Construct an inet_diag_req_v2 message. This method will throw
+     * {@link IllegalArgumentException} if local and remote are not both null or both non-null.
      */
     public static byte[] inetDiagReqV2(int protocol, InetSocketAddress local,
             InetSocketAddress remote, int family, short flags) {
@@ -68,16 +84,16 @@
     }
 
     /**
-     * Construct an inet_diag_req_v2 message. This method will throw {@code NullPointerException}
-     * if local and remote are not both null or both non-null.
+     * Construct an inet_diag_req_v2 message. This method will throw
+     * {@code IllegalArgumentException} if local and remote are not both null or both non-null.
      *
      * @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
      *                 IPPROTO_UDP, or IPPROTO_UDPLITE.
      * @param local local socket address of the target socket. This will be packed into a
-     *              {@Code StructInetDiagSockId}. Request to diagnose for all sockets if both of
+     *              {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
      *              local or remote address is null.
      * @param remote remote socket address of the target socket. This will be packed into a
-     *              {@Code StructInetDiagSockId}. Request to diagnose for all sockets if both of
+     *              {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
      *              local or remote address is null.
      * @param family the ip family of the request message. This should be set to either AF_INET or
      *               AF_INET6 for IPv4 or IPv6 sockets respectively.
@@ -90,18 +106,47 @@
      */
     public static byte[] inetDiagReqV2(int protocol, @Nullable InetSocketAddress local,
             @Nullable InetSocketAddress remote, int family, short flags, int pad, int idiagExt,
-            int state) throws NullPointerException {
+            int state) throws IllegalArgumentException {
+        // Request for all sockets if no specific socket is requested. Specify the local and remote
+        // socket address information for target request socket.
+        if ((local == null) != (remote == null)) {
+            throw new IllegalArgumentException(
+                    "Local and remote must be both null or both non-null");
+        }
+        final StructInetDiagSockId id = ((local != null && remote != null)
+                ? new StructInetDiagSockId(local, remote) : null);
+        return inetDiagReqV2(protocol, id, family,
+                SOCK_DIAG_BY_FAMILY, flags, pad, idiagExt, state);
+    }
+
+    /**
+     * Construct an inet_diag_req_v2 message.
+     *
+     * @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
+     *                 IPPROTO_UDP, or IPPROTO_UDPLITE.
+     * @param id inet_diag_sockid. See {@link StructInetDiagSockId}
+     * @param family the ip family of the request message. This should be set to either AF_INET or
+     *               AF_INET6 for IPv4 or IPv6 sockets respectively.
+     * @param type message types.
+     * @param flags message flags. See &lt;linux_src&gt;/include/uapi/linux/netlink.h.
+     * @param pad for raw socket protocol specification.
+     * @param idiagExt a set of flags defining what kind of extended information to report.
+     * @param state a bit mask that defines a filter of socket states.
+     * @return bytes array representation of the message
+     */
+    public static byte[] inetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family,
+            short type, short flags, int pad, int idiagExt, int state) {
         final byte[] bytes = new byte[StructNlMsgHdr.STRUCT_SIZE + StructInetDiagReqV2.STRUCT_SIZE];
         final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
         byteBuffer.order(ByteOrder.nativeOrder());
 
         final StructNlMsgHdr nlMsgHdr = new StructNlMsgHdr();
         nlMsgHdr.nlmsg_len = bytes.length;
-        nlMsgHdr.nlmsg_type = SOCK_DIAG_BY_FAMILY;
+        nlMsgHdr.nlmsg_type = type;
         nlMsgHdr.nlmsg_flags = flags;
         nlMsgHdr.pack(byteBuffer);
         final StructInetDiagReqV2 inetDiagReqV2 =
-                new StructInetDiagReqV2(protocol, local, remote, family, pad, idiagExt, state);
+                new StructInetDiagReqV2(protocol, id, family, pad, idiagExt, state);
 
         inetDiagReqV2.pack(byteBuffer);
         return bytes;
@@ -109,7 +154,8 @@
 
     public StructInetDiagMsg inetDiagMsg;
 
-    private InetDiagMessage(@NonNull StructNlMsgHdr header) {
+    @VisibleForTesting
+    public InetDiagMessage(@NonNull StructNlMsgHdr header) {
         super(header);
         inetDiagMsg = new StructInetDiagMsg();
     }
@@ -128,6 +174,13 @@
         return msg;
     }
 
+    private static void closeSocketQuietly(final FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException ignored) {
+        }
+    }
+
     private static int lookupUidByFamily(int protocol, InetSocketAddress local,
                                          InetSocketAddress remote, int family, short flags,
                                          FileDescriptor fd)
@@ -218,13 +271,7 @@
                 | InterruptedIOException e) {
             Log.e(TAG, e.toString());
         } finally {
-            if (fd != null) {
-                try {
-                    SocketUtils.closeSocket(fd);
-                } catch (IOException e) {
-                    Log.e(TAG, e.toString());
-                }
-            }
+            closeSocketQuietly(fd);
         }
         return uid;
     }
@@ -240,7 +287,185 @@
                 (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP) /* flag */,
                 0 /* pad */,
                 1 << NetlinkConstants.INET_DIAG_MEMINFO /* idiagExt */,
-                NetlinkUtils.TCP_MONITOR_STATE_FILTER);
+                TCP_ALIVE_STATE_FILTER);
+    }
+
+    private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
+            InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+        final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
+                proto,
+                diagMsg.inetDiagMsg.id,
+                diagMsg.inetDiagMsg.idiag_family,
+                SOCK_DESTROY,
+                (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
+                0 /* pad */,
+                0 /* idiagExt */,
+                1 << diagMsg.inetDiagMsg.idiag_state
+        );
+        NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
+        NetlinkUtils.receiveNetlinkAck(fd);
+    }
+
+    private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
+            throws InterruptedIOException, ErrnoException {
+        final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+                proto,
+                null /* id */,
+                family,
+                SOCK_DIAG_BY_FAMILY,
+                (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP),
+                0 /* pad */,
+                0 /* idiagExt */,
+                states);
+        NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
+    }
+
+    private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+            FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
+            throws InterruptedIOException, ErrnoException {
+        int destroyedSockets = 0;
+
+        while (true) {
+            final ByteBuffer buf = NetlinkUtils.recvMessage(
+                    dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+            while (buf.remaining() > 0) {
+                final int position = buf.position();
+                final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
+                if (nlMsg == null) {
+                    // Move to the position where parse started for error log.
+                    buf.position(position);
+                    Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+                    break;
+                }
+
+                if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+                    return destroyedSockets;
+                }
+
+                if (!(nlMsg instanceof InetDiagMessage)) {
+                    Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+                    continue;
+                }
+
+                final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+                if (filter.test(diagMsg)) {
+                    try {
+                        sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+                        destroyedSockets++;
+                    } catch (InterruptedIOException | ErrnoException e) {
+                        if (!(e instanceof ErrnoException
+                                && ((ErrnoException) e).errno == ENOENT)) {
+                            Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns whether the InetDiagMessage is for adb socket or not
+     */
+    @VisibleForTesting
+    public static boolean isAdbSocket(final InetDiagMessage msg) {
+        // This is inaccurate since adb could run with ROOT_UID or other services can run with
+        // SHELL_UID. But this check covers most cases and enough.
+        // Note that getting service.adb.tcp.port system property is prohibited by sepolicy
+        // TODO: skip the socket only if there is a listen socket owned by SHELL_UID with the same
+        // source port as this socket
+        return msg.inetDiagMsg.idiag_uid == Process.SHELL_UID;
+    }
+
+    /**
+     * Returns whether the range contains the uid in the InetDiagMessage or not
+     */
+    @VisibleForTesting
+    public static boolean containsUid(InetDiagMessage msg, Set<Range<Integer>> ranges) {
+        for (final Range<Integer> range: ranges) {
+            if (range.contains(msg.inetDiagMsg.idiag_uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isLoopbackAddress(InetAddress addr) {
+        if (addr.isLoopbackAddress()) return true;
+        if (!(addr instanceof Inet6Address)) return false;
+
+        // Following check is for v4-mapped v6 address. StructInetDiagSockId contains v4-mapped v6
+        // address as Inet6Address, See StructInetDiagSockId#parse
+        final byte[] addrBytes = addr.getAddress();
+        for (int i = 0; i < 10; i++) {
+            if (addrBytes[i] != 0) return false;
+        }
+        return addrBytes[10] == (byte) 0xff
+                && addrBytes[11] == (byte) 0xff
+                && addrBytes[12] == 127;
+    }
+
+    /**
+     * Returns whether the socket address in the InetDiagMessage is loopback or not
+     */
+    @VisibleForTesting
+    public static boolean isLoopback(InetDiagMessage msg) {
+        final InetAddress srcAddr = msg.inetDiagMsg.id.locSocketAddress.getAddress();
+        final InetAddress dstAddr = msg.inetDiagMsg.id.remSocketAddress.getAddress();
+        return isLoopbackAddress(srcAddr)
+                || isLoopbackAddress(dstAddr)
+                || srcAddr.equals(dstAddr);
+    }
+
+    private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
+            throws ErrnoException, SocketException, InterruptedIOException {
+        FileDescriptor dumpFd = null;
+        FileDescriptor destroyFd = null;
+
+        try {
+            dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
+            destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
+            connectSocketToNetlink(dumpFd);
+            connectSocketToNetlink(destroyFd);
+
+            for (int family : List.of(AF_INET, AF_INET6)) {
+                try {
+                    sendNetlinkDumpRequest(dumpFd, proto, states, family);
+                } catch (InterruptedIOException | ErrnoException e) {
+                    Log.e(TAG, "Failed to send netlink dump request: " + e);
+                    continue;
+                }
+                final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+                        dumpFd, destroyFd, proto, filter);
+                Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+                        + ", proto=" + stringForProtocol(proto)
+                        + ", family=" + stringForAddressFamily(family)
+                        + ", states=" + states);
+            }
+        } finally {
+            closeSocketQuietly(dumpFd);
+            closeSocketQuietly(destroyFd);
+        }
+    }
+
+    /**
+     * Close tcp sockets that match the following condition
+     *  1. TCP status is one of TCP_ESTABLISHED, TCP_SYN_SENT, and TCP_SYN_RECV
+     *  2. Owner uid of socket is not in the exemptUids
+     *  3. Owner uid of socket is in the ranges
+     *  4. Socket is not loopback
+     *  5. Socket is not adb socket
+     *
+     * @param ranges target uid ranges
+     * @param exemptUids uids to skip close socket
+     */
+    public static void destroyLiveTcpSockets(Set<Range<Integer>> ranges, Set<Integer> exemptUids)
+            throws SocketException, InterruptedIOException, ErrnoException {
+        destroySockets(IPPROTO_TCP, TCP_ALIVE_STATE_FILTER,
+                (diagMsg) -> !exemptUids.contains(diagMsg.inetDiagMsg.idiag_uid)
+                        && containsUid(diagMsg, ranges)
+                        && !isLoopback(diagMsg)
+                        && !isAdbSocket(diagMsg));
     }
 
     @Override
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index c44a5b4..44c51d8 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -141,6 +141,7 @@
 
     /* see include/uapi/linux/sock_diag.h */
     public static final short SOCK_DIAG_BY_FAMILY = 20;
+    public static final short SOCK_DESTROY = 21;
 
     // Netlink groups.
     public static final int RTMGRP_LINK = 1;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index ae16cf8..308ea24 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -55,7 +56,7 @@
     private static final int TCP_SYN_SENT = 2;
     private static final int TCP_SYN_RECV = 3;
 
-    public static final int TCP_MONITOR_STATE_FILTER =
+    public static final int TCP_ALIVE_STATE_FILTER =
             (1 << TCP_ESTABLISHED) | (1 << TCP_SYN_SENT) | (1 << TCP_SYN_RECV);
 
     public static final int UNKNOWN_MARK = 0xffffffff;
@@ -81,6 +82,54 @@
     }
 
     /**
+     * Parse netlink error message
+     *
+     * @param bytes byteBuffer to parse netlink error message
+     * @return NetlinkErrorMessage if bytes contains valid NetlinkErrorMessage, else {@code null}
+     */
+    @Nullable
+    private static NetlinkErrorMessage parseNetlinkErrorMessage(ByteBuffer bytes) {
+        final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes);
+        if (nlmsghdr == null || nlmsghdr.nlmsg_type != NetlinkConstants.NLMSG_ERROR) {
+            return null;
+        }
+        return NetlinkErrorMessage.parse(nlmsghdr, bytes);
+    }
+
+    /**
+     * Receive netlink ack message and check error
+     *
+     * @param fd fd to read netlink message
+     */
+    public static void receiveNetlinkAck(final FileDescriptor fd)
+            throws InterruptedIOException, ErrnoException {
+        final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+        // recvMessage() guaranteed to not return null if it did not throw.
+        final NetlinkErrorMessage response = parseNetlinkErrorMessage(bytes);
+        if (response != null && response.getNlMsgError() != null) {
+            final int errno = response.getNlMsgError().error;
+            if (errno != 0) {
+                // TODO: consider ignoring EINVAL (-22), which appears to be
+                // normal when probing a neighbor for which the kernel does
+                // not already have / no longer has a link layer address.
+                Log.e(TAG, "receiveNetlinkAck, errmsg=" + response.toString());
+                // Note: convert kernel errnos (negative) into userspace errnos (positive).
+                throw new ErrnoException(response.toString(), Math.abs(errno));
+            }
+        } else {
+            final String errmsg;
+            if (response == null) {
+                bytes.position(0);
+                errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
+            } else {
+                errmsg = response.toString();
+            }
+            Log.e(TAG, "receiveNetlinkAck, errmsg=" + errmsg);
+            throw new ErrnoException(errmsg, EPROTO);
+        }
+    }
+
+    /**
      * Send one netlink message to kernel via netlink socket.
      *
      * @param nlProto netlink protocol type.
@@ -93,31 +142,7 @@
         try {
             connectSocketToNetlink(fd);
             sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT_MS);
-            final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-            // recvMessage() guaranteed to not return null if it did not throw.
-            final NetlinkMessage response = NetlinkMessage.parse(bytes, nlProto);
-            if (response != null && response instanceof NetlinkErrorMessage
-                    && (((NetlinkErrorMessage) response).getNlMsgError() != null)) {
-                final int errno = ((NetlinkErrorMessage) response).getNlMsgError().error;
-                if (errno != 0) {
-                    // TODO: consider ignoring EINVAL (-22), which appears to be
-                    // normal when probing a neighbor for which the kernel does
-                    // not already have / no longer has a link layer address.
-                    Log.e(TAG, errPrefix + ", errmsg=" + response.toString());
-                    // Note: convert kernel errnos (negative) into userspace errnos (positive).
-                    throw new ErrnoException(response.toString(), Math.abs(errno));
-                }
-            } else {
-                final String errmsg;
-                if (response == null) {
-                    bytes.position(0);
-                    errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
-                } else {
-                    errmsg = response.toString();
-                }
-                Log.e(TAG, errPrefix + ", errmsg=" + errmsg);
-                throw new ErrnoException(errmsg, EPROTO);
-            }
+            receiveNetlinkAck(fd);
         } catch (InterruptedIOException e) {
             Log.e(TAG, errPrefix, e);
             throw new ErrnoException(errPrefix, ETIMEDOUT, e);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
index c07cec0..2829b92 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util.netlink;
 
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
 import android.system.OsConstants;
@@ -58,12 +59,20 @@
     @Nullable
     private StructIfacacheInfo mIfacacheInfo;
 
-    private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
+    @VisibleForTesting
+    public RtNetlinkAddressMessage(@NonNull final StructNlMsgHdr header,
+            @NonNull final StructIfaddrMsg ifaddrMsg,
+            @NonNull final InetAddress ipAddress,
+            @Nullable final StructIfacacheInfo structIfacacheInfo,
+            int flags) {
         super(header);
-        mIfaddrmsg = null;
-        mIpAddress = null;
-        mIfacacheInfo = null;
-        mFlags = 0;
+        mIfaddrmsg = ifaddrMsg;
+        mIpAddress = ipAddress;
+        mIfacacheInfo = structIfacacheInfo;
+        mFlags = flags;
+    }
+    private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
+        this(header, null, null, null, 0);
     }
 
     public int getFlags() {
@@ -160,7 +169,7 @@
 
         final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
         nlmsghdr.nlmsg_type = NetlinkConstants.RTM_NEWADDR;
-        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_REPLACE | NLM_F_ACK;
         nlmsghdr.nlmsg_seq = seqNo;
 
         final RtNetlinkAddressMessage msg = new RtNetlinkAddressMessage(nlmsghdr);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
index 9196feb..2802726 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.Field;
@@ -49,7 +50,8 @@
     @Field(order = 4, type = Type.S32)
     public final int index;
 
-    StructIfaddrMsg(short family, short prefixLen, short flags, short scope, int index) {
+    @VisibleForTesting
+    public StructIfaddrMsg(short family, short prefixLen, short flags, short scope, int index) {
         this.family = family;
         this.prefixLen = prefixLen;
         this.flags = flags;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
index ea018cf..cbd895d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
@@ -43,12 +43,22 @@
  */
 public class StructInetDiagMsg {
     public static final int STRUCT_SIZE = 4 + StructInetDiagSockId.STRUCT_SIZE + 20;
-    private static final int IDIAG_SOCK_ID_OFFSET = StructNlMsgHdr.STRUCT_SIZE + 4;
-    private static final int IDIAG_UID_OFFSET = StructNlMsgHdr.STRUCT_SIZE + 4
-            + StructInetDiagSockId.STRUCT_SIZE + 12;
-    public int idiag_uid;
+    public short idiag_family;
+    public short idiag_state;
+    public short idiag_timer;
+    public short idiag_retrans;
     @NonNull
     public StructInetDiagSockId id;
+    public long idiag_expires;
+    public long idiag_rqueue;
+    public long idiag_wqueue;
+    // Use int for uid since other code use int for uid and uid fits to int
+    public int idiag_uid;
+    public long idiag_inode;
+
+    private static short unsignedByte(byte b) {
+        return (short) (b & 0xFF);
+    }
 
     /**
      * Parse inet diag netlink message from buffer.
@@ -59,21 +69,35 @@
             return null;
         }
         StructInetDiagMsg struct = new StructInetDiagMsg();
-        final byte family = byteBuffer.get();
-        byteBuffer.position(IDIAG_SOCK_ID_OFFSET);
-        struct.id = StructInetDiagSockId.parse(byteBuffer, family);
+        struct.idiag_family = unsignedByte(byteBuffer.get());
+        struct.idiag_state = unsignedByte(byteBuffer.get());
+        struct.idiag_timer = unsignedByte(byteBuffer.get());
+        struct.idiag_retrans = unsignedByte(byteBuffer.get());
+        struct.id = StructInetDiagSockId.parse(byteBuffer, struct.idiag_family);
         if (struct.id == null) {
             return null;
         }
-        struct.idiag_uid = byteBuffer.getInt(IDIAG_UID_OFFSET);
+        struct.idiag_expires = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_rqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_wqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+        struct.idiag_uid = byteBuffer.getInt();
+        struct.idiag_inode = Integer.toUnsignedLong(byteBuffer.getInt());
         return struct;
     }
 
     @Override
     public String toString() {
         return "StructInetDiagMsg{ "
-                + "idiag_uid{" + idiag_uid + "}, "
+                + "idiag_family{" + idiag_family + "}, "
+                + "idiag_state{" + idiag_state + "}, "
+                + "idiag_timer{" + idiag_timer + "}, "
+                + "idiag_retrans{" + idiag_retrans + "}, "
                 + "id{" + id + "}, "
+                + "idiag_expires{" + idiag_expires + "}, "
+                + "idiag_rqueue{" + idiag_rqueue + "}, "
+                + "idiag_wqueue{" + idiag_wqueue + "}, "
+                + "idiag_uid{" + idiag_uid + "}, "
+                + "idiag_inode{" + idiag_inode + "}, "
                 + "}";
     }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
index 6eef865..3b47008 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
@@ -18,7 +18,6 @@
 
 import androidx.annotation.Nullable;
 
-import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 
 /**
@@ -48,23 +47,11 @@
     private final int mState;
     public static final int INET_DIAG_REQ_V2_ALL_STATES = (int) 0xffffffff;
 
-    public StructInetDiagReqV2(int protocol, InetSocketAddress local, InetSocketAddress remote,
-            int family) {
-        this(protocol, local, remote, family, 0 /* pad */, 0 /* extension */,
-                INET_DIAG_REQ_V2_ALL_STATES);
-    }
-
-    public StructInetDiagReqV2(int protocol, @Nullable InetSocketAddress local,
-            @Nullable InetSocketAddress remote, int family, int pad, int extension, int state)
-            throws NullPointerException {
+    public StructInetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family, int pad,
+            int extension, int state) {
         mSdiagFamily = (byte) family;
         mSdiagProtocol = (byte) protocol;
-        // Request for all sockets if no specific socket is requested. Specify the local and remote
-        // socket address information for target request socket.
-        if ((local == null) != (remote == null)) {
-            throw new NullPointerException("Local and remote must be both null or both non-null");
-        }
-        mId = ((local != null && remote != null) ? new StructInetDiagSockId(local, remote) : null);
+        mId = id;
         mPad = (byte) pad;
         mIdiagExt = (byte) extension;
         mState = state;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
index 648a020..dd85934 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
@@ -29,6 +29,7 @@
 import androidx.annotation.Nullable;
 
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
@@ -80,7 +81,7 @@
      * Parse inet diag socket id from buffer.
      */
     @Nullable
-    public static StructInetDiagSockId parse(final ByteBuffer byteBuffer, final byte family) {
+    public static StructInetDiagSockId parse(final ByteBuffer byteBuffer, final short family) {
         if (byteBuffer.remaining() < STRUCT_SIZE) {
             return null;
         }
@@ -89,43 +90,53 @@
         final int srcPort = Short.toUnsignedInt(byteBuffer.getShort());
         final int dstPort = Short.toUnsignedInt(byteBuffer.getShort());
 
-        final byte[] srcAddrByte;
-        final byte[] dstAddrByte;
+        final InetAddress srcAddr;
+        final InetAddress dstAddr;
         if (family == AF_INET) {
-            srcAddrByte = new byte[IPV4_ADDR_LEN];
-            dstAddrByte = new byte[IPV4_ADDR_LEN];
+            final byte[] srcAddrByte = new byte[IPV4_ADDR_LEN];
+            final byte[] dstAddrByte = new byte[IPV4_ADDR_LEN];
             byteBuffer.get(srcAddrByte);
             // Address always uses IPV6_ADDR_LEN in the buffer. So if the address is IPv4, position
             // needs to be advanced to the next field.
             byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
             byteBuffer.get(dstAddrByte);
             byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
+            try {
+                srcAddr = Inet4Address.getByAddress(srcAddrByte);
+                dstAddr = Inet4Address.getByAddress(dstAddrByte);
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Failed to parse address: " + e);
+                return null;
+            }
         } else if (family == AF_INET6) {
-            srcAddrByte = new byte[IPV6_ADDR_LEN];
-            dstAddrByte = new byte[IPV6_ADDR_LEN];
+            final byte[] srcAddrByte = new byte[IPV6_ADDR_LEN];
+            final byte[] dstAddrByte = new byte[IPV6_ADDR_LEN];
             byteBuffer.get(srcAddrByte);
             byteBuffer.get(dstAddrByte);
+            try {
+                // Using Inet6Address.getByAddress to be consistent with idiag_family field since
+                // InetAddress.getByAddress returns Inet4Address if the address is v4-mapped v6
+                // address.
+                srcAddr = Inet6Address.getByAddress(
+                        null /* host */, srcAddrByte, -1 /* scope_id */);
+                dstAddr = Inet6Address.getByAddress(
+                        null /* host */, dstAddrByte, -1 /* scope_id */);
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Failed to parse address: " + e);
+                return null;
+            }
         } else {
             Log.wtf(TAG, "Invalid address family: " + family);
             return null;
         }
 
-        final InetSocketAddress srcAddr;
-        final InetSocketAddress dstAddr;
-        try {
-            srcAddr = new InetSocketAddress(InetAddress.getByAddress(srcAddrByte), srcPort);
-            dstAddr = new InetSocketAddress(InetAddress.getByAddress(dstAddrByte), dstPort);
-        } catch (UnknownHostException e) {
-            // Should not happen. UnknownHostException is thrown only if addr byte array is of
-            // illegal length.
-            Log.wtf(TAG, "Failed to parse address: " + e);
-            return null;
-        }
+        final InetSocketAddress srcSocketAddr = new InetSocketAddress(srcAddr, srcPort);
+        final InetSocketAddress dstSocketAddr = new InetSocketAddress(dstAddr, dstPort);
 
         byteBuffer.order(ByteOrder.nativeOrder());
         final int ifIndex = byteBuffer.getInt();
         final long cookie = byteBuffer.getLong();
-        return new StructInetDiagSockId(srcAddr, dstAddr, ifIndex, cookie);
+        return new StructInetDiagSockId(srcSocketAddr, dstSocketAddr, ifIndex, cookie);
     }
 
     /**
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..dbf79dc
--- /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 final short code;
+    @Field(order = 1, type = Type.S16)
+    public final short length;
+    @Field(order = 2, type = Type.U32)
+    public final long id;
+    @Field(order = 3, type = Type.U32)
+    public final long t1;
+    @Field(order = 4, type = Type.U32)
+    public final 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..cd974e6
--- /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 final short code;
+    @Field(order = 1, type = Type.S16)
+    public final short length;
+    @Field(order = 2, type = Type.U32)
+    public final long preferred;
+    @Field(order = 3, type = Type.U32)
+    public final long valid;
+    @Field(order = 4, type = Type.U8)
+    public final short prefixLen;
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final 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/device/com/android/net/module/util/wear/NetPacketHelpers.java b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
new file mode 100644
index 0000000..341c44b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
@@ -0,0 +1,41 @@
+/*
+ * 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.wear;
+
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+/**
+ * Implements utilities for decoding parts of TCP/UDP/IP headers.
+ *
+ * @hide
+ */
+final class NetPacketHelpers {
+    static void encodeNetworkUnsignedInt16(int value, byte[] dst, final int dstPos) {
+        dst[dstPos] = (byte) ((value >> 8) & 0xFF);
+        dst[dstPos + 1] = (byte) (value & 0xFF);
+    }
+
+    static int decodeNetworkUnsignedInt16(byte[] data, final int pos) {
+        return ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
+    }
+
+    static int decodeNetworkUnsignedInt16(ReadableByteBuffer data, final int pos) {
+        return ((data.peek(pos) & 0xFF) << 8) | (data.peek(pos + 1) & 0xFF);
+    }
+
+    private NetPacketHelpers() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/PacketFile.java b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
new file mode 100644
index 0000000..7f5ed78
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
@@ -0,0 +1,85 @@
+/*
+ * 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.wear;
+
+/**
+ * Defines bidirectional file where all transmissions are made as complete packets.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ *   - When read buffer has more space - asks EventManager to notify on more data
+ *   - When write buffer has more space - asks the user to provide more data
+ *   - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public interface PacketFile {
+    /** @hide */
+    public enum ErrorCode {
+        UNEXPECTED_ERROR,
+        IO_ERROR,
+        INBOUND_PACKET_TOO_LARGE,
+        OUTBOUND_PACKET_TOO_LARGE,
+    }
+
+    /**
+     * Receives notifications when new data or output space is available.
+     *
+     * @hide
+     */
+    public interface Listener {
+        /**
+         * Handles the initial part of the stream, which on some systems provides lower-level
+         * configuration data.
+         *
+         * Returns the number of bytes consumed, or zero if the preamble has been fully read.
+         */
+        int onPreambleData(byte[] data, int pos, int len);
+
+        /** Handles one extracted packet. */
+        void onInboundPacket(byte[] data, int pos, int len);
+
+        /** Notifies on new data being added to the buffer. */
+        void onInboundBuffered(int newByteCount, int totalBufferedSize);
+
+        /** Notifies on data being flushed from output buffer. */
+        void onOutboundPacketSpace();
+
+        /** Notifies on unrecoverable error in the packet processing. */
+        void onPacketFileError(ErrorCode error, String message);
+    }
+
+    /** Requests this file to be closed. */
+    void close();
+
+    /** Permanently disables reading of this file, and clears all buffered data. */
+    void shutdownReading();
+
+    /** Starts or resumes async read operations on this file. */
+    void continueReading();
+
+    /** Returns the number of bytes currently buffered as input. */
+    int getInboundBufferSize();
+
+    /** Returns the number of bytes currently available for buffering for output. */
+    int getOutboundFreeSize();
+
+    /**
+     * Queues the given data for output.
+     * Throws runtime exception if there is not enough space.
+     */
+    boolean enqueueOutboundPacket(byte[] data, int pos, int len);
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
new file mode 100644
index 0000000..52dbee4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
@@ -0,0 +1,221 @@
+/*
+ * 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.wear;
+
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.Assertions;
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+import java.io.IOException;
+
+/**
+ * Implements PacketFile based on a streaming file descriptor.
+ *
+ * Packets are delineated using network-order 2-byte length indicators.
+ *
+ * @hide
+ */
+public final class StreamingPacketFile implements PacketFile, BufferedFile.Listener {
+    private static final int HEADER_SIZE = 2;
+
+    private final EventManager mEventManager;
+    private final Listener mListener;
+    private final BufferedFile mFile;
+    private final int mMaxPacketSize;
+    private final ReadableByteBuffer mInboundBuffer;
+    private boolean mIsInPreamble = true;
+
+    private final byte[] mTempPacketReadBuffer;
+    private final byte[] mTempHeaderWriteBuffer;
+
+    public StreamingPacketFile(
+            EventManager eventManager,
+            FileHandle fileHandle,
+            Listener listener,
+            int maxPacketSize,
+            int maxBufferedInboundPackets,
+            int maxBufferedOutboundPackets) throws IOException {
+        if (eventManager == null || fileHandle == null || listener == null) {
+            throw new NullPointerException();
+        }
+
+        mEventManager = eventManager;
+        mListener = listener;
+        mMaxPacketSize = maxPacketSize;
+
+        final int maxTotalLength = HEADER_SIZE + maxPacketSize;
+
+        mFile = BufferedFile.create(eventManager, fileHandle, this,
+            maxTotalLength * maxBufferedInboundPackets,
+            maxTotalLength * maxBufferedOutboundPackets);
+        mInboundBuffer = mFile.getInboundBuffer();
+
+        mTempPacketReadBuffer = new byte[maxTotalLength];
+        mTempHeaderWriteBuffer = new byte[HEADER_SIZE];
+    }
+
+    @Override
+    public void close() {
+        mFile.close();
+    }
+
+    public BufferedFile getUnderlyingFileForTest() {
+        return mFile;
+    }
+
+    @Override
+    public void shutdownReading() {
+        mFile.shutdownReading();
+    }
+
+    @Override
+    public void continueReading() {
+        mFile.continueReading();
+    }
+
+    @Override
+    public int getInboundBufferSize() {
+        return mInboundBuffer.size();
+    }
+
+    @Override
+    public void onBufferedFileClosed() {
+    }
+
+    @Override
+    public void onBufferedFileInboundData(int readByteCount) {
+        if (mFile.isReadingShutdown()) {
+            return;
+        }
+
+        if (readByteCount > 0) {
+            mListener.onInboundBuffered(readByteCount, mInboundBuffer.size());
+        }
+
+        if (extractOnePacket() && !mFile.isReadingShutdown()) {
+            // There could be more packets already buffered, continue parsing next
+            // packet even before another read event comes
+            mEventManager.execute(() -> {
+                onBufferedFileInboundData(0);
+            });
+        } else {
+            continueReading();
+        }
+    }
+
+    private boolean extractOnePacket() {
+        while (mIsInPreamble) {
+            final int directReadSize = Math.min(
+                mInboundBuffer.getDirectReadSize(), mTempPacketReadBuffer.length);
+            if (directReadSize == 0) {
+                return false;
+            }
+
+            // Copy for safety, so higher-level callback cannot modify the data.
+            System.arraycopy(mInboundBuffer.getDirectReadBuffer(),
+                mInboundBuffer.getDirectReadPos(), mTempPacketReadBuffer, 0, directReadSize);
+
+            final int preambleConsumedBytes = mListener.onPreambleData(
+                mTempPacketReadBuffer, 0, directReadSize);
+            if (mFile.isReadingShutdown()) {
+                return false;  // The callback has called shutdownReading().
+            }
+
+            if (preambleConsumedBytes == 0) {
+                mIsInPreamble = false;
+                break;
+            }
+
+            mInboundBuffer.accountForDirectRead(preambleConsumedBytes);
+        }
+
+        final int bufferedSize = mInboundBuffer.size();
+        if (bufferedSize < HEADER_SIZE) {
+            return false;
+        }
+
+        final int dataLength = NetPacketHelpers.decodeNetworkUnsignedInt16(mInboundBuffer, 0);
+        if (dataLength > mMaxPacketSize) {
+            mListener.onPacketFileError(
+                PacketFile.ErrorCode.INBOUND_PACKET_TOO_LARGE,
+                "Inbound packet length: " + dataLength);
+            return false;
+        }
+
+        final int totalLength = HEADER_SIZE + dataLength;
+        if (bufferedSize < totalLength) {
+            return false;
+        }
+
+        mInboundBuffer.readBytes(mTempPacketReadBuffer, 0, totalLength);
+
+        mListener.onInboundPacket(mTempPacketReadBuffer, HEADER_SIZE, dataLength);
+        return true;
+    }
+
+    @Override
+    public int getOutboundFreeSize() {
+        final int freeSize = mFile.getOutboundBufferFreeSize();
+        return (freeSize > HEADER_SIZE ? freeSize - HEADER_SIZE : 0);
+    }
+
+    @Override
+    public boolean enqueueOutboundPacket(byte[] buffer, int pos, int len) {
+        Assertions.throwsIfOutOfBounds(buffer, pos, len);
+
+        if (len == 0) {
+            return true;
+        }
+
+        if (len > mMaxPacketSize) {
+            mListener.onPacketFileError(
+                PacketFile.ErrorCode.OUTBOUND_PACKET_TOO_LARGE,
+                "Outbound packet length: " + len);
+            return false;
+        }
+
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len, mTempHeaderWriteBuffer, 0);
+
+        mFile.enqueueOutboundData(
+            mTempHeaderWriteBuffer, 0, mTempHeaderWriteBuffer.length,
+            buffer, pos, len);
+        return true;
+    }
+
+    @Override
+    public void onBufferedFileOutboundSpace() {
+        mListener.onOutboundPacketSpace();
+    }
+
+    @Override
+    public void onBufferedFileIoError(String message) {
+        mListener.onPacketFileError(PacketFile.ErrorCode.IO_ERROR, message);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("maxPacket=");
+        sb.append(mMaxPacketSize);
+        sb.append(", file={");
+        sb.append(mFile);
+        sb.append("}");
+        return sb.toString();
+    }
+}
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/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f08880c..39e7ce9 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -101,7 +101,6 @@
     /**
      * @return The index of the first element that matches the predicate, or -1 if none.
      */
-    @Nullable
     public static <T> int indexOf(@NonNull final Collection<T> elem,
             @NonNull final Predicate<? super T> predicate) {
         int idx = 0;
diff --git a/staticlibs/device/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
similarity index 99%
rename from staticlibs/device/com/android/net/module/util/HexDump.java
rename to staticlibs/framework/com/android/net/module/util/HexDump.java
index a27c0a3..a22c258 100644
--- a/staticlibs/device/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -20,6 +20,8 @@
 
 /**
  * Hex utility functions.
+ *
+ * @hide
  */
 public class HexDump {
     private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
index 31d0729..40fc59f 100644
--- a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -16,7 +16,10 @@
 
 package com.android.net.module.util;
 
+import android.annotation.NonNull;
 import android.os.Parcel;
+import android.util.Log;
+
 
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -28,6 +31,7 @@
  */
 public class InetAddressUtils {
 
+    private static final String TAG = InetAddressUtils.class.getSimpleName();
     private static final int INET6_ADDR_LENGTH = 16;
 
     /**
@@ -71,5 +75,23 @@
         }
     }
 
+    /**
+     * Create a Inet6Address with scope id if it is a link local address. Otherwise, returns the
+     * original address.
+     */
+    public static Inet6Address withScopeId(@NonNull final Inet6Address addr, int scopeid) {
+        if (!addr.isLinkLocalAddress()) {
+            return addr;
+        }
+        try {
+            return Inet6Address.getByAddress(null /* host */, addr.getAddress(),
+                    scopeid);
+        } catch (UnknownHostException impossible) {
+            Log.wtf(TAG, "Cannot construct scoped Inet6Address with Inet6Address.getAddress("
+                    + addr.getHostAddress() + "): ", impossible);
+            return null;
+        }
+    }
+
     private InetAddressUtils() {}
 }
diff --git a/staticlibs/framework/com/android/net/module/util/IpUtils.java b/staticlibs/framework/com/android/net/module/util/IpUtils.java
index 569733e..18d96f3 100644
--- a/staticlibs/framework/com/android/net/module/util/IpUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/IpUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.net.module.util;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -42,8 +44,9 @@
      * payload) or ICMP checksum on the specified portion of a ByteBuffer.  The seed
      * allows the checksum to commence with a specified value.
      */
-    private static int checksum(ByteBuffer buf, int seed, int start, int end) {
-        int sum = seed;
+    @VisibleForTesting
+    public static int checksum(ByteBuffer buf, int seed, int start, int end) {
+        int sum = seed + 0xFFFF;  // to make things work with empty / zero-filled buffer
         final int bufPosition = buf.position();
 
         // set position of original ByteBuffer, so that the ShortBuffer
@@ -69,13 +72,12 @@
                 b += 256;
             }
 
-            sum += b * 256;
+            sum += b * 256;  // assumes bytebuffer is network order (ie. big endian)
         }
 
-        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);
-        sum = ((sum + ((sum >> 16) & 0xFFFF)) & 0xFFFF);
-        int negated = ~sum;
-        return intAbs((short) negated);
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);  // max sum is 0x1FFFE
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);  // max sum is 0xFFFF
+        return sum ^ 0xFFFF;  // u16 bitwise negation
     }
 
     private static int pseudoChecksumIPv4(
diff --git a/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
index 1565f2b..e271f64 100644
--- a/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
@@ -146,6 +146,24 @@
                 right != null ? right.getLinkAddresses() : null);
     }
 
+    /**
+     * Compares {@code left} {@code LinkProperties} allLinkAddresses against the {@code right}.
+     *
+     * @param left A LinkProperties or null
+     * @param right A LinkProperties or null
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @see LinkProperties#getAllLinkAddresses()
+     */
+    public static boolean isIdenticalAllLinkAddresses(@Nullable LinkProperties left,
+            @Nullable LinkProperties right) {
+        if (left == right) return true;
+        if (left == null || right == null) return false;
+        final List<LinkAddress> leftAddresses = left.getAllLinkAddresses();
+        final List<LinkAddress> rightAddresses = right.getAllLinkAddresses();
+        if (leftAddresses.size() != rightAddresses.size()) return false;
+        return leftAddresses.containsAll(rightAddresses);
+    }
+
    /**
      * Compares {@code left} {@code LinkProperties} interface addresses against the {@code right}.
      *
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 7d4ae30..ba0cab8 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -135,7 +135,7 @@
      *     - https://tools.ietf.org/html/rfc792
      */
     public static final int ICMP_CHECKSUM_OFFSET = 2;
-
+    public static final int ICMP_HEADER_LEN = 8;
     /**
      * ICMPv6 constants.
      *
@@ -210,17 +210,23 @@
      */
     public static final int INFINITE_LEASE = 0xffffffff;
     public static final int DHCP4_CLIENT_PORT = 68;
+    // The maximum length of a DHCP packet that can be constructed.
+    public static final int DHCP_MAX_LENGTH = 1500;
+    public static final int DHCP_MAX_OPTION_LEN = 255;
 
     /**
      * DHCPv6 constants.
      *
      * 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/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index be5b0cd..8315b8f 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -54,6 +54,20 @@
     }
 
     /**
+     * Return true if the context has one of give permission that is allowed
+     * for a particular process and user ID running in the system.
+     */
+    public static boolean checkAnyPermissionOf(@NonNull Context context,
+            int pid, int uid, @NonNull String... permissions) {
+        for (String permission : permissions) {
+            if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Enforce permission check on the context that should have one of given permission.
      */
     public static void enforceAnyPermissionOf(@NonNull Context context,
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/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
new file mode 100644
index 0000000..9b38dee
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+// Accept the full packet
+#define BPF_ACCEPT BPF_STMT(BPF_RET | BPF_K, 0xFFFFFFFF)
+
+// Reject the packet
+#define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
+
+// *TWO* instructions: compare and if equal jump over the reject statement
+#define BPF2_REJECT_IF_NOT_EQUAL(v) \
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
+	BPF_REJECT
+
+// 8-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_U8(ofs) \
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE16(ofs) \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE32(ofs) \
+	BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// 8-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_U8(ofs) \
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE16(ofs) \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE32(ofs) \
+	BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+#define field_sizeof(struct_type,field) sizeof(((struct_type *)0)->field)
+
+// 8-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_U8(field) \
+	BPF_LOAD_NET_RELATIVE_U8(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 1, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// Big/Network Endian 16-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE16(field) \
+	BPF_LOAD_NET_RELATIVE_BE16(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 2, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// Big/Network Endian 32-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE32(field) \
+	BPF_LOAD_NET_RELATIVE_BE32(({ \
+	  _Static_assert(field_sizeof(struct iphdr, field) == 4, "field of wrong size"); \
+	  offsetof(iphdr, field); \
+	}))
+
+// 8-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_U8(field) \
+	BPF_LOAD_NET_RELATIVE_U8(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 1, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
+
+// Big/Network Endian 16-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE16(field) \
+	BPF_LOAD_NET_RELATIVE_BE16(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 2, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
+
+// Big/Network Endian 32-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE32(field) \
+	BPF_LOAD_NET_RELATIVE_BE32(({ \
+	  _Static_assert(field_sizeof(struct ipv6hdr, field) == 4, "field of wrong size"); \
+	  offsetof(ipv6hdr, field); \
+	}))
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/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
index e2cb676..99c7a91 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
@@ -33,17 +33,26 @@
 namespace android {
 namespace bpf {
 
+// See kernel's net/core/sock_diag.c __sock_gen_cookie()
+// the implementation of which guarantees 0 will never be returned,
+// primarily because 0 is used to mean not yet initialized,
+// and socket cookies are only assigned on first fetch.
 constexpr const uint64_t NONEXISTENT_COOKIE = 0;
 
 static inline uint64_t getSocketCookie(int sockFd) {
     uint64_t sock_cookie;
     socklen_t cookie_len = sizeof(sock_cookie);
-    int res = getsockopt(sockFd, SOL_SOCKET, SO_COOKIE, &sock_cookie, &cookie_len);
-    if (res < 0) {
-        res = -errno;
-        ALOGE("Failed to get socket cookie: %s\n", strerror(errno));
-        errno = -res;
-        // 0 is an invalid cookie. See sock_gen_cookie.
+    if (getsockopt(sockFd, SOL_SOCKET, SO_COOKIE, &sock_cookie, &cookie_len)) {
+        // Failure is almost certainly either EBADF or ENOTSOCK
+        const int err = errno;
+        ALOGE("Failed to get socket cookie: %s\n", strerror(err));
+        errno = err;
+        return NONEXISTENT_COOKIE;
+    }
+    if (cookie_len != sizeof(sock_cookie)) {
+        // This probably cannot actually happen, but...
+        ALOGE("Failed to get socket cookie: len %d != 8\n", cookie_len);
+        errno = 523; // EBADCOOKIE: kernel internal, seems reasonable enough...
         return NONEXISTENT_COOKIE;
     }
     return sock_cookie;
@@ -54,21 +63,22 @@
     // 4.9 kernels. The kernel code of socket release on pf_key socket will
     // explicitly call synchronize_rcu() which is exactly what we need.
     //
-    // Linux 4.14/4.19/5.4/5.10/5.15 (and 5.18) still have this same behaviour.
+    // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
     // see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
-    int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+    // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+    const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
 
     if (pfSocket < 0) {
-        int ret = -errno;
-        ALOGE("create PF_KEY socket failed: %s", strerror(errno));
-        return ret;
+        const int err = errno;
+        ALOGE("create PF_KEY socket failed: %s", strerror(err));
+        return -err;
     }
 
     // When closing socket, synchronize_rcu() gets called in sock_release().
     if (close(pfSocket)) {
-        int ret = -errno;
-        ALOGE("failed to close the PF_KEY socket: %s", strerror(errno));
-        return ret;
+        const int err = errno;
+        ALOGE("failed to close the PF_KEY socket: %s", strerror(err));
+        return -err;
     }
     return 0;
 }
@@ -79,10 +89,8 @@
             .rlim_cur = 1073741824,  // 1 GiB
             .rlim_max = 1073741824,  // 1 GiB
     };
-    int res = setrlimit(RLIMIT_MEMLOCK, &limit);
-    if (res) {
-        ALOGE("Failed to set the default MEMLOCK rlimit: %s", strerror(errno));
-    }
+    const int res = setrlimit(RLIMIT_MEMLOCK, &limit);
+    if (res) ALOGE("Failed to set the default MEMLOCK rlimit: %s", strerror(errno));
     return res;
 }
 
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index ed1ee51..70c0f89 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -27,13 +27,6 @@
 // Android S / 12 (api level 31) - added 'tethering' mainline eBPF support
 #define BPFLOADER_S_VERSION 2u
 
-// Android T / 13 Beta 3 (api level 33) - added support for 'netd_shared'
-#define BPFLOADER_T_BETA3_VERSION 13u
-
-// v0.18 added support for shared and pindir, but still ignores selinux_content
-// v0.19 added support for selinux_content along with the required selinux changes
-// and should be available starting with Android T Beta 4
-//
 // Android T / 13 (api level 33) - support for shared/selinux_context/pindir
 #define BPFLOADER_T_VERSION 19u
 
@@ -43,6 +36,9 @@
 // Bpfloader v0.33+ supports {map,prog}.ignore_on_{eng,user,userdebug}
 #define BPFLOADER_IGNORED_ON_VERSION 33u
 
+// Android U / 14 (api level 34) - various new program types added
+#define BPFLOADER_U_VERSION 37u
+
 /* 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.
@@ -225,7 +221,7 @@
 
 #ifdef THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
 #define BPF_MAP_ASSERT_OK(type, entries, mode)
-#elif BPFLOADER_MIN_VER >= BPFLOADER_T_BETA3_VERSION
+#elif BPFLOADER_MIN_VER >= BPFLOADER_T_VERSION
 #define BPF_MAP_ASSERT_OK(type, entries, mode)
 #else
 #define BPF_MAP_ASSERT_OK(type, entries, mode) \
@@ -235,11 +231,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, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,                 \
-                      false, false, false);                                                      \
+                      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);                                           \
                                                                                                  \
@@ -271,9 +268,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, \
@@ -302,6 +301,8 @@
 
 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 int (*bpf_probe_read_user)(void* dst, int size, const void* unsafe_ptr) = (void*)BPF_FUNC_probe_read_user;
+static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
 static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
 static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
 static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
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 d286eba..65540e0 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,9 +48,8 @@
 #define DEFAULT_SIZEOF_BPF_MAP_DEF 32       // v0.0 struct: enum (uint sized) + 7 uint
 #define DEFAULT_SIZEOF_BPF_PROG_DEF 20      // v0.0 struct: 4 uint + bool + 3 byte alignment pad
 
-// By default, unless otherwise specified, allow the use of features only supported by v0.28,
-// which first added working support for map uid != root
-#define COMPILE_FOR_BPFLOADER_VERSION 28u
+// By default, unless otherwise specified, allow the use of features only supported by v0.37.
+#define COMPILE_FOR_BPFLOADER_VERSION 37u
 
 /*
  * The bpf_{map,prog}_def structures are compiled for different architectures.
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/ip_checksum/checksum.c b/staticlibs/native/ip_checksum/checksum.c
index 04217a7..5641fad 100644
--- a/staticlibs/native/ip_checksum/checksum.c
+++ b/staticlibs/native/ip_checksum/checksum.c
@@ -32,20 +32,16 @@
  *   len         - length of data
  */
 uint32_t ip_checksum_add(uint32_t current, const void* data, int len) {
-    uint32_t checksum = current;
-    int left = len;
     const uint16_t* data_16 = data;
 
-    while (left > 1) {
-        checksum += *data_16;
+    while (len >= 2) {
+        current += *data_16;
         data_16++;
-        left -= 2;
+        len -= 2;
     }
-    if (left) {
-        checksum += *(uint8_t*)data_16;
-    }
+    if (len) current += *(uint8_t*)data_16;  // assumes little endian!
 
-    return checksum;
+    return current;
 }
 
 /* function: ip_checksum_fold
@@ -54,9 +50,8 @@
  *   returns: the folded checksum in network byte order
  */
 uint16_t ip_checksum_fold(uint32_t temp_sum) {
-    while (temp_sum > 0xffff) {
-        temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
-    }
+    temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
+    temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
     return temp_sum;
 }
 
@@ -75,12 +70,7 @@
  *   len  - length of data
  */
 uint16_t ip_checksum(const void* data, int len) {
-    // TODO: consider starting from 0xffff so the checksum of a buffer entirely consisting of zeros
-    // is correctly calculated as 0.
-    uint32_t temp_sum;
-
-    temp_sum = ip_checksum_add(0, data, len);
-    return ip_checksum_finish(temp_sum);
+    return ip_checksum_finish(ip_checksum_add(0xFFFF, data, len));
 }
 
 /* function: ipv6_pseudo_header_checksum
@@ -92,7 +82,6 @@
 uint32_t ipv6_pseudo_header_checksum(const struct ip6_hdr* ip6, uint32_t len, uint8_t protocol) {
     uint32_t checksum_len = htonl(len);
     uint32_t checksum_next = htonl(protocol);
-
     uint32_t current = 0;
 
     current = ip_checksum_add(current, &(ip6->ip6_src), sizeof(struct in6_addr));
@@ -109,11 +98,8 @@
  *   len     - the transport length (transport header + payload)
  */
 uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len) {
-    uint16_t temp_protocol, temp_length;
-
-    temp_protocol = htons(ip->protocol);
-    temp_length = htons(len);
-
+    uint16_t temp_protocol = htons(ip->protocol);
+    uint16_t temp_length = htons(len);
     uint32_t current = 0;
 
     current = ip_checksum_add(current, &(ip->saddr), sizeof(uint32_t));
@@ -135,7 +121,7 @@
     // Algorithm suggested in RFC 1624.
     // http://tools.ietf.org/html/rfc1624#section-3
     checksum = ~checksum;
-    uint16_t folded_sum = ip_checksum_fold(checksum + new_hdr_sum);
+    uint16_t folded_sum = ip_checksum_fold(new_hdr_sum + checksum);
     uint16_t folded_old = ip_checksum_fold(old_hdr_sum);
     if (folded_sum > folded_old) {
         return ~(folded_sum - folded_old);
diff --git a/staticlibs/native/ip_checksum/checksum.h b/staticlibs/native/ip_checksum/checksum.h
index 868217c..87393c9 100644
--- a/staticlibs/native/ip_checksum/checksum.h
+++ b/staticlibs/native/ip_checksum/checksum.h
@@ -12,11 +12,8 @@
  * 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.
- *
- * checksum.h - checksum functions
  */
-#ifndef __CHECKSUM_H__
-#define __CHECKSUM_H__
+#pragma once
 
 #include <netinet/ip.h>
 #include <netinet/ip6.h>
@@ -30,5 +27,3 @@
 uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len);
 
 uint16_t ip_checksum_adjust(uint16_t checksum, uint32_t old_hdr_sum, uint32_t new_hdr_sum);
-
-#endif /* __CHECKSUM_H__ */
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 6e223bd..40371e6 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -21,6 +21,7 @@
         "net-utils-device-common-async",
         "net-utils-device-common-bpf",
         "net-utils-device-common-ip",
+        "net-utils-device-common-wear",
     ],
     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/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 302388d..b75939b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -16,25 +16,30 @@
 
 package com.android.net.module.util;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.net.module.util.DeviceConfigUtils.FIXED_PACKAGE_VERSION;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
 
@@ -49,6 +54,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 
+import java.util.Arrays;
+
 
 /**
  * Tests for DeviceConfigUtils.
@@ -66,14 +73,23 @@
     private static final int TEST_MIN_FLAG_VALUE = 100;
     private static final long TEST_PACKAGE_VERSION = 290000000;
     private static final String TEST_PACKAGE_NAME = "test.package.name";
-    private static final String TETHERING_AOSP_PACKAGE_NAME = "com.android.networkstack.tethering";
-    private static final String TEST_APEX_NAME = "test.apex.name";
+    // The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
+    // used for its mount point in /apex. APEX packages are actually APKs with a different
+    // file extension, so they have an AndroidManifest: the APEX package name is the package name in
+    // that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
+    // (module) name, different package names are typically used to identify the organization that
+    // built and signed the APEX modules.
+    private static final String TEST_APEX_NAME = "com.android.tethering";
+    private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
+    private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+    private static final String TEST_CONNRES_PACKAGE_NAME =
+            "com.prefix.android.connectivity.resources";
+    private final PackageInfo mPackageInfo = new PackageInfo();
+    private final PackageInfo mApexPackageInfo = new PackageInfo();
     private MockitoSession mSession;
 
     @Mock private Context mContext;
     @Mock private PackageManager mPm;
-    @Mock private ModuleInfo mMi;
-    @Mock private PackageInfo mPi;
     @Mock private Resources mResources;
 
     @Before
@@ -81,15 +97,26 @@
         MockitoAnnotations.initMocks(this);
         mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
 
-        final PackageInfo pi = new PackageInfo();
-        pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+        mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
 
         doReturn(mPm).when(mContext).getPackageManager();
         doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
-        doReturn(mMi).when(mPm).getModuleInfo(eq(TEST_APEX_NAME), anyInt());
-        doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName();
-        doReturn(pi).when(mPm).getPackageInfo(anyString(), anyInt());
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+        doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+
         doReturn(mResources).when(mContext).getResources();
+
+        final ResolveInfo ri = new ResolveInfo();
+        ri.activityInfo = new ActivityInfo();
+        ri.activityInfo.applicationInfo = new ApplicationInfo();
+        ri.activityInfo.applicationInfo.packageName = TEST_CONNRES_PACKAGE_NAME;
+        ri.activityInfo.applicationInfo.sourceDir =
+                "/apex/com.android.tethering/priv-app/ServiceConnectivityResources@version";
+        doReturn(Arrays.asList(ri)).when(mPm).queryIntentActivities(argThat(
+                intent -> intent.getAction().equals(DeviceConfigUtils.RESOURCES_APK_INTENT)),
+                eq(MATCH_SYSTEM_ONLY));
     }
 
     @After
@@ -223,39 +250,32 @@
                 TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
-        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
-        doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
-        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
-        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
     }
 
-
     @Test
-    public void testFeatureIsEnabledUsingFixedVersion() throws Exception {
-        doReturn(TETHERING_AOSP_PACKAGE_NAME).when(mContext).getPackageName();
-        doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
-
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION)).when(() -> DeviceConfig.getProperty(
+    public void testFeatureIsEnabledOnGo() throws Exception {
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
+                eq(TEST_APEX_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+        doReturn("0").when(() -> DeviceConfig.getProperty(
                 eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
 
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION + 1)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
 
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION - 1)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
     }
 
     @Test
-    public void testFeatureIsEnabledCaching() throws Exception {
+    public void testFeatureIsEnabledCaching_APK() throws Exception {
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
                 eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
@@ -267,14 +287,20 @@
         verify(mContext, times(1)).getPackageManager();
         verify(mContext, times(1)).getPackageName();
         verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+    }
 
+    @Test
+    public void testFeatureIsEnabledCaching_APEX() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
 
-        // Module info is only queried once
-        verify(mPm, times(1)).getModuleInfo(anyString(), anyInt());
+        // Package info is only queried once
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+        verify(mContext, never()).getPackageName();
     }
 
     @Test
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
index 2736c53..bb2b933 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -18,6 +18,10 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
 import android.os.Parcel;
 
 import androidx.test.filters.SmallTest;
@@ -67,4 +71,25 @@
         assertEquals(ipv6, out);
         assertEquals(42, out.getScopeId());
     }
+
+    @Test
+    public void testWithScopeId() {
+        final int scopeId = 999;
+
+        final String globalAddrStr = "2401:fa00:49c:484:dc41:e6ff:fefd:f180";
+        final Inet6Address globalAddr = (Inet6Address) InetAddresses
+                .parseNumericAddress(globalAddrStr);
+        final Inet6Address updatedGlobalAddr = InetAddressUtils.withScopeId(globalAddr, scopeId);
+        assertFalse(updatedGlobalAddr.isLinkLocalAddress());
+        assertEquals(globalAddrStr, updatedGlobalAddr.getHostAddress());
+        assertEquals(0, updatedGlobalAddr.getScopeId());
+
+        final String localAddrStr = "fe80::4735:9628:d038:2087";
+        final Inet6Address localAddr = (Inet6Address) InetAddresses
+                .parseNumericAddress(localAddrStr);
+        final Inet6Address updatedLocalAddr = InetAddressUtils.withScopeId(localAddr, scopeId);
+        assertTrue(updatedLocalAddr.isLinkLocalAddress());
+        assertEquals(localAddrStr + "%" + scopeId, updatedLocalAddr.getHostAddress());
+        assertEquals(scopeId, updatedLocalAddr.getScopeId());
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
index 20555b3..d57023c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
@@ -74,6 +74,20 @@
     // print JavaPacketDefinition(str(packet))
 
     @Test
+    public void testEmptyAndZeroBufferChecksum() throws Exception {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[] { (byte) 0x00, (byte) 0x00, });
+        // the following should *not* return 0xFFFF
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 0));
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 1));
+        assertEquals(0, IpUtils.checksum(packet, 0, 0, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0, 1, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 0));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 1));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 2));
+        assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 1, 2));
+    }
+
+    @Test
     public void testIpv6TcpChecksum() throws Exception {
         // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80) /
         //           scapy.TCP(sport=12345, dport=7,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
index 09f0490..80ab618 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
@@ -94,6 +94,9 @@
         assertTrue(LinkPropertiesUtils.isIdenticalAddresses(source, target));
         assertTrue(LinkPropertiesUtils.isIdenticalAddresses(target, source));
 
+        assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
         assertTrue(LinkPropertiesUtils.isIdenticalDnses(source, target));
         assertTrue(LinkPropertiesUtils.isIdenticalDnses(target, source));
 
@@ -116,12 +119,17 @@
         assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
         assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
 
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
         // Currently, target contains V4_LINKADDR, V6_LINKADDR and testLinkAddr.
         // Compare addresses.size() equals but contains different address.
         target.removeLinkAddress(V4_LINKADDR);
         assertEquals(source.getAddresses().size(), target.getAddresses().size());
         assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
         assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
         // Restore link address
         target.addLinkAddress(V4_LINKADDR);
         target.removeLinkAddress(testLinkAddr);
@@ -169,6 +177,13 @@
         target.setHttpProxy(null);
         assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(source, target));
         assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(target, source));
+
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("v4-" + target.getInterfaceName());
+        stacked.addLinkAddress(testLinkAddr);
+        target.addStackedLink(stacked);
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+        assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
     }
 
     private <T> void compareResult(List<T> oldItems, List<T> newItems, List<T> expectRemoved,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
new file mode 100644
index 0000000..11a74f2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BufferedFileTest {
+    @Mock EventManager mockEventManager;
+    @Mock BufferedFile.Listener mockFileListener;
+    @Mock AsyncFile mockAsyncFile;
+    @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+    }
+
+    @Test
+    public void onClosed() throws Exception {
+        final int inboundBufferSize = 1024;
+        final int outboundBufferSize = 768;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        file.onClosed(mockAsyncFile);
+
+        verify(mockFileListener).onBufferedFileClosed();
+    }
+
+    @Test
+    public void continueReadingAndClose() throws Exception {
+        final int inboundBufferSize = 1024;
+        final int outboundBufferSize = 768;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        assertEquals(inboundBufferSize, file.getInboundBufferFreeSizeForTest());
+        assertEquals(outboundBufferSize, file.getOutboundBufferFreeSize());
+
+        file.continueReading();
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        file.close();
+        verify(mockAsyncFile).close();
+    }
+
+    @Test
+    public void enqueueOutboundData() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        final int totalLen = data1.length + data2.length;
+
+        when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, null, 0, 0));
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        assertEquals(data1.length, file.getOutboundBufferSize());
+
+        checkAndResetMocks();
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundData(data2, 0, data2.length, null, 0, 0));
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+    }
+
+    @Test
+    public void enqueueOutboundData_combined() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        final int totalLen = data1.length + data2.length;
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+    }
+
+    @Test
+    public void enableWriteEvents() throws Exception {
+        final int inboundBufferSize = 10;
+        final int outboundBufferSize = 250;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        final byte[] data3 = new byte[103];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+        data3[0] = (byte) 3;
+
+        assertEquals(0, file.getOutboundBufferSize());
+
+        // Write first 2 buffers, but fail to flush them, causing async write request.
+        final int data1And2Len = data1.length + data2.length;
+        when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data1And2Len, file.getOutboundBufferSize());
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        // Try to write 3rd buffers, which won't fit, then fail to flush.
+        when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+        assertFalse(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data1And2Len, file.getOutboundBufferSize());
+        verify(mockAsyncFile, times(2)).enableWriteEvents(true);
+
+        checkAndResetMocks();
+
+        // Simulate writeability event, and successfully flush.
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(arrayCaptor.capture(),
+                posCaptor.capture(), lenCaptor.capture())).thenReturn(data1And2Len);
+        file.onWriteReady(mockAsyncFile);
+        verify(mockAsyncFile).enableWriteEvents(false);
+        verify(mockFileListener).onBufferedFileOutboundSpace();
+        assertEquals(0, file.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(data1And2Len, lenCaptor.getValue().intValue());
+        assertEquals(data1[0], arrayCaptor.getValue()[0]);
+        assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+
+        checkAndResetMocks();
+
+        // Now write, but fail to flush the third buffer.
+        when(mockAsyncFile.write(arrayCaptor.capture(),
+                posCaptor.capture(), lenCaptor.capture())).thenReturn(0);
+        assertTrue(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+        verify(mockAsyncFile).enableWriteEvents(true);
+        assertEquals(data3.length, file.getOutboundBufferSize());
+
+        assertEquals(data1And2Len, posCaptor.getValue().intValue());
+        assertEquals(outboundBufferSize - data1And2Len, lenCaptor.getValue().intValue());
+        assertEquals(data3[0], arrayCaptor.getValue()[data1And2Len]);
+    }
+
+    @Test
+    public void read() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2);
+        final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(true);
+        verify(mockFileListener).onBufferedFileInboundData(eq(data1.length + data2.length));
+
+        assertEquals(0, file.getOutboundBufferSize());
+        assertEquals(data1.length + data2.length, inboundBuffer.size());
+        assertEquals((byte) 1, inboundBuffer.peek(0));
+        assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+    }
+
+    @Test
+    public void enableReadEvents() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data1 = new byte[101];
+        final byte[] data2 = new byte[102];
+        final byte[] data3 = new byte[103];
+        data1[0] = (byte) 1;
+        data2[0] = (byte) 2;
+        data3[0] = (byte) 3;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2, data3);
+        final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(false);
+        verify(mockFileListener).onBufferedFileInboundData(eq(inboundBufferSize));
+
+        assertEquals(0, file.getOutboundBufferSize());
+        assertEquals(inboundBufferSize, inboundBuffer.size());
+        assertEquals((byte) 1, inboundBuffer.peek(0));
+        assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+        assertEquals((byte) 3, inboundBuffer.peek(data1.length + data2.length));
+
+        checkAndResetMocks();
+
+        // Cannot enable read events since the buffer is full.
+        file.continueReading();
+
+        checkAndResetMocks();
+
+        final byte[] tmp = new byte[inboundBufferSize];
+        inboundBuffer.readBytes(tmp, 0, data1.length);
+        assertEquals(inboundBufferSize - data1.length, inboundBuffer.size());
+
+        file.continueReading();
+
+        inboundBuffer.readBytes(tmp, 0, data2.length);
+        assertEquals(inboundBufferSize - data1.length - data2.length, inboundBuffer.size());
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile, times(2)).enableReadEvents(true);
+        verify(mockFileListener).onBufferedFileInboundData(
+            eq(data1.length + data2.length + data3.length - inboundBufferSize));
+
+        assertEquals(data3.length, inboundBuffer.size());
+        assertEquals((byte) 3, inboundBuffer.peek(0));
+    }
+
+    @Test
+    public void shutdownReading() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        file.shutdownReading();
+        file.onReadReady(mockAsyncFile);
+
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(data.length, dataAnswer.getRemainingSize());
+    }
+
+    @Test
+    public void shutdownReading_inCallback() throws Exception {
+        final int inboundBufferSize = 250;
+        final int outboundBufferSize = 10;
+
+        final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        doAnswer(new Answer() {
+            @Override public Object answer(InvocationOnMock invocation) {
+                file.shutdownReading();
+                return null;
+            }}).when(mockFileListener).onBufferedFileInboundData(anyInt());
+
+        file.onReadReady(mockAsyncFile);
+
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, file.getInboundBuffer().size());
+        assertEquals(0, dataAnswer.getRemainingSize());
+    }
+
+    private void checkAndResetMocks() {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+            mockParcelFileDescriptor));
+        reset(mockFileListener, mockAsyncFile, mockEventManager);
+    }
+
+    private BufferedFile createFile(
+            int inboundBufferSize, int outboundBufferSize) throws Exception {
+        when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+        return BufferedFile.create(
+            mockEventManager,
+            FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+            mockFileListener,
+            inboundBufferSize,
+            outboundBufferSize);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index c7e2a4d..65e99f8 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -16,12 +16,16 @@
 
 package com.android.net.module.util.netlink;
 
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SHELL_UID;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.NETLINK_INET_DIAG;
 
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
@@ -32,6 +36,8 @@
 import static org.junit.Assert.fail;
 
 import android.net.InetAddresses;
+import android.util.ArraySet;
+import android.util.Range;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -41,14 +47,33 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.List;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class InetDiagSocketTest {
+    // ::FFFF:192.0.2.1
+    private static final byte[] SRC_V4_MAPPED_V6_ADDRESS_BYTES = {
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+    };
+    // ::FFFF:192.0.2.2
+    private static final byte[] DST_V4_MAPPED_V6_ADDRESS_BYTES = {
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+            (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+            (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+    };
+
     // Hexadecimal representation of InetDiagReqV2 request.
     private static final String INET_DIAG_REQ_V2_UDP_INET4_HEX =
             // struct nlmsghdr
@@ -205,14 +230,14 @@
             msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, remote, AF_INET6,
                     NLM_F_REQUEST);
             fail("Both remote and local should be null, expected UnknownHostException");
-        } catch (NullPointerException e) {
+        } catch (IllegalArgumentException e) {
         }
 
         try {
             msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, null, AF_INET6,
                     NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
             fail("Both remote and local should be null, expected UnknownHostException");
-        } catch (NullPointerException e) {
+        } catch (IllegalArgumentException e) {
         }
 
         msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, null, AF_INET6,
@@ -221,19 +246,115 @@
         assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES, msgExt);
     }
 
-    // Hexadecimal representation of InetDiagReqV2 request.
-    private static final String INET_DIAG_MSG_HEX =
+    // Hexadecimal representation of InetDiagReqV2 request with v4-mapped v6 address
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0100" +         // flags = NLM_F_REQUEST
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states
+            // inet_diag_sockid
+            "a817" +     // idiag_sport = 43031
+            "960f" +     // idiag_dport = 38415
+            "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+            "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+            "00000000" +     // idiag_if
+            "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6V4Mapped() throws Exception {
+        final Inet6Address srcAddr = Inet6Address.getByAddress(
+                null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final Inet6Address dstAddr = Inet6Address.getByAddress(
+                null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final byte[] msg = InetDiagMessage.inetDiagReqV2(
+                IPPROTO_TCP,
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415),
+                AF_INET6,
+                NLM_F_REQUEST);
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES, msg);
+    }
+
+    // Hexadecimal representation of InetDiagReqV2 request with SOCK_DESTROY
+    private static final String INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX =
+            // struct nlmsghdr
+            "48000000" +     // length = 72
+            "1500" +         // type = SOCK_DESTROY
+            "0500" +         // flags = NLM_F_REQUEST | NLM_F_ACK
+            "00000000" +     // seqno
+            "00000000" +     // pid (0 == kernel)
+            // struct inet_diag_req_v2
+            "0a" +           // family = AF_INET6
+            "06" +           // protcol = IPPROTO_TCP
+            "00" +           // idiag_ext
+            "00" +           // pad
+            "ffffffff" +     // idiag_states = TCP_ALL_STATES
+            // inet_diag_sockid
+            "a817" +     // idiag_sport = 43031
+            "960f" +     // idiag_dport = 38415
+            "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
+            "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
+            "07000000" + // idiag_if = 7
+            "5800000000000000"; // idiag_cookie = 88
+
+    private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES =
+            HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX.toCharArray(), false);
+
+    @Test
+    public void testInetDiagReqV2TcpInet6Destroy() throws Exception {
+        final StructInetDiagSockId sockId = new StructInetDiagSockId(
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+        final byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, sockId, AF_INET6,
+                SOCK_DESTROY, (short) (NLM_F_REQUEST | NLM_F_ACK), 0 /* pad */, 0 /* idiagExt */,
+                TCP_ALL_STATES);
+
+        assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES, msg);
+    }
+
+    private void assertNlMsgHdr(StructNlMsgHdr hdr, short type, short flags, int seq, int pid) {
+        assertNotNull(hdr);
+        assertEquals(type, hdr.nlmsg_type);
+        assertEquals(flags, hdr.nlmsg_flags);
+        assertEquals(seq, hdr.nlmsg_seq);
+        assertEquals(pid, hdr.nlmsg_pid);
+    }
+
+    private void assertInetDiagSockId(StructInetDiagSockId sockId,
+            InetSocketAddress locSocketAddress, InetSocketAddress remSocketAddress,
+            int ifIndex, long cookie) {
+        assertEquals(locSocketAddress, sockId.locSocketAddress);
+        assertEquals(remSocketAddress, sockId.remSocketAddress);
+        assertEquals(ifIndex, sockId.ifIndex);
+        assertEquals(cookie, sockId.cookie);
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX1 =
             // struct nlmsghdr
             "58000000" +     // length = 88
             "1400" +         // type = SOCK_DIAG_BY_FAMILY
             "0200" +         // flags = NLM_F_MULTI
             "00000000" +     // seqno
-            "f5220000" +     // pid (0 == kernel)
+            "f5220000" +     // pid
             // struct inet_diag_msg
             "0a" +           // family = AF_INET6
-            "01" +           // idiag_state
-            "00" +           // idiag_timer
-            "00" +           // idiag_retrans
+            "01" +           // idiag_state = 1
+            "02" +           // idiag_timer = 2
+            "ff" +           // idiag_retrans = 255
                 // inet_diag_sockid
                 "a817" +     // idiag_sport = 43031
                 "960f" +     // idiag_dport = 38415
@@ -241,39 +362,331 @@
                 "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
                 "07000000" + // idiag_if = 7
                 "5800000000000000" + // idiag_cookie = 88
-            "00000000" +     // idiag_expires
-            "00000000" +     // idiag_rqueue
-            "00000000" +     // idiag_wqueue
-            "a3270000" +     // idiag_uid
-            "A57E1900";      // idiag_inode
-    private static final byte[] INET_DIAG_MSG_BYTES =
-            HexEncoding.decode(INET_DIAG_MSG_HEX.toCharArray(), false);
+            "04000000" +     // idiag_expires = 4
+            "05000000" +     // idiag_rqueue = 5
+            "06000000" +     // idiag_wqueue = 6
+            "a3270000" +     // idiag_uid = 10147
+            "a57e19f0";      // idiag_inode = 4028202661
 
-    @Test
-    public void testParseInetDiagResponse() throws Exception {
-        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
-        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
-        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
+    private void assertInetDiagMsg1(final NetlinkMessage msg) {
         assertNotNull(msg);
 
         assertTrue(msg instanceof InetDiagMessage);
         final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
-        assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
-        final StructInetDiagSockId sockId = inetDiagMsg.inetDiagMsg.id;
-        assertEquals(43031, sockId.locSocketAddress.getPort());
-        assertEquals(InetAddresses.parseNumericAddress("2001:db8::1"),
-                sockId.locSocketAddress.getAddress());
-        assertEquals(38415, sockId.remSocketAddress.getPort());
-        assertEquals(InetAddresses.parseNumericAddress("2001:db8::2"),
-                sockId.remSocketAddress.getAddress());
-        assertEquals(7, sockId.ifIndex);
-        assertEquals(88, sockId.cookie);
 
-        final StructNlMsgHdr hdr = inetDiagMsg.getHeader();
-        assertNotNull(hdr);
-        assertEquals(NetlinkConstants.SOCK_DIAG_BY_FAMILY, hdr.nlmsg_type);
-        assertEquals(StructNlMsgHdr.NLM_F_MULTI, hdr.nlmsg_flags);
-        assertEquals(0, hdr.nlmsg_seq);
-        assertEquals(8949, hdr.nlmsg_pid);
+        assertNlMsgHdr(inetDiagMsg.getHeader(),
+                NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+                StructNlMsgHdr.NLM_F_MULTI,
+                0    /* seq */,
+                8949 /* pid */);
+
+        assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+        assertEquals(1, inetDiagMsg.inetDiagMsg.idiag_state);
+        assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_timer);
+        assertEquals(255, inetDiagMsg.inetDiagMsg.idiag_retrans);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+        assertEquals(4, inetDiagMsg.inetDiagMsg.idiag_expires);
+        assertEquals(5, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+        assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+        assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
+        assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+    }
+
+    // Hexadecimal representation of InetDiagMessage
+    private static final String INET_DIAG_MSG_HEX2 =
+            // struct nlmsghdr
+            "58000000" +     // length = 88
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0200" +         // flags = NLM_F_MULTI
+            "00000000" +     // seqno
+            "f5220000" +     // pid
+            // struct inet_diag_msg
+            "0a" +           // family = AF_INET6
+            "02" +           // idiag_state = 2
+            "10" +           // idiag_timer = 16
+            "20" +           // idiag_retrans = 32
+                // inet_diag_sockid
+                "a845" +     // idiag_sport = 43077
+                "01bb" +     // idiag_dport = 443
+                "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
+                "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
+                "08000000" + // idiag_if = 8
+                "6300000000000000" + // idiag_cookie = 99
+            "30000000" +     // idiag_expires = 48
+            "40000000" +     // idiag_rqueue = 64
+            "50000000" +     // idiag_wqueue = 80
+            "39300000" +     // idiag_uid = 12345
+            "851a0000";      // idiag_inode = 6789
+
+    private void assertInetDiagMsg2(final NetlinkMessage msg) {
+        assertNotNull(msg);
+
+        assertTrue(msg instanceof InetDiagMessage);
+        final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+
+        assertNlMsgHdr(inetDiagMsg.getHeader(),
+                NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+                StructNlMsgHdr.NLM_F_MULTI,
+                0    /* seq */,
+                8949 /* pid */);
+
+        assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+        assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_state);
+        assertEquals(16, inetDiagMsg.inetDiagMsg.idiag_timer);
+        assertEquals(32, inetDiagMsg.inetDiagMsg.idiag_retrans);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::3"), 43077),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::4"), 443),
+                8  /* ifIndex */,
+                99 /* cookie */);
+        assertEquals(48, inetDiagMsg.inetDiagMsg.idiag_expires);
+        assertEquals(64, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+        assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+        assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
+        assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+    }
+
+    private static final byte[] INET_DIAG_MSG_BYTES =
+            HexEncoding.decode(INET_DIAG_MSG_HEX1.toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponse() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+
+    private static final byte[] INET_DIAG_MSG_BYTES_MULTIPLE =
+            HexEncoding.decode((INET_DIAG_MSG_HEX1 + INET_DIAG_MSG_HEX2).toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponseMultiple() {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES_MULTIPLE);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+        assertInetDiagMsg2(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+    }
+
+    private static final String INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX =
+            "a845" +     // idiag_sport = 43077
+            "01bb" +     // idiag_dport = 443
+            "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+            "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+            "08000000" + // idiag_if = 8
+            "6300000000000000"; // idiag_cookie = 99
+
+    private static final byte[] INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES =
+            HexEncoding.decode(INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+    @Test
+    public void testParseAndPackInetDiagSockIdV4MappedV6() {
+        final ByteBuffer parseByteBuffer = ByteBuffer.wrap(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES);
+        parseByteBuffer.order(ByteOrder.nativeOrder());
+        final StructInetDiagSockId diagSockId =
+                StructInetDiagSockId.parse(parseByteBuffer, (short) AF_INET6);
+        assertNotNull(diagSockId);
+
+        final ByteBuffer packByteBuffer =
+                ByteBuffer.allocate(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES.length);
+        diagSockId.pack(packByteBuffer);
+
+        // Move position to the head since ByteBuffer#equals compares the values from the current
+        // position.
+        parseByteBuffer.position(0);
+        packByteBuffer.position(0);
+        assertEquals(parseByteBuffer, packByteBuffer);
+    }
+
+    // Hexadecimal representation of InetDiagMessage with v4-mapped v6 address
+    private static final String INET_DIAG_MSG_V4_MAPPED_V6_HEX =
+            // struct nlmsghdr
+            "58000000" +     // length = 88
+            "1400" +         // type = SOCK_DIAG_BY_FAMILY
+            "0200" +         // flags = NLM_F_MULTI
+            "00000000" +     // seqno
+            "f5220000" +     // pid
+            // struct inet_diag_msg
+            "0a" +           // family = AF_INET6
+            "01" +           // idiag_state = 1
+            "02" +           // idiag_timer = 2
+            "03" +           // idiag_retrans = 3
+                // inet_diag_sockid
+                "a817" +     // idiag_sport = 43031
+                "960f" +     // idiag_dport = 38415
+                "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+                "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+                "07000000" + // idiag_if = 7
+                "5800000000000000" + // idiag_cookie = 88
+            "04000000" +     // idiag_expires = 4
+            "05000000" +     // idiag_rqueue = 5
+            "06000000" +     // idiag_wqueue = 6
+            "a3270000" +     // idiag_uid = 10147
+            "A57E1900";      // idiag_inode = 1670821
+
+    private static final byte[] INET_DIAG_MSG_V4_MAPPED_V6_BYTES =
+            HexEncoding.decode(INET_DIAG_MSG_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+    @Test
+    public void testParseInetDiagResponseV4MappedV6() throws Exception {
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_V4_MAPPED_V6_BYTES);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
+
+        assertNotNull(msg);
+        assertTrue(msg instanceof InetDiagMessage);
+        final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+        final Inet6Address srcAddr = Inet6Address.getByAddress(
+                null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        final Inet6Address dstAddr = Inet6Address.getByAddress(
+                null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+        assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415),
+                7  /* ifIndex */,
+                88 /* cookie */);
+    }
+
+    private void doTestIsLoopback(InetAddress srcAddr, InetAddress dstAddr, boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+                new InetSocketAddress(srcAddr, 43031),
+                new InetSocketAddress(dstAddr, 38415)
+        );
+
+        assertEquals(expected, InetDiagMessage.isLoopback(inetDiagMsg));
+    }
+
+    @Test
+    public void testIsLoopback() {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("127.0.0.1"),
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("127.7.7.7"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("::1"),
+                InetAddresses.parseNumericAddress("::1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("::1"),
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                true
+        );
+    }
+
+    @Test
+    public void testIsLoopbackSameSrcDstAddress()  {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                true
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                true
+        );
+    }
+
+    @Test
+    public void testIsLoopbackNonLoopbackSocket()  {
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("192.0.2.1"),
+                InetAddresses.parseNumericAddress("192.0.2.2"),
+                false
+        );
+        doTestIsLoopback(
+                InetAddresses.parseNumericAddress("2001:db8::1"),
+                InetAddresses.parseNumericAddress("2001:db8::2"),
+                false
+        );
+    }
+
+    @Test
+    public void testIsLoopbackV4MappedV6() throws UnknownHostException {
+        // ::FFFF:127.1.2.3
+        final byte[] addrLoopbackByte = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0x7f, (byte) 0x01, (byte) 0x02, (byte) 0x03,
+        };
+        // ::FFFF:192.0.2.1
+        final byte[] addrNonLoopbackByte1 = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+        };
+        // ::FFFF:192.0.2.2
+        final byte[] addrNonLoopbackByte2 = {
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+        };
+
+        final Inet6Address addrLoopback = Inet6Address.getByAddress(null, addrLoopbackByte, -1);
+        final Inet6Address addrNonLoopback1 =
+                Inet6Address.getByAddress(null, addrNonLoopbackByte1, -1);
+        final Inet6Address addrNonLoopback2 =
+                Inet6Address.getByAddress(null, addrNonLoopbackByte2, -1);
+
+        doTestIsLoopback(addrLoopback, addrNonLoopback1, true);
+        doTestIsLoopback(addrNonLoopback1, addrNonLoopback2, false);
+        doTestIsLoopback(addrNonLoopback1, addrNonLoopback1, true);
+    }
+
+    private void doTestContainsUid(final int uid, final Set<Range<Integer>> ranges,
+            final boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+        assertEquals(expected, InetDiagMessage.containsUid(inetDiagMsg, ranges));
+    }
+
+    @Test
+    public void testContainsUid() {
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(0, 100))),
+                true /* expected */);
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(77, 77), new Range<>(100, 200))),
+                true /* expected */);
+
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(100, 200))),
+                false /* expected */);
+        doTestContainsUid(77 /* uid */,
+                new ArraySet<>(List.of(new Range<>(0, 76), new Range<>(78, 100))),
+                false /* expected */);
+    }
+
+    private void doTestIsAdbSocket(final int uid, final boolean expected) {
+        final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+        inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+        inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 38417),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415)
+        );
+        assertEquals(expected, InetDiagMessage.isAdbSocket(inetDiagMsg));
+    }
+
+    @Test
+    public void testIsAdbSocket() {
+        final int appUid = 10108;
+        doTestIsAdbSocket(SHELL_UID,  true /* expected */);
+        doTestIsAdbSocket(ROOT_UID, false /* expected */);
+        doTestIsAdbSocket(appUid, false /* expected */);
     }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 7a1639a..6fbfbf9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -30,6 +30,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
 
 import android.content.Context;
 import android.system.ErrnoException;
@@ -51,11 +52,13 @@
 import java.io.FileDescriptor;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class NetlinkUtilsTest {
-    private static final String TAG = "NetlinkUitlsTest";
+    private static final String TAG = "NetlinkUtilsTest";
     private static final int TEST_SEQNO = 5;
     private static final int TEST_TIMEOUT_MS = 500;
 
@@ -82,6 +85,8 @@
 
         // Apps targeting an SDK version > S are not allowed to send RTM_GETNEIGH{TBL} messages
         if (SdkLevel.isAtLeastT() && targetSdk > 31) {
+            var ctxt = new String(Files.readAllBytes(Paths.get("/proc/thread-self/attr/current")));
+            assumeFalse("must not be platform app", ctxt.startsWith("u:r:platform_app:s0:"));
             try {
                 NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
                 fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
index f845eb4..99d96b5 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
@@ -144,7 +144,7 @@
                 // struct nlmsghdr
                 "48000000" +    // length = 72
                 "1400" +        // type = 20 (RTM_NEWADDR)
-                "0500" +        // flags = NLM_F_ACK | NLM_F_REQUEST
+                "0501" +        // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
                 "01000000" +    // seqno = 1
                 "00000000" +    // pid = 0 (send to kernel)
                 // struct IfaddrMsg
@@ -195,7 +195,7 @@
                 // struct nlmsghdr
                 "48000000" +    // length = 72
                 "1400" +        // type = 20 (RTM_NEWADDR)
-                "0500" +        // flags = NLM_F_ACK | NLM_F_REQUEST
+                "0501" +        // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
                 "01000000" +    // seqno = 1
                 "00000000" +    // pid = 0 (send to kernel)
                 // struct IfaddrMsg
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
new file mode 100644
index 0000000..23e7b15
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.wear;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetPacketHelpersTest {
+    @Test
+    public void decodeNetworkUnsignedInt16() {
+        final byte[] data = new byte[4];
+        data[0] = (byte) 0xFF;
+        data[1] = (byte) 1;
+        data[2] = (byte) 2;
+        data[3] = (byte) 0xFF;
+
+        assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(data, 1));
+
+        CircularByteBuffer buffer = new CircularByteBuffer(100);
+        buffer.writeBytes(data, 0, data.length);
+
+        assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(buffer, 1));
+    }
+
+    @Test
+    public void encodeNetworkUnsignedInt16() {
+        final byte[] data = new byte[4];
+        data[0] = (byte) 0xFF;
+        data[3] = (byte) 0xFF;
+        NetPacketHelpers.encodeNetworkUnsignedInt16(0x0102, data, 1);
+
+        assertEquals((byte) 0xFF, data[0]);
+        assertEquals((byte) 1, data[1]);
+        assertEquals((byte) 2, data[2]);
+        assertEquals((byte) 0xFF, data[3]);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
new file mode 100644
index 0000000..1fcca70
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.wear;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.async.AsyncFile;
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.ReadableByteBuffer;
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StreamingPacketFileTest {
+    private static final int MAX_PACKET_SIZE = 100;
+
+    @Mock EventManager mockEventManager;
+    @Mock PacketFile.Listener mockFileListener;
+    @Mock AsyncFile mockAsyncFile;
+    @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+    }
+
+    @Test
+    public void continueReadingAndClose() throws Exception {
+        final int maxBufferedInboundPackets = 3;
+        final int maxBufferedOutboundPackets = 5;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        assertEquals(maxBufferedInboundPackets * (MAX_PACKET_SIZE + 2),
+            bufferedFile.getInboundBufferFreeSizeForTest());
+        assertEquals(maxBufferedOutboundPackets * (MAX_PACKET_SIZE + 2),
+            bufferedFile.getOutboundBufferFreeSize());
+        assertEquals(bufferedFile.getOutboundBufferFreeSize() - 2,
+            file.getOutboundFreeSize());
+
+        file.continueReading();
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        file.close();
+        verify(mockAsyncFile).close();
+    }
+
+    @Test
+    public void enqueueOutboundPacket() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        final byte[] packet1 = new byte[11];
+        final byte[] packet2 = new byte[12];
+        packet1[0] = (byte) 1;
+        packet2[0] = (byte) 2;
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+        when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+        assertTrue(file.enqueueOutboundPacket(packet1, 0, packet1.length));
+        verify(mockAsyncFile).enableWriteEvents(true);
+
+        assertEquals(packet1.length + 2, bufferedFile.getOutboundBufferSize());
+
+        checkAndResetMocks();
+
+        final int totalLen = packet1.length + packet2.length + 4;
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+        when(mockAsyncFile.write(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+        assertTrue(file.enqueueOutboundPacket(packet2, 0, packet2.length));
+
+        assertEquals(0, bufferedFile.getInboundBuffer().size());
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+        assertEquals(0, posCaptor.getValue().intValue());
+        assertEquals(totalLen, lenCaptor.getValue().intValue());
+
+        final byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(packet1.length, NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, 0));
+        assertEquals(packet2.length,
+            NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, packet1.length + 2));
+        assertEquals(packet1[0], capturedData[2]);
+        assertEquals(packet2[0], capturedData[packet1.length + 4]);
+    }
+
+    @Test
+    public void onInboundPacket() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+        final int len1 = 11;
+        final int len2 = 12;
+        final byte[] data = new byte[len1 + len2 + 4];
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, 0);
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len2, data, 11 + 2);
+        data[2] = (byte) 1;
+        data[len1 + 4] = (byte) 2;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(0);
+        bufferedFile.onReadReady(mockAsyncFile);
+        verify(mockAsyncFile).enableReadEvents(true);
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+
+        byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len1, lenCaptor.getValue().intValue());
+        assertEquals((byte) 1, capturedData[2]);
+
+        checkAndResetMocks();
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        file.onBufferedFileInboundData(0);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+
+        capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len2, lenCaptor.getValue().intValue());
+        assertEquals((byte) 2, capturedData[2]);
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+        assertEquals(0, inboundBuffer.size());
+    }
+
+    @Test
+    public void onReadReady_preambleData() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+        final int preambleLen = 23;
+        final int len1 = 11;
+        final byte[] data = new byte[preambleLen + 2 + len1];
+        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, preambleLen);
+        data[preambleLen + 2] = (byte) 1;
+
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(5);
+        when(mockFileListener.onPreambleData(
+            any(), eq(0), eq(data.length - 5))).thenReturn(preambleLen - 5);
+        when(mockFileListener.onPreambleData(
+            any(), eq(0), eq(data.length - preambleLen))).thenReturn(0);
+
+        bufferedFile.onReadReady(mockAsyncFile);
+
+        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockFileListener).onInboundPacket(
+            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+        verify(mockEventManager).execute(any());
+        verify(mockAsyncFile).enableReadEvents(true);
+
+        final byte[] capturedData = arrayCaptor.getValue();
+        assertEquals(2, posCaptor.getValue().intValue());
+        assertEquals(len1, lenCaptor.getValue().intValue());
+        assertEquals((byte) 1, capturedData[2]);
+
+        assertEquals(0, bufferedFile.getOutboundBufferSize());
+        assertEquals(0, inboundBuffer.size());
+    }
+
+    @Test
+    public void shutdownReading() throws Exception {
+        final int maxBufferedInboundPackets = 10;
+        final int maxBufferedOutboundPackets = 20;
+
+        final StreamingPacketFile file =
+            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+        final byte[] data = new byte[100];
+        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+        doAnswer(new Answer() {
+            @Override public Object answer(InvocationOnMock invocation) {
+                file.shutdownReading();
+                return Integer.valueOf(-1);
+            }}).when(mockFileListener).onPreambleData(any(), anyInt(), anyInt());
+
+        bufferedFile.onReadReady(mockAsyncFile);
+
+        verify(mockFileListener).onInboundBuffered(data.length, data.length);
+        verify(mockAsyncFile).enableReadEvents(false);
+
+        assertEquals(0, bufferedFile.getInboundBuffer().size());
+    }
+
+    private void checkAndResetMocks() {
+        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+            mockParcelFileDescriptor));
+        reset(mockFileListener, mockAsyncFile, mockEventManager);
+    }
+
+    private StreamingPacketFile createFile(
+            int maxBufferedInboundPackets, int maxBufferedOutboundPackets) throws Exception {
+        when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+        return new StreamingPacketFile(
+            mockEventManager,
+            FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+            mockFileListener,
+            MAX_PACKET_SIZE,
+            maxBufferedInboundPackets,
+            maxBufferedOutboundPackets);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
index 46a3588..30e0daf 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -19,6 +19,7 @@
 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
@@ -72,5 +73,9 @@
             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 eed31e0..4ed881a 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
@@ -34,6 +34,7 @@
 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.LinkPropertiesChanged
 import kotlin.reflect.KClass
 import kotlin.test.assertEquals
 import kotlin.test.assertFails
@@ -140,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
@@ -179,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
@@ -220,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
         } }
     }
 
@@ -359,18 +366,6 @@
     }
 
     @Test
-    fun testPollOrThrow() {
-        assertFails { mCallback.pollOrThrow(SHORT_TIMEOUT_MS) }
-        TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
-                threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
-            sleep; onAvailable(133)    | pollOrThrow(2) = Available(133) time 1..4
-                                       | pollOrThrow(1) fails
-            onCapabilitiesChanged(108) | pollOrThrow(1) = CapabilitiesChanged(108) time 0..3
-            onBlockedStatus(199)       | pollOrThrow(1) = BlockedStatus(199) time 0..3
-        """)
-    }
-
-    @Test
     fun testEventuallyExpect() {
         // TODO: Current test does not verify the inline one. Also verify the behavior after
         // aligning two eventuallyExpect()
@@ -448,7 +443,6 @@
         }
     },
     Regex("""poll\((\d+)\)""") to { i, cb, t -> cb.poll(t.timeArg(1)) },
-    Regex("""pollOrThrow\((\d+)\)""") to { i, cb, t -> cb.pollOrThrow(t.timeArg(1)) },
     // Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects
     // CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and
     // likewise for all callback types.
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index c9b3d07..3382156 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -36,7 +36,9 @@
         "libnanohttpd",
         "net-tests-utils-host-device-common",
         "net-utils-device-common",
+        "net-utils-device-common-async",
         "net-utils-device-common-netlink",
+        "net-utils-device-common-wear",
         "modules-utils-build_system",
     ],
     lint: { strict_updatability_linting: true },
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
index f34ca22..f72938d 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
@@ -29,10 +29,10 @@
 import com.android.testutils.RecorderCallback
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.tryTest
-import org.junit.Test
-import org.junit.runner.RunWith
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 class ConnectivityCheckTest {
@@ -76,7 +76,7 @@
                         .addTransportType(TRANSPORT_CELLULAR)
                         .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
         tryTest {
-            cb.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>()
+            cb.poll { it is RecorderCallback.CallbackEntry.Available }
                     ?: fail("The device does not have mobile data available. Check that it is " +
                             "setup with a SIM card that has a working data plan, and that the " +
                             "APN configuration is valid.")
@@ -84,4 +84,4 @@
             cm.unregisterNetworkCallback(cb)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 7b5ad01..71f7877 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -32,6 +32,7 @@
 import android.os.SystemClock
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.testutils.RecorderCallback.CallbackEntry
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertNotNull
@@ -72,9 +73,7 @@
                 val config = getOrCreateWifiConfiguration()
                 connectToWifiConfig(config)
             }
-            val cb = callback.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>(
-                    timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
-
+            val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
             assertNotNull(cb, "Could not connect to a wifi access point within " +
                     "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
                     "configured, and that the test access point is functioning properly.")
@@ -201,4 +200,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
index 6871349..aa252a5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -65,11 +65,13 @@
  */
 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()
     }
@@ -77,4 +79,5 @@
     // 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/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
new file mode 100644
index 0000000..d7961a0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
@@ -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.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatExternalPacketForwarder(
+    srcFd: FileDescriptor,
+    mtu: Int,
+    dstFd: FileDescriptor,
+    extAddr: InetAddress,
+    natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+    /**
+     * Rewrite addresses, ports and fix up checksums for packets received on the external
+     * interface.
+     *
+     * Incoming response from external interface which is being forwarded to the internal
+     * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
+     * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
+     *
+     * For packets that are not an incoming response, do not forward them to the
+     * internal interface.
+     */
+    override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+        val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+        // TODO: support one external address per ip version.
+        val extAddrBuf = mExtAddr.address
+        if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+        // Get internal address by port.
+        val transportOffset =
+            if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+            else PacketReflector.IPV6_HEADER_LENGTH
+        val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
+        val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
+        // No mapping, skip. This usually happens if the connection is initiated directly on
+        // the external interface, e.g. DNS64 resolution, network validation, etc.
+        if (intAddrInfo == null) return
+
+        val intAddrBuf = intAddrInfo.address.address
+        val intPort = intAddrInfo.port
+
+        // Copy the original destination to into the source address.
+        for (i in 0 until addrLen) {
+            buf[addrPos + i] = buf[addrPos + addrLen + i]
+        }
+
+        // Copy the internal address into the destination address.
+        for (i in 0 until addrLen) {
+            buf[addrPos + addrLen + i] = intAddrBuf[i]
+        }
+
+        // Copy the internal port into the destination port.
+        setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
+
+        // Fix IP and Transport layer checksum.
+        fixPacketChecksum(buf, len, version, proto.toByte())
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
new file mode 100644
index 0000000..fa39d19
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatInternalPacketForwarder(
+    srcFd: FileDescriptor,
+    mtu: Int,
+    dstFd: FileDescriptor,
+    extAddr: InetAddress,
+    natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+    /**
+     * Rewrite addresses, ports and fix up checksums for packets received on the internal
+     * interface.
+     *
+     * Outgoing packet from the internal interface which is being forwarded to the
+     * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
+     * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+     *
+     * The external port, e.g. 1234 in the above example, is the port number assigned by
+     * the forwarder when creating the mapping to identify the source address and port when
+     * the response is coming from the external interface. See {@link PacketBridge.NatMap}
+     * for detail.
+     */
+    override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+        val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+        // TODO: support one external address per ip version.
+        val extAddrBuf = mExtAddr.address
+        if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+        val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
+
+        // Copy the original destination to into the source address.
+        for (i in 0 until addrLen) {
+            buf[addrPos + i] = buf[addrPos + addrLen + i]
+        }
+
+        // Copy the external address into the destination address.
+        for (i in 0 until addrLen) {
+            buf[addrPos + addrLen + i] = extAddrBuf[i]
+        }
+
+        // Add an entry to NAT mapping table.
+        val transportOffset =
+            if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+            else PacketReflector.IPV6_HEADER_LENGTH
+        val srcPort = getPortAt(buf, transportOffset)
+        val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
+        // Copy the external port to into the source port.
+        setPortAt(extPort, buf, transportOffset)
+
+        // Fix IP and Transport layer checksum.
+        fixPacketChecksum(buf, len, version, proto.toByte())
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
new file mode 100644
index 0000000..85c6493
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
@@ -0,0 +1,206 @@
+/*
+ * 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 static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface} with NAT.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the external address.
+ *
+ * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
+ * is implemented during forwarding, which will swap the source and destination,
+ * but replacing the source address with the external address,
+ * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * For the above example, a client who sends http request will have a hallucination that
+ * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
+ * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
+ * to a local address 1.2.3.4.
+ *
+ * And a NAT mapping is created at the time when the outgoing packet is forwarded.
+ * With a different internal source port, the instance learned that when a response with the
+ * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped, and the source address will be replaced
+ * with the internal address, which is obtained from the NAT mapping described above.
+ */
+public abstract class NatPacketForwarderBase extends Thread {
+    private static final String TAG = "NatPacketForwarder";
+    static final int DESTINATION_PORT_OFFSET = 2;
+
+    // The source fd to read packets from.
+    @NonNull
+    final FileDescriptor mSrcFd;
+    // The buffer to temporarily hold the entire packet after receiving.
+    @NonNull
+    final byte[] mBuf;
+    // The destination fd to write packets to.
+    @NonNull
+    final FileDescriptor mDstFd;
+    // The NAT mapping table shared between two NatPacketForwarder instances to map from
+    // the source port to the associated internal address. The map can be read/write from two
+    // different threads on any given time whenever receiving packets on the
+    // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
+    @GuardedBy("mNatMap")
+    @NonNull
+    final PacketBridge.NatMap mNatMap;
+    // The address of the external interface. See {@link NatPacketForwarder}.
+    @NonNull
+    final InetAddress mExtAddr;
+
+    /**
+     * Construct a {@link NatPacketForwarderBase}.
+     *
+     * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+     * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
+     * NAT applied. See {@link NatPacketForwarderBase}.
+     *
+     * To apply NAT, the address of the external interface needs to be supplied through
+     * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
+     * {@code natMap} is needed to be shared between these two instances.
+     *
+     * Note that this class is not useful if the instance is not managed by a
+     * {@link PacketBridge} to set up a two-way communication.
+     *
+     * @param srcFd   {@link FileDescriptor} to read packets from.
+     * @param mtu     MTU of the test network.
+     * @param dstFd   {@link FileDescriptor} to write packets to.
+     * @param extAddr the external address, which is the address of the external interface.
+     *                See {@link NatPacketForwarderBase}.
+     * @param natMap  the NAT mapping table shared between two {@link NatPacketForwarderBase}
+     *                instance.
+     */
+    public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+            @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
+            @NonNull PacketBridge.NatMap natMap) {
+        super(TAG);
+        mSrcFd = Objects.requireNonNull(srcFd);
+        mBuf = new byte[mtu];
+        mDstFd = Objects.requireNonNull(dstFd);
+        mExtAddr = Objects.requireNonNull(extAddr);
+        mNatMap = Objects.requireNonNull(natMap);
+    }
+
+    /**
+     * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+     * which includes re-write addresses, ports and fix up checksums.
+     * Subclasses should override this method to implement a simple NAT.
+     */
+    abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+
+    private void forwardPacket(@NonNull byte[] buf, int len) {
+        try {
+            Os.write(mDstFd, buf, 0, len);
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Error writing packet: " + e.getMessage());
+        }
+    }
+
+    // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+    private void processPacket() {
+        final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+        if (len < 1) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        final int version = mBuf[0] >>> 4;
+        final int protoPos, ipHdrLen;
+        switch (version) {
+            case 4:
+                ipHdrLen = IPV4_HEADER_LENGTH;
+                protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+                break;
+            case 6:
+                ipHdrLen = IPV6_HEADER_LENGTH;
+                protoPos = IPV6_PROTO_OFFSET;
+                break;
+            default:
+                throw new IllegalStateException("Unexpected version: " + version);
+        }
+        if (len < ipHdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        final byte proto = mBuf[protoPos];
+        final int transportHdrLen;
+        switch (proto) {
+            case IPPROTO_TCP:
+                transportHdrLen = TCP_HEADER_LENGTH;
+                break;
+            case IPPROTO_UDP:
+                transportHdrLen = UDP_HEADER_LENGTH;
+                break;
+            // TODO: Support ICMP.
+            default:
+                return; // Unknown protocol, ignored.
+        }
+
+        if (len < ipHdrLen + transportHdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+        // Re-write addresses, ports and fix up checksums.
+        preparePacketForForwarding(mBuf, len, version, proto);
+        // Send the packet to the destination fd.
+        forwardPacket(mBuf, len);
+    }
+
+    @Override
+    public void run() {
+        Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+        while (!interrupted() && mSrcFd.valid()) {
+            processPacket();
+        }
+        Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 0000000..da3508d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+private const val MIN_PORT_NUMBER = 1025
+private const val MAX_PORT_NUMBER = 65535
+
+/**
+ * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ *
+ * See {@link NatPacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+    context: Context,
+    internalAddr: LinkAddress,
+    externalAddr: LinkAddress,
+    dnsAddr: InetAddress
+) {
+    private val natMap = NatMap()
+    private val binder = Binder()
+
+    private val cm = context.getSystemService(ConnectivityManager::class.java)
+    private val tnm = context.getSystemService(TestNetworkManager::class.java)
+
+    // Create test networks.
+    private val internalIface = tnm.createTunInterface(listOf(internalAddr))
+    private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+
+    // Register test networks to ConnectivityService.
+    private val internalNetworkCallback: TestableNetworkCallback
+    private val externalNetworkCallback: TestableNetworkCallback
+    val internalNetwork: Network
+    val externalNetwork: Network
+    init {
+        val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
+        val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+        internalNetworkCallback = inCb
+        externalNetworkCallback = exCb
+        internalNetwork = inNet
+        externalNetwork = exNet
+    }
+
+    // Setup the packet bridge.
+    private val internalFd = internalIface.fileDescriptor.fileDescriptor
+    private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+    private val pr1 = NatInternalPacketForwarder(
+        internalFd,
+        1500,
+        externalFd,
+        externalAddr.address,
+        natMap
+    )
+    private val pr2 = NatExternalPacketForwarder(
+        externalFd,
+        1500,
+        internalFd,
+        externalAddr.address,
+        natMap
+    )
+
+    fun start() {
+        IoUtils.setBlocking(internalFd, true /* blocking */)
+        IoUtils.setBlocking(externalFd, true /* blocking */)
+        pr1.start()
+        pr2.start()
+    }
+
+    fun stop() {
+        pr1.interrupt()
+        pr2.interrupt()
+        cm.unregisterNetworkCallback(internalNetworkCallback)
+        cm.unregisterNetworkCallback(externalNetworkCallback)
+    }
+
+    /**
+     * Creates a test network with given test TUN interface and addresses.
+     */
+    private fun createTestNetwork(
+        testIface: TestNetworkInterface,
+        addr: LinkAddress,
+        dnsAddr: InetAddress
+    ): Pair<TestableNetworkCallback, Network> {
+        // Make a network request to hold the test network
+        val nr = NetworkRequest.Builder()
+            .clearCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+            .build()
+        val testCb = TestableNetworkCallback()
+        cm.requestNetwork(nr, testCb)
+
+        val lp = LinkProperties().apply {
+            addLinkAddress(addr)
+            interfaceName = testIface.interfaceName
+            addDnsServer(dnsAddr)
+        }
+        tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+        // Wait for available before return.
+        val network = testCb.expect<Available>().network
+        return testCb to network
+    }
+
+    /**
+     * A helper class to maintain the mappings between internal addresses/ports and external
+     * ports.
+     *
+     * This class assigns an unused external port number if the mapping between
+     * srcaddress:srcport:protocol and the external port does not exist yet.
+     *
+     * Note that this class is not thread-safe. The instance of the class needs to be
+     * synchronized in the callers when being used in multiple threads.
+     */
+    class NatMap {
+        data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
+
+        private val mToExternalPort = HashMap<AddressInfo, Int>()
+        private val mFromExternalPort = HashMap<Int, AddressInfo>()
+
+        // Skip well-known port 0~1024.
+        private var nextExternalPort = MIN_PORT_NUMBER
+
+        fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
+            val info = AddressInfo(addr, port, protocol)
+            val extPort: Int
+            if (!mToExternalPort.containsKey(info)) {
+                extPort = nextExternalPort++
+                if (nextExternalPort > MAX_PORT_NUMBER) {
+                    throw IllegalStateException("Available ports are exhausted")
+                }
+                mToExternalPort[info] = extPort
+                mFromExternalPort[extPort] = info
+            } else {
+                extPort = mToExternalPort[info]!!
+            }
+            return extPort
+        }
+
+        fun fromExternalPort(port: Int): AddressInfo? {
+            return mFromExternalPort[port]
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
new file mode 100644
index 0000000..69392d4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2014 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 static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * A class that echoes packets received on a {@link TestNetworkInterface} back to itself.
+ *
+ * For testing purposes, sometimes a mocked environment to simulate a simple echo from the
+ * server side is needed. This is particularly useful if the test, e.g. VpnTest, is
+ * heavily relying on the outside world.
+ *
+ * This class reads packets from the {@link FileDescriptor} of a {@link TestNetworkInterface}, and:
+ *   1. For TCP and UDP packets, simply swaps the source address and the destination
+ *      address, then send it back to the {@link FileDescriptor}.
+ *   2. For ICMP ping packets, composes a ping reply and sends it back to the sender.
+ *   3. Ignore all other packets.
+ */
+public class PacketReflector extends Thread {
+
+    static final int IPV4_HEADER_LENGTH = 20;
+    static final int IPV6_HEADER_LENGTH = 40;
+
+    static final int IPV4_ADDR_OFFSET = 12;
+    static final int IPV6_ADDR_OFFSET = 8;
+    static final int IPV4_ADDR_LENGTH = 4;
+    static final int IPV6_ADDR_LENGTH = 16;
+
+    static final int IPV4_PROTO_OFFSET = 9;
+    static final int IPV6_PROTO_OFFSET = 6;
+
+    static final byte IPPROTO_ICMP = 1;
+    static final byte IPPROTO_TCP = 6;
+    static final byte IPPROTO_UDP = 17;
+    private static final byte IPPROTO_ICMPV6 = 58;
+
+    private static final int ICMP_HEADER_LENGTH = 8;
+    static final int TCP_HEADER_LENGTH = 20;
+    static final int UDP_HEADER_LENGTH = 8;
+
+    private static final byte ICMP_ECHO = 8;
+    private static final byte ICMP_ECHOREPLY = 0;
+
+    private static String TAG = "PacketReflector";
+
+    @NonNull
+    private final FileDescriptor mFd;
+    @NonNull
+    private final byte[] mBuf;
+
+    /**
+     * Construct a {@link PacketReflector} from the given {@code fd} of
+     * a {@link TestNetworkInterface}.
+     *
+     * @param fd {@link FileDescriptor} to read/write packets.
+     * @param mtu MTU of the test network.
+     */
+    public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
+        super("PacketReflector");
+        mFd = Objects.requireNonNull(fd);
+        mBuf = new byte[mtu];
+    }
+
+    private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
+        for (int i = 0; i < len; i++) {
+            byte b = buf[pos1 + i];
+            buf[pos1 + i] = buf[pos2 + i];
+            buf[pos2 + i] = b;
+        }
+    }
+
+    private static void swapAddresses(@NonNull byte[] buf, int version) {
+        int addrPos, addrLen;
+        switch (version) {
+            case 4:
+                addrPos = IPV4_ADDR_OFFSET;
+                addrLen = IPV4_ADDR_LENGTH;
+                break;
+            case 6:
+                addrPos = IPV6_ADDR_OFFSET;
+                addrLen = IPV6_ADDR_LENGTH;
+                break;
+            default:
+                throw new IllegalArgumentException();
+        }
+        swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+    }
+
+    // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+    // This is used by the test to "connect to itself" through the VPN.
+    private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + TCP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        swapAddresses(buf, version);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+    // This is used by the test to check that the bytes it sends are echoed back.
+    private void processUdpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + UDP_HEADER_LENGTH) {
+            return;
+        }
+
+        // Swap src and dst IP addresses.
+        swapAddresses(buf, version);
+
+        // Swap dst and src ports.
+        int portOffset = hdrLen;
+        swapBytes(buf, portOffset, portOffset + 2, 2);
+
+        // Send the packet back.
+        writePacket(buf, len);
+    }
+
+    private void processIcmpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+        if (len < hdrLen + ICMP_HEADER_LENGTH) {
+            return;
+        }
+
+        byte type = buf[hdrLen];
+        if (!(version == 4 && type == ICMP_ECHO) &&
+                !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+            return;
+        }
+
+        // Save the ping packet we received.
+        byte[] request = buf.clone();
+
+        // Swap src and dst IP addresses, and send the packet back.
+        // This effectively pings the device to see if it replies.
+        swapAddresses(buf, version);
+        writePacket(buf, len);
+
+        // The device should have replied, and buf should now contain a ping response.
+        int received = PacketReflectorUtil.readPacket(mFd, buf);
+        if (received != len) {
+            Log.i(TAG, "Reflecting ping did not result in ping response: " +
+                    "read=" + received + " expected=" + len);
+            return;
+        }
+
+        byte replyType = buf[hdrLen];
+        if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+                || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+            Log.i(TAG, "Received unexpected ICMP reply: original " + type
+                    + ", reply " + replyType);
+            return;
+        }
+
+        // Compare the response we got with the original packet.
+        // The only thing that should have changed are addresses, type and checksum.
+        // Overwrite them with the received bytes and see if the packet is otherwise identical.
+        request[hdrLen] = buf[hdrLen];          // Type
+        request[hdrLen + 2] = buf[hdrLen + 2];  // Checksum byte 1.
+        request[hdrLen + 3] = buf[hdrLen + 3];  // Checksum byte 2.
+
+        // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+        // the request and reply may have different IPv6 flow label: ignore that as well.
+        if (version == 6) {
+            request[1] = (byte) (request[1] & 0xf0 | buf[1] & 0x0f);
+            request[2] = buf[2];
+            request[3] = buf[3];
+        }
+
+        for (int i = 0; i < len; i++) {
+            if (buf[i] != request[i]) {
+                Log.i(TAG, "Received non-matching packet when expecting ping response.");
+                return;
+            }
+        }
+
+        // Now swap the addresses again and reflect the packet. This sends a ping reply.
+        swapAddresses(buf, version);
+        writePacket(buf, len);
+    }
+
+    private void writePacket(@NonNull byte[] buf, int len) {
+        try {
+            Os.write(mFd, buf, 0, len);
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Error writing packet: " + e.getMessage());
+        }
+    }
+
+    // Reads one packet from our mFd, and possibly writes the packet back.
+    private void processPacket() {
+        int len = PacketReflectorUtil.readPacket(mFd, mBuf);
+        if (len < 1) {
+            // Usually happens when socket read is being interrupted, e.g. stopping PacketReflector.
+            return;
+        }
+
+        int version = mBuf[0] >> 4;
+        int protoPos, hdrLen;
+        if (version == 4) {
+            hdrLen = IPV4_HEADER_LENGTH;
+            protoPos = IPV4_PROTO_OFFSET;
+        } else if (version == 6) {
+            hdrLen = IPV6_HEADER_LENGTH;
+            protoPos = IPV6_PROTO_OFFSET;
+        } else {
+            throw new IllegalStateException("Unexpected version: " + version);
+        }
+
+        if (len < hdrLen) {
+            throw new IllegalStateException("Unexpected buffer length: " + len);
+        }
+
+        byte proto = mBuf[protoPos];
+        switch (proto) {
+            case IPPROTO_ICMP:
+                // fall through
+            case IPPROTO_ICMPV6:
+                processIcmpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_TCP:
+                processTcpPacket(mBuf, version, len, hdrLen);
+                break;
+            case IPPROTO_UDP:
+                processUdpPacket(mBuf, version, len, hdrLen);
+                break;
+        }
+    }
+
+    public void run() {
+        Log.i(TAG, "starting fd=" + mFd + " valid=" + mFd.valid());
+        while (!interrupted() && mFd.valid()) {
+            processPacket();
+        }
+        Log.i(TAG, "exiting fd=" + mFd + " valid=" + mFd.valid());
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 0000000..b028045
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.IOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+    return try {
+        Os.read(fd, buf, 0, buf.size)
+    } catch (e: ErrnoException) {
+        -1
+    } catch (e: IOException) {
+        -1
+    }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+    InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+    return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+    buf[pos] = (port ushr 8).toByte()
+    buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+    4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+    6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+    else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+    // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+    if (version == 4) {
+        val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+        // Place checksum in Big-endian order.
+        buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+        buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+    }
+
+    // Fill transport layer checksum.
+    val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+    when (protocol) {
+        PacketReflector.IPPROTO_UDP -> {
+            val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+            // Clear before calculate.
+            buf[checksumPos + 1] = 0x00
+            buf[checksumPos] = buf[checksumPos + 1]
+            val checksum = IpUtils.udpChecksum(
+                ByteBuffer.wrap(buf), 0,
+                transportOffset
+            )
+            buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+            buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+        }
+        PacketReflector.IPPROTO_TCP -> {
+            val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+            // Clear before calculate.
+            buf[checksumPos + 1] = 0x00
+            buf[checksumPos] = buf[checksumPos + 1]
+            val transportLen: Int = len - transportOffset
+            val checksum = IpUtils.tcpChecksum(
+                ByteBuffer.wrap(buf), 0, transportOffset,
+                transportLen
+            )
+            buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+            buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+        }
+        // TODO: Support ICMP.
+        else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 39ce487..740bf63 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -19,6 +19,7 @@
 import android.net.Uri
 import com.android.net.module.util.ArrayTrackRecord
 import fi.iki.elonen.NanoHTTPD
+import java.io.IOException
 
 /**
  * A minimal HTTP server running on a random available port.
@@ -82,7 +83,23 @@
         val request = Request(session.uri
                 ?: "", session.method, session.queryParameterString ?: "")
         requestsRecord.add(request)
+
+        // For PUT and POST, call parseBody to read InputStream before responding.
+        if (Method.PUT == session.method || Method.POST == session.method) {
+            try {
+                session.parseBody(HashMap())
+            } catch (e: Exception) {
+                when (e) {
+                    is IOException, is ResponseException -> e.toResponse()
+                    else -> throw e
+                }
+            }
+        }
+
         // Default response is a 404
         return responses[request] ?: super.serve(session)
     }
-}
\ No newline at end of file
+
+    fun Exception.toResponse() =
+        newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", this.toString())
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 68d5fa9..0e73112 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 {
@@ -220,19 +220,8 @@
      * Long.MAX_VALUE.
      */
     @JvmOverloads
-    fun poll(timeoutMs: Long = defaultTimeoutMs): CallbackEntry? = history.poll(timeoutMs)
-
-    /**
-     * Get the next callback or throw if timeout.
-     *
-     * With no argument, this method waits out the default timeout. To wait forever, pass
-     * Long.MAX_VALUE.
-     */
-    @JvmOverloads
-    fun pollOrThrow(
-        timeoutMs: Long = defaultTimeoutMs,
-        errorMsg: String = "Did not receive callback after $timeoutMs"
-    ): CallbackEntry = poll(timeoutMs) ?: fail(errorMsg)
+    fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (CallbackEntry) -> Boolean = { true }) =
+            history.poll(timeoutMs, predicate)
 
     /*****
      * expect family of methods.
@@ -349,15 +338,16 @@
         timeoutMs: Long = defaultTimeoutMs,
         errorMsg: String? = null,
         test: (T) -> Boolean = { true }
-    ) = pollOrThrow(timeoutMs, "Did not receive ${T::class.simpleName} after ${timeoutMs}ms").also {
-        if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
-        if (ANY_NETWORK !== network && it.network != network) {
-            fail("Expected network $network for callback : $it")
-        }
-        if (!test(it)) {
-            fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
-        }
-    } as T
+    ) = (poll(timeoutMs) ?: fail("Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+            .also {
+                if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
+                if (ANY_NETWORK !== network && it.network != network) {
+                    fail("Expected network $network for callback : $it")
+                }
+                if (!test(it)) {
+                    fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
+                }
+            } as T
 
     inline fun <reified T : CallbackEntry> expect(
         network: HasNetwork,
@@ -366,6 +356,12 @@
         test: (T) -> Boolean = { true }
     ) = expect(network.network, timeoutMs, errorMsg, test)
 
+    /*****
+     * assertNoCallback family of methods.
+     * These methods make sure that no callback that matches the predicate was received.
+     * If no predicate is given, they make sure that no callback at all was received.
+     * These methods run the waiter func given in the constructor if any.
+     */
     @JvmOverloads
     fun assertNoCallback(
         timeoutMs: Long = defaultNoCallbackTimeoutMs,
@@ -378,14 +374,17 @@
     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.
+    /*****
+     * eventuallyExpect family of methods.
+     * These methods make sure a callback that matches the type/predicate is received eventually.
+     * Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
+     * They fail if no callback matching the predicate is received within the timeout.
+     */
     inline fun <reified T : CallbackEntry> eventuallyExpect(
         timeoutMs: Long = defaultTimeoutMs,
         from: Int = mark,
         crossinline predicate: (T) -> Boolean = { true }
-    ): T = eventuallyExpectOrNull(timeoutMs, from, predicate).also {
+    ): T = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
         assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms")
     } as T
 
@@ -407,27 +406,6 @@
         assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms")
     } as T
 
-    // TODO (b/157405399) straighten and unify the method names
-    inline fun <reified T : CallbackEntry> eventuallyExpectOrNull(
-        timeoutMs: Long = defaultTimeoutMs,
-        from: Int = mark,
-        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.
@@ -448,18 +426,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(
@@ -472,10 +450,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)
     }
@@ -488,16 +464,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.
@@ -514,17 +480,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
@@ -571,38 +537,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..48b57d7
--- /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.async;
+
+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..d5cca0a
--- /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.async;
+
+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/async/ReadableDataAnswer.java b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
new file mode 100644
index 0000000..4bf5527
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
@@ -0,0 +1,76 @@
+/*
+ * 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.async;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+public class ReadableDataAnswer implements Answer {
+    private final ArrayList<byte[]> mBuffers = new ArrayList<>();
+    private int mBufferPos;
+
+    public ReadableDataAnswer(byte[] ... buffers) {
+        for (byte[] buffer : buffers) {
+            addBuffer(buffer);
+        }
+    }
+
+    public void addBuffer(byte[] buffer) {
+        if (buffer.length != 0) {
+            mBuffers.add(buffer);
+        }
+    }
+
+    public int getRemainingSize() {
+        int totalSize = 0;
+        for (byte[] buffer : mBuffers) {
+            totalSize += buffer.length;
+        }
+        return totalSize - mBufferPos;
+    }
+
+    private void cleanupBuffers() {
+        if (!mBuffers.isEmpty() && mBufferPos == mBuffers.get(0).length) {
+            mBuffers.remove(0);
+            mBufferPos = 0;
+        }
+    }
+
+    @Override
+    public Object answer(InvocationOnMock invocation) throws Throwable {
+        cleanupBuffers();
+
+        if (mBuffers.isEmpty()) {
+            return Integer.valueOf(0);
+        }
+
+        byte[] src = mBuffers.get(0);
+
+        byte[] dst = invocation.<byte[]>getArgument(0);
+        int dstPos = invocation.<Integer>getArgument(1);
+        int dstLen = invocation.<Integer>getArgument(2);
+
+        int copyLen = Math.min(dstLen, src.length - mBufferPos);
+        System.arraycopy(src, mBufferPos, dst, dstPos, copyLen);
+        mBufferPos += copyLen;
+
+        cleanupBuffers();
+        return Integer.valueOf(copyLen);
+    }
+}
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)