Merge "Add attach bpf with flags and detach single bpf"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 0585c09..ef80170 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -111,21 +111,25 @@
     name: "net-utils-device-common-bpf",
     srcs: [
         "device/com/android/net/module/util/BpfMap.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",
+        "device/com/android/net/module/util/bpf/*.java",
     ],
-    sdk_version: "system_current",
+    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__",
         "//packages/modules/NetworkStack:__subpackages__",
-    ],
-    static_libs: [
-        "net-utils-device-common-struct",
+        "//frameworks/base/services/core",
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
         "com.android.tethering",
@@ -139,10 +143,11 @@
     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",
         "device/com/android/net/module/util/structs/*.java",
     ],
-    sdk_version: "system_current",
+    sdk_version: "module_current",
     min_sdk_version: "29",
     visibility: [
         "//frameworks/libs/net/common/testutils:__subpackages__",
@@ -154,6 +159,7 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
         "com.android.tethering",
@@ -167,7 +173,7 @@
     srcs: [
         "device/com/android/net/module/util/netlink/*.java",
     ],
-    sdk_version: "system_current",
+    sdk_version: "module_current",
     min_sdk_version: "29",
     visibility: [
         "//frameworks/libs/net/common/testutils:__subpackages__",
@@ -179,6 +185,7 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
         "com.android.tethering",
@@ -222,11 +229,13 @@
     name: "net-utils-framework-common",
     srcs: [
         ":net-utils-framework-common-srcs",
-        // TODO: avoid including all framework annotations as they end up in library users jars
-        // and need jarjaring
-        ":framework-annotations",
     ],
-    sdk_version: "system_current",
+    sdk_version: "module_current",
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity.stubs.module_lib",
+        "framework-connectivity-tiramisu.stubs.module_lib",
+    ],
     jarjar_rules: "jarjar-rules-shared.txt",
     visibility: [
         "//cts/tests/tests/net",
@@ -245,6 +254,7 @@
         "//frameworks/libs/net/common/tests:__subpackages__",
         "//frameworks/libs/net/common/device",
         "//packages/modules/Wifi/framework/tests:__subpackages__",
+        "//packages/apps/Settings",
     ],
     lint: { strict_updatability_linting: true },
 }
@@ -276,8 +286,10 @@
         "framework-connectivity",
     ],
     visibility: [
+        // TODO: remove after NetworkStatsService moves to the module.
         "//frameworks/base/services/net",
         "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Bluetooth/android/app",
     ],
     lint: { strict_updatability_linting: true },
 }
@@ -333,31 +345,3 @@
         "//packages/modules/Wifi/service",
     ],
 }
-
-// This file group is deprecated; new users should use net-utils-annotations
-filegroup {
-    name: "net-utils-annotations-srcs",
-    srcs: [
-        "annotations/android/net/annotations/PolicyDirection.java",
-    ],
-    visibility: [
-        "//frameworks/base",
-    ],
-}
-
-
-java_library {
-    name: "net-utils-annotations",
-    srcs: [":net-utils-annotations-srcs"],
-    libs: [
-        "framework-annotations-lib",
-    ],
-    sdk_version: "system_current",
-    min_sdk_version: "30",
-    visibility: ["//visibility:public"],
-    apex_available: [
-        "//apex_available:anyapex",
-        "//apex_available:platform",
-    ],
-    lint: { strict_updatability_linting: true },
-}
diff --git a/staticlibs/annotations/android/net/annotations/PolicyDirection.java b/staticlibs/annotations/android/net/annotations/PolicyDirection.java
deleted file mode 100644
index febd9b4..0000000
--- a/staticlibs/annotations/android/net/annotations/PolicyDirection.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.annotations;
-
-import android.annotation.IntDef;
-import android.net.IpSecManager;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * IPsec traffic direction.
- *
- * <p>Mainline modules cannot reference hidden @IntDef. Moving this annotation to a separate class
- * to allow others to statically include it.
- *
- * @hide
- */
-@IntDef(value = {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT})
-@Retention(RetentionPolicy.SOURCE)
-public @interface PolicyDirection {}
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 5f05c7c..b42c388 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -40,7 +40,7 @@
  * @param <K> the key of the map.
  * @param <V> the value of the map.
  */
-public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable {
+public class BpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V>, AutoCloseable {
     static {
         System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
     }
@@ -100,6 +100,7 @@
      * Update an existing or create a new key -> value entry in an eBbpf map.
      * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
      */
+    @Override
     public void updateEntry(K key, V value) throws ErrnoException {
         writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
     }
@@ -108,6 +109,7 @@
      * If the key does not exist in the map, insert key -> value entry into eBpf map.
      * Otherwise IllegalStateException will be thrown.
      */
+    @Override
     public void insertEntry(K key, V value)
             throws ErrnoException, IllegalStateException {
         try {
@@ -123,6 +125,7 @@
      * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
      * will be thrown.
      */
+    @Override
     public void replaceEntry(K key, V value)
             throws ErrnoException, NoSuchElementException {
         try {
@@ -140,6 +143,7 @@
      * (use updateEntry() if you don't care whether insert or replace happened)
      * Note: see inline comment below if running concurrently with delete operations.
      */
+    @Override
     public boolean insertOrReplaceEntry(K key, V value)
             throws ErrnoException {
         try {
@@ -164,11 +168,13 @@
     }
 
     /** Remove existing key from eBpf map. Return false if map was not modified. */
+    @Override
     public boolean deleteEntry(K key) throws ErrnoException {
         return deleteMapEntry(mMapFd, key.writeToBytes());
     }
 
     /** Returns {@code true} if this map contains no elements. */
+    @Override
     public boolean isEmpty() throws ErrnoException {
         return getFirstKey() == null;
     }
@@ -189,6 +195,7 @@
      *
      * TODO: consider allowing null passed-in key.
      */
+    @Override
     public K getNextKey(@NonNull K key) throws ErrnoException {
         Objects.requireNonNull(key);
         return getNextKeyInternal(key);
@@ -202,11 +209,13 @@
     }
 
     /** Get the first key of eBpf map. */
+    @Override
     public K getFirstKey() throws ErrnoException {
         return getNextKeyInternal(null);
     }
 
     /** Check whether a key exists in the map. */
+    @Override
     public boolean containsKey(@NonNull K key) throws ErrnoException {
         Objects.requireNonNull(key);
 
@@ -215,6 +224,7 @@
     }
 
     /** Retrieve a value from the map. Return null if there is no such key. */
+    @Override
     public V getValue(@NonNull K key) throws ErrnoException {
         Objects.requireNonNull(key);
         final byte[] rawValue = getRawValue(key.writeToBytes());
@@ -239,6 +249,7 @@
      * other structural modifications to the map, such as adding entries or deleting other entries.
      * Otherwise, iteration will result in undefined behaviour.
      */
+    @Override
     public void forEach(BiConsumer<K, V> action) throws ErrnoException {
         @Nullable K nextKey = getFirstKey();
 
@@ -262,6 +273,7 @@
      * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
      *                        or if a non-ENOENT error occurred when deleting a key.
      */
+    @Override
     public void clear() throws ErrnoException {
         K key = getFirstKey();
         while (key != null) {
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
new file mode 100644
index 0000000..708cf61
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+
+import java.util.NoSuchElementException;
+import java.util.function.BiConsumer;
+
+/**
+ * The interface of BpfMap. This could be used to inject for testing.
+ * So the testing code won't load the JNI and update the entries to kernel.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public interface IBpfMap<K extends Struct, V extends Struct> {
+    /** Update an existing or create a new key -> value entry in an eBbpf map. */
+    void updateEntry(K key, V value) throws ErrnoException;
+
+    /** If the key does not exist in the map, insert key -> value entry into eBpf map. */
+    void insertEntry(K key, V value) throws ErrnoException, IllegalStateException;
+
+    /** If the key already exists in the map, replace its value. */
+    void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException;
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map. Returns true if
+     * inserted, false if replaced. (use updateEntry() if you don't care whether insert or replace
+     * happened).
+     */
+    boolean insertOrReplaceEntry(K key, V value) throws ErrnoException;
+
+    /** Remove existing key from eBpf map. Return true if something was deleted. */
+    boolean deleteEntry(K key) throws ErrnoException;
+
+    /** Returns {@code true} if this map contains no elements. */
+    boolean isEmpty() throws ErrnoException;
+
+    /** Get the key after the passed-in key. */
+    K getNextKey(@NonNull K key) throws ErrnoException;
+
+    /** Get the first key of the eBpf map. */
+    K getFirstKey() throws ErrnoException;
+
+    /** Check whether a key exists in the map. */
+    boolean containsKey(@NonNull K key) throws ErrnoException;
+
+    /** Retrieve a value from the map. */
+    V getValue(@NonNull K key) throws ErrnoException;
+
+    /**
+     * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+     */
+    void forEach(BiConsumer<K, V> action) throws ErrnoException;
+
+    /** Clears the map. */
+    void clear() throws ErrnoException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
new file mode 100644
index 0000000..c908528
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2021 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;
+
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.IpUtils.ipChecksum;
+import static com.android.net.module.util.IpUtils.tcpChecksum;
+import static com.android.net.module.util.IpUtils.udpChecksum;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.TCP_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.UDP_CHECKSUM_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.UDP_LENGTH_OFFSET;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.TcpHeader;
+import com.android.net.module.util.structs.UdpHeader;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+
+/**
+ * The class is used to build a packet.
+ *
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                Layer 2 header (EthernetHeader)                | (optional)
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                  Layer 3 header (Ipv4Header)                  |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Layer 4 header (TcpHeader, UdpHeader)               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Payload                             | (optional)
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Below is a sample code to build a packet.
+ *
+ * // Initialize builder
+ * final ByteBuffer buf = ByteBuffer.allocate(...);
+ * final PacketBuilder pb = new PacketBuilder(buf);
+ * // Write headers
+ * pb.writeL2Header(...);
+ * pb.writeIpHeader(...);
+ * pb.writeTcpHeader(...);
+ * // Write payload
+ * buf.putInt(...);
+ * buf.putShort(...);
+ * buf.putByte(...);
+ * // Finalize and use the packet
+ * pb.finalizePacket();
+ * sendPacket(buf);
+ */
+public class PacketBuilder {
+    private final ByteBuffer mBuffer;
+
+    private int mIpv4HeaderOffset = -1;
+    private int mTcpHeaderOffset = -1;
+    private int mUdpHeaderOffset = -1;
+
+    public PacketBuilder(@NonNull ByteBuffer buffer) {
+        mBuffer = buffer;
+    }
+
+    /**
+     * Write an ethernet header.
+     *
+     * @param srcMac source MAC address
+     * @param dstMac destination MAC address
+     * @param etherType ether type
+     */
+    public void writeL2Header(MacAddress srcMac, MacAddress dstMac, short etherType) throws
+            IOException {
+        final EthernetHeader ethv4Header = new EthernetHeader(dstMac, srcMac, etherType);
+        try {
+            ethv4Header.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write an IPv4 header.
+     * The IP header length and checksum are calculated and written back in #finalizePacket.
+     *
+     * @param tos type of service
+     * @param id the identification
+     * @param flagsAndFragmentOffset flags and fragment offset
+     * @param ttl time to live
+     * @param protocol protocol
+     * @param srcIp source IP address
+     * @param dstIp destination IP address
+     */
+    public void writeIpv4Header(byte tos, short id, short flagsAndFragmentOffset, byte ttl,
+            byte protocol, @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp)
+            throws IOException {
+        mIpv4HeaderOffset = mBuffer.position();
+        final Ipv4Header ipv4Header = new Ipv4Header(tos,
+                (short) 0 /* totalLength, calculate in #finalizePacket */, id,
+                flagsAndFragmentOffset, ttl, protocol,
+                (short) 0 /* checksum, calculate in #finalizePacket */, srcIp, dstIp);
+
+        try {
+            ipv4Header.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write a TCP header.
+     * The TCP header checksum is calculated and written back in #finalizePacket.
+     *
+     * @param srcPort source port
+     * @param dstPort destination port
+     * @param seq sequence number
+     * @param ack acknowledgement number
+     * @param tcpFlags tcp flags
+     * @param window window size
+     * @param urgentPointer urgent pointer
+     */
+    public void writeTcpHeader(short srcPort, short dstPort, short seq, short ack,
+            byte tcpFlags, short window, short urgentPointer) throws IOException {
+        mTcpHeaderOffset = mBuffer.position();
+        final TcpHeader tcpHeader = new TcpHeader(srcPort, dstPort, seq, ack,
+                (short) ((short) 0x5000 | ((byte) 0x3f & tcpFlags)) /* dataOffsetAndControlBits,
+                dataOffset is always 5(*4bytes) because options not supported */, window,
+                (short) 0 /* checksum, calculate in #finalizePacket */,
+                urgentPointer);
+
+        try {
+            tcpHeader.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Write a UDP header.
+     * The UDP header length and checksum are calculated and written back in #finalizePacket.
+     *
+     * @param srcPort source port
+     * @param dstPort destination port
+     */
+    public void writeUdpHeader(short srcPort, short dstPort) throws IOException {
+        mUdpHeaderOffset = mBuffer.position();
+        final UdpHeader udpHeader = new UdpHeader(srcPort, dstPort,
+                (short) 0 /* length, calculate in #finalizePacket */,
+                (short) 0 /* checksum, calculate in #finalizePacket */);
+
+        try {
+            udpHeader.writeToByteBuffer(mBuffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
+    /**
+     * Finalize the packet.
+     *
+     * Call after writing L4 header (no payload) or payload to the buffer used by the builder.
+     * L3 header length, L3 header checksum and L4 header checksum are calculated and written back
+     * after finalization.
+     */
+    @NonNull
+    public ByteBuffer finalizePacket() throws IOException {
+        if (mIpv4HeaderOffset < 0) {
+            // TODO: add support for IPv6
+            throw new IOException("Packet is missing IPv4 header");
+        }
+
+        // Populate the IPv4 totalLength field.
+        mBuffer.putShort(mIpv4HeaderOffset + IPV4_LENGTH_OFFSET,
+                (short) (mBuffer.position() - mIpv4HeaderOffset));
+
+        // Populate the IPv4 header checksum field.
+        mBuffer.putShort(mIpv4HeaderOffset + IPV4_CHECKSUM_OFFSET,
+                ipChecksum(mBuffer, mIpv4HeaderOffset /* headerOffset */));
+
+        if (mTcpHeaderOffset > 0) {
+            // Populate the TCP header checksum field.
+            mBuffer.putShort(mTcpHeaderOffset + TCP_CHECKSUM_OFFSET, tcpChecksum(mBuffer,
+                    mIpv4HeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */,
+                    mBuffer.position() - mTcpHeaderOffset /* transportLen */));
+        } else if (mUdpHeaderOffset > 0) {
+            // Populate the UDP header length field.
+            mBuffer.putShort(mUdpHeaderOffset + UDP_LENGTH_OFFSET,
+                    (short) (mBuffer.position() - mUdpHeaderOffset));
+
+            // Populate the UDP header checksum field.
+            mBuffer.putShort(mUdpHeaderOffset + UDP_CHECKSUM_OFFSET, udpChecksum(mBuffer,
+                    mIpv4HeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
+        } else {
+            throw new IOException("Packet is missing neither TCP nor UDP header");
+        }
+
+        mBuffer.flip();
+        return mBuffer;
+    }
+
+    /**
+     * Allocate bytebuffer for building the packet.
+     *
+     * @param hasEther has ethernet header. Set this flag to indicate that the packet has an
+     *        ethernet header.
+     * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} currently supported.
+     * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
+     *        currently supported.
+     * @param payloadLen length of the payload.
+     */
+    @NonNull
+    public static ByteBuffer allocate(boolean hasEther, int l3proto, int l4proto, int payloadLen) {
+        if (l3proto != IPPROTO_IP) {
+            // TODO: add support for IPv6
+            throw new IllegalArgumentException("Unsupported layer 3 protocol " + l3proto);
+        }
+
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            throw new IllegalArgumentException("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        if (payloadLen < 0) {
+            throw new IllegalArgumentException("Invalid payload length " + payloadLen);
+        }
+
+        int packetLen = 0;
+        if (hasEther) packetLen += Struct.getSize(EthernetHeader.class);
+        packetLen += Struct.getSize(Ipv4Header.class);
+        packetLen += (l4proto == IPPROTO_TCP) ? Struct.getSize(TcpHeader.class)
+                : Struct.getSize(UdpHeader.class);
+        packetLen += payloadLen;
+
+        return ByteBuffer.allocate(packetLen);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index b43e2c4..d717bc7 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -518,7 +518,8 @@
     private static FieldInfo[] getClassFieldInfo(final Class clazz) {
         if (!isStructSubclass(clazz)) {
             throw new IllegalArgumentException(clazz.getName() + " is not a subclass of "
-                    + Struct.class.getName());
+                    + Struct.class.getName() + ", its superclass is "
+                    + clazz.getSuperclass().getName());
         }
 
         final FieldInfo[] cachedAnnotationFields = sFieldCache.get(clazz);
@@ -730,4 +731,32 @@
         }
         return sb.toString();
     }
+
+    /** A simple Struct which only contains a u8 field. */
+    public static class U8 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.U8)
+        public final short val;
+
+        public U8(final short val) {
+            this.val = val;
+        }
+    }
+
+    public static class U32 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.U32)
+        public final long val;
+
+        public U32(final long val) {
+            this.val = val;
+        }
+    }
+
+    public static class S64 extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.S64)
+        public final long val;
+
+        public S64(final long val) {
+            this.val = val;
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/TcUtils.java b/staticlibs/device/com/android/net/module/util/TcUtils.java
new file mode 100644
index 0000000..cf01490
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TcUtils.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import java.io.IOException;
+
+/**
+ * Contains mostly tc-related functionality.
+ */
+public class TcUtils {
+    static {
+        System.loadLibrary(JniUtil.getJniLibraryName(TcUtils.class.getPackage()));
+    }
+
+    /**
+     * Checks if the network interface uses an ethernet L2 header.
+     *
+     * @param iface the network interface.
+     * @return true if the interface uses an ethernet L2 header.
+     * @throws IOException
+     */
+    public static native boolean isEthernet(String iface) throws IOException;
+
+    /**
+     * Attach a tc bpf filter.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter add dev .. in/egress prio .. protocol ipv6/ip bpf object-pinned
+     * /sys/fs/bpf/... direct-action
+     *
+     * @param ifIndex the network interface index.
+     * @param ingress ingress or egress qdisc.
+     * @param prio
+     * @param proto
+     * @param bpfProgPath
+     * @throws IOException
+     */
+    public static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio,
+            short proto, String bpfProgPath) throws IOException;
+
+    /**
+     * Attach a tc police action.
+     *
+     * Attaches a matchall filter to the clsact qdisc with a tc police and tc bpf action attached.
+     * This causes the ingress rate to be limited and exceeding packets to be forwarded to a bpf
+     * program (specified in bpfProgPah) that accounts for the packets before dropping them.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter add dev .. ingress prio .. protocol .. matchall \
+     *     action police rate .. burst .. conform-exceed pipe/continue \
+     *     action bpf object-pinned .. \
+     *     drop
+     *
+     * @param ifIndex the network interface index.
+     * @param prio the filter preference.
+     * @param proto protocol.
+     * @param rateInBytesPerSec rate limit in bytes/s.
+     * @param bpfProgPath bpg program that accounts for rate exceeding packets before they are
+     *                    dropped.
+     * @throws IOException
+     */
+    public static native void tcFilterAddDevIngressPolice(int ifIndex, short prio, short proto,
+            int rateInBytesPerSec, String bpfProgPath) throws IOException;
+
+    /**
+     * Delete a tc filter.
+     *
+     * Equivalent to the following 'tc' command:
+     * tc filter del dev .. in/egress prio .. protocol ..
+     *
+     * @param ifIndex the network interface index.
+     * @param ingress ingress or egress qdisc.
+     * @param prio the filter preference.
+     * @param proto protocol.
+     * @throws IOException
+     */
+    public static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio,
+            short proto) throws IOException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/bpf/Tether4Key.java b/staticlibs/device/com/android/net/module/util/bpf/Tether4Key.java
new file mode 100644
index 0000000..638576f
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/bpf/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 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.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+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.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress dstMac;
+
+    @Field(order = 2, type = Type.U8, padding = 1)
+    public final short l4proto;
+
+    @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 6, type = Type.UBE16)
+    public final int dstPort;
+
+    public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort) {
+        Objects.requireNonNull(dstMac);
+
+        this.iif = iif;
+        this.dstMac = dstMac;
+        this.l4proto = l4proto;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+                            + "srcPort: %d, dstPort: %d",
+                    iif, dstMac, l4proto,
+                    Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/bpf/Tether4Value.java b/staticlibs/device/com/android/net/module/util/bpf/Tether4Value.java
new file mode 100644
index 0000000..de98766
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/bpf/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 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.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+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.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif;
+
+    // The ethhdr struct which is defined in uapi/linux/if_ether.h
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress ethDstMac;
+    @Field(order = 2, type = Type.EUI48)
+    public final MacAddress ethSrcMac;
+    @Field(order = 3, type = Type.UBE16)
+    public final int ethProto;  // Packet type ID field.
+
+    @Field(order = 4, type = Type.U16)
+    public final int pmtu;
+
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 7, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 8, type = Type.UBE16)
+    public final int dstPort;
+
+    // TODO: consider using U64.
+    @Field(order = 9, type = Type.U63)
+    public final long lastUsed;
+
+    public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+            @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+            final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPort, final long lastUsed) {
+        Objects.requireNonNull(ethDstMac);
+        Objects.requireNonNull(ethSrcMac);
+
+        this.oif = oif;
+        this.ethDstMac = ethDstMac;
+        this.ethSrcMac = ethSrcMac;
+        this.ethProto = ethProto;
+        this.pmtu = pmtu;
+        this.src46 = src46;
+        this.dst46 = dst46;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.lastUsed = lastUsed;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+                            + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+                            + "lastUsed: %d",
+                    oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+                    InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+                    lastUsed);
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", 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 a518c76..f7b0d02 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -114,9 +114,10 @@
         // and will overwrite the flags set above.
         byteBuffer.position(baseOffset);
         nlAttr = StructNlAttr.findNextAttrOfType(IFA_FLAGS, byteBuffer);
-        if (nlAttr != null) {
-            addrMsg.mFlags = nlAttr.getValueAsInt(0 /* default value */);
-        }
+        if (nlAttr == null) return null;
+        final Integer value = nlAttr.getValueAsInteger();
+        if (value == null) return null;
+        addrMsg.mFlags = value;
 
         return addrMsg;
     }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index c5efcb2..1705f1c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -22,6 +22,7 @@
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
 
+import android.annotation.SuppressLint;
 import android.net.IpPrefix;
 import android.system.OsConstants;
 
@@ -107,6 +108,7 @@
      * @param header netlink message header.
      * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
      */
+    @SuppressLint("NewApi")
     @Nullable
     public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
             @NonNull final ByteBuffer byteBuffer) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java
index f6b2e0e..8226346 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPref64.java
@@ -16,6 +16,7 @@
 
 package com.android.net.module.util.netlink;
 
+import android.annotation.SuppressLint;
 import android.net.IpPrefix;
 import android.util.Log;
 
@@ -107,6 +108,7 @@
         this.lifetime = lifetime & 0xfff8;
     }
 
+    @SuppressLint("NewApi")
     private StructNdOptPref64(@NonNull ByteBuffer buf) {
         super(buf.get(), Byte.toUnsignedInt(buf.get()));
         if (type != TYPE) throw new IllegalArgumentException("Invalid type " + type);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index 485e67c..a9b6495 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -287,14 +287,22 @@
     }
 
     /**
-     * Get attribute value as Integer.
+     * Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
      */
-    public int getValueAsInt(int defaultValue) {
+    public Integer getValueAsInteger() {
         final ByteBuffer byteBuffer = getValueAsByteBuffer();
         if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
-            return defaultValue;
+            return null;
         }
-        return getValueAsByteBuffer().getInt();
+        return byteBuffer.getInt();
+    }
+
+    /**
+     * Get attribute value as Int, default value if malformed.
+     */
+    public int getValueAsInt(int defaultValue) {
+        final Integer value = getValueAsInteger();
+        return (value != null) ? value : defaultValue;
     }
 
     /**
@@ -341,6 +349,7 @@
     public String getValueAsString() {
         if (nla_value == null) return null;
         // Check the attribute value length after removing string termination flag '\0'.
+        // This assumes that all netlink strings are null-terminated.
         if (nla_value.length < (nla_len - NLA_HEADERLEN - 1)) return null;
 
         try {
diff --git a/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java b/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java
new file mode 100644
index 0000000..5249454
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/Ipv4Header.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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 androidx.annotation.VisibleForTesting;
+
+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.net.Inet4Address;
+
+/**
+ * L3 IPv4 header as per https://tools.ietf.org/html/rfc791.
+ * This class doesn't contain options field.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |Version|  IHL  |Type of Service|          Total Length         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |         Identification        |Flags|      Fragment Offset    |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Time to Live |    Protocol   |         Header Checksum       |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                       Source Address                          |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Destination Address                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class Ipv4Header extends Struct {
+    // IP Version=IPv4, IHL is always 5(*4bytes) because options are not supported.
+    @VisibleForTesting
+    public static final byte IPHDR_VERSION_IHL = 0x45;
+
+    @Field(order = 0, type = Type.S8)
+    // version (4 bits), IHL (4 bits)
+    public final byte vi;
+    @Field(order = 1, type = Type.S8)
+    public final byte tos;
+    @Field(order = 2, type = Type.U16)
+    public final int totalLength;
+    @Field(order = 3, type = Type.S16)
+    public final short id;
+    @Field(order = 4, type = Type.S16)
+    // flags (3 bits), fragment offset (13 bits)
+    public final short flagsAndFragmentOffset;
+    @Field(order = 5, type = Type.U8)
+    public final short ttl;
+    @Field(order = 6, type = Type.S8)
+    public final byte protocol;
+    @Field(order = 7, type = Type.S16)
+    public final short checksum;
+    @Field(order = 8, type = Type.Ipv4Address)
+    public final Inet4Address srcIp;
+    @Field(order = 9, type = Type.Ipv4Address)
+    public final Inet4Address dstIp;
+
+    public Ipv4Header(final byte tos, final int totalLength, final short id,
+            final short flagsAndFragmentOffset, final short ttl, final byte protocol,
+            final short checksum, final Inet4Address srcIp, final Inet4Address dstIp) {
+        this(IPHDR_VERSION_IHL, tos, totalLength, id, flagsAndFragmentOffset, ttl,
+                protocol, checksum, srcIp, dstIp);
+    }
+
+    private Ipv4Header(final byte vi, final byte tos, final int totalLength, final short id,
+            final short flagsAndFragmentOffset, final short ttl, final byte protocol,
+            final short checksum, final Inet4Address srcIp, final Inet4Address dstIp) {
+        this.vi = vi;
+        this.tos = tos;
+        this.totalLength = totalLength;
+        this.id = id;
+        this.flagsAndFragmentOffset = flagsAndFragmentOffset;
+        this.ttl = ttl;
+        this.protocol = protocol;
+        this.checksum = checksum;
+        this.srcIp = srcIp;
+        this.dstIp = dstIp;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java b/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java
new file mode 100644
index 0000000..0c97401
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/TcpHeader.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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 com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * L4 TCP header as per https://tools.ietf.org/html/rfc793.
+ * This class does not contain option and data fields.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          Source Port          |       Destination Port        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        Sequence Number                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Acknowledgment Number                      |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Data |           |U|A|P|R|S|F|                               |
+ * | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
+ * |       |           |G|K|H|T|N|N|                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Checksum            |         Urgent Pointer        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                    Options                    |    Padding    |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                             data                              |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class TcpHeader extends Struct {
+    @Field(order = 0, type = Type.U16)
+    public final int srcPort;
+    @Field(order = 1, type = Type.U16)
+    public final int dstPort;
+    @Field(order = 2, type = Type.U32)
+    public final long seq;
+    @Field(order = 3, type = Type.U32)
+    public final long ack;
+    @Field(order = 4, type = Type.S16)
+    // data Offset (4 bits), reserved (6 bits), control bits (6 bits)
+    // TODO: update with bitfields once class Struct supports it
+    public final short dataOffsetAndControlBits;
+    @Field(order = 5, type = Type.U16)
+    public final int window;
+    @Field(order = 6, type = Type.S16)
+    public final short checksum;
+    @Field(order = 7, type = Type.U16)
+    public final int urgentPointer;
+
+    public TcpHeader(final int srcPort, final int dstPort, final long seq, final long ack,
+            final short dataOffsetAndControlBits, final int window, final short checksum,
+            final int urgentPointer) {
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.seq = seq;
+        this.ack = ack;
+        this.dataOffsetAndControlBits = dataOffsetAndControlBits;
+        this.window = window;
+        this.checksum = checksum;
+        this.urgentPointer = urgentPointer;
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java b/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java
new file mode 100644
index 0000000..8b0316b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/UdpHeader.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 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 com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * L4 UDP header as per https://tools.ietf.org/html/rfc768.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |          Source Port          |       Destination Port        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |           Length              |          Checksum             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                          data octets  ...
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ...
+ */
+public class UdpHeader extends Struct {
+    @Field(order = 0, type = Type.U16)
+    public final int srcPort;
+    @Field(order = 1, type = Type.U16)
+    public final int dstPort;
+    @Field(order = 2, type = Type.U16)
+    public final int length;
+    @Field(order = 3, type = Type.S16)
+    public final short checksum;
+
+    public UdpHeader(final int srcPort, final int dstPort, final int length,
+            final short checksum) {
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.length = length;
+        this.checksum = checksum;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BestClock.java b/staticlibs/framework/com/android/net/module/util/BestClock.java
new file mode 100644
index 0000000..35391ad
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/BestClock.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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;
+
+import android.util.Log;
+
+import java.time.Clock;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Arrays;
+
+/**
+ * Single {@link Clock} that will return the best available time from a set of
+ * prioritized {@link Clock} instances.
+ * <p>
+ * For example, when {@link SystemClock#currentNetworkTimeClock()} isn't able to
+ * provide the time, this class could use {@link Clock#systemUTC()} instead.
+ *
+ * Note that this is re-implemented based on {@code android.os.BestClock} to be used inside
+ * the mainline module. And the class does NOT support serialization.
+ *
+ * @hide
+ */
+final public class BestClock extends Clock {
+    private static final String TAG = "BestClock";
+    private final ZoneId mZone;
+    private final Clock[] mClocks;
+
+    public BestClock(ZoneId zone, Clock... clocks) {
+        super();
+        this.mZone = zone;
+        this.mClocks = clocks;
+    }
+
+    @Override
+    public long millis() {
+        for (Clock clock : mClocks) {
+            try {
+                return clock.millis();
+            } catch (DateTimeException e) {
+                // Ignore and attempt the next clock
+                Log.w(TAG, e.toString());
+            }
+        }
+        throw new DateTimeException(
+                "No clocks in " + Arrays.toString(mClocks) + " were able to provide time");
+    }
+
+    @Override
+    public ZoneId getZone() {
+        return mZone;
+    }
+
+    @Override
+    public Clock withZone(ZoneId zone) {
+        return new BestClock(zone, mClocks);
+    }
+
+    @Override
+    public Instant instant() {
+        return Instant.ofEpochMilli(millis());
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index 6e1af55..312ca48 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -180,4 +180,17 @@
         }
         return matches;
     }
+
+    /**
+     * Return sum of the given long array.
+     */
+    public static long total(@Nullable long[] array) {
+        long total = 0;
+        if (array != null) {
+            for (long value : array) {
+                total += value;
+            }
+        }
+        return total;
+    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
index 903214e..26c24f8 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -16,18 +16,24 @@
 
 package com.android.net.module.util;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_BIP;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MCX;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMTEL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
@@ -74,82 +80,46 @@
     };
 
     /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_OEM_PRIVATE = 26;
-
-    /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27;
-
-    /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
-
-    /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_ENTERPRISE = 29;
-
-    /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_VSIM
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_VSIM = 30;
-
-    /**
-     * See android.net.NetworkCapabilities.NET_CAPABILITY_BIP
-     * TODO: Use API constant when all downstream branches are S-based
-     */
-    public static final int NET_CAPABILITY_BIP = 31;
-
-
-    /**
      * Capabilities that suggest that a network is restricted.
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted},
       * and {@code FORCE_RESTRICTED_CAPABILITIES}.
      */
     @VisibleForTesting
-    static final long RESTRICTED_CAPABILITIES =
-            (1 << NET_CAPABILITY_BIP)
-            | (1 << NET_CAPABILITY_CBS)
-            | (1 << NET_CAPABILITY_DUN)
-            | (1 << NET_CAPABILITY_EIMS)
-            | (1 << NET_CAPABILITY_ENTERPRISE)
-            | (1 << NET_CAPABILITY_FOTA)
-            | (1 << NET_CAPABILITY_IA)
-            | (1 << NET_CAPABILITY_IMS)
-            | (1 << NET_CAPABILITY_MCX)
-            | (1 << NET_CAPABILITY_RCS)
-            | (1 << NET_CAPABILITY_VEHICLE_INTERNAL)
-            | (1 << NET_CAPABILITY_VSIM)
-            | (1 << NET_CAPABILITY_XCAP);
+    public static final long RESTRICTED_CAPABILITIES = packBitList(
+            NET_CAPABILITY_BIP,
+            NET_CAPABILITY_CBS,
+            NET_CAPABILITY_DUN,
+            NET_CAPABILITY_EIMS,
+            NET_CAPABILITY_ENTERPRISE,
+            NET_CAPABILITY_FOTA,
+            NET_CAPABILITY_IA,
+            NET_CAPABILITY_IMS,
+            NET_CAPABILITY_MCX,
+            NET_CAPABILITY_RCS,
+            NET_CAPABILITY_VEHICLE_INTERNAL,
+            NET_CAPABILITY_VSIM,
+            NET_CAPABILITY_XCAP,
+            NET_CAPABILITY_MMTEL);
 
     /**
      * Capabilities that force network to be restricted.
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
      */
-    private static final long FORCE_RESTRICTED_CAPABILITIES =
-            (1 << NET_CAPABILITY_ENTERPRISE)
-            | (1 << NET_CAPABILITY_OEM_PAID)
-            | (1 << NET_CAPABILITY_OEM_PRIVATE);
+    private static final long FORCE_RESTRICTED_CAPABILITIES = packBitList(
+            NET_CAPABILITY_ENTERPRISE,
+            NET_CAPABILITY_OEM_PAID,
+            NET_CAPABILITY_OEM_PRIVATE);
 
     /**
      * Capabilities that suggest that a network is unrestricted.
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
      */
     @VisibleForTesting
-    static final long UNRESTRICTED_CAPABILITIES =
-            (1 << NET_CAPABILITY_INTERNET)
-            | (1 << NET_CAPABILITY_MMS)
-            | (1 << NET_CAPABILITY_SUPL)
-            | (1 << NET_CAPABILITY_WIFI_P2P);
+    public static final long UNRESTRICTED_CAPABILITIES = packBitList(
+            NET_CAPABILITY_INTERNET,
+            NET_CAPABILITY_MMS,
+            NET_CAPABILITY_SUPL,
+            NET_CAPABILITY_WIFI_P2P);
 
     /**
      * Get a transport that can be used to classify a network when displaying its info to users.
@@ -228,7 +198,26 @@
     }
 
     /**
+     * Packs a list of ints in the same way as packBits()
+     *
+     * Each passed int is the rank of a bit that should be set in the returned long.
+     * Example : passing (1,3) will return in 0b00001010 and passing (5,6,0) will return 0b01100001
+     *
+     * @param bits bits to pack
+     * @return a long with the specified bits set.
+     */
+    public static long packBitList(int... bits) {
+        return packBits(bits);
+    }
+
+    /**
      * Packs array of bits into a long value.
+     *
+     * Each passed int is the rank of a bit that should be set in the returned long.
+     * Example : passing [1,3] will return in 0b00001010 and passing [5,6,0] will return 0b01100001
+     *
+     * @param bits bits to pack
+     * @return a long with the specified bits set.
      */
     public static long packBits(int[] bits) {
         long packed = 0;
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index f7151d7..353fe69 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -89,9 +89,11 @@
     public static final int IPV4_MAX_MTU = 65_535;
     public static final int IPV4_HEADER_MIN_LEN = 20;
     public static final int IPV4_IHL_MASK = 0xf;
+    public static final int IPV4_LENGTH_OFFSET = 2;
     public static final int IPV4_FLAGS_OFFSET = 6;
     public static final int IPV4_FRAGMENT_MASK = 0x1fff;
     public static final int IPV4_PROTOCOL_OFFSET = 9;
+    public static final int IPV4_CHECKSUM_OFFSET = 10;
     public static final int IPV4_SRC_ADDR_OFFSET = 12;
     public static final int IPV4_DST_ADDR_OFFSET = 16;
     public static final int IPV4_ADDR_LEN = 4;
@@ -164,13 +166,29 @@
     public static final byte PIO_FLAG_AUTONOMOUS = (byte) (1 << 6);
 
     /**
+     * TCP constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc793
+     */
+    public static final int TCP_HEADER_MIN_LEN = 20;
+    public static final int TCP_CHECKSUM_OFFSET = 16;
+    public static final byte TCPHDR_FIN = (byte) (1 << 0);
+    public static final byte TCPHDR_SYN = (byte) (1 << 1);
+    public static final byte TCPHDR_RST = (byte) (1 << 2);
+    public static final byte TCPHDR_PSH = (byte) (1 << 3);
+    public static final byte TCPHDR_ACK = (byte) (1 << 4);
+    public static final byte TCPHDR_URG = (byte) (1 << 5);
+
+    /**
      * UDP constants.
      *
      * See also:
      *     - https://tools.ietf.org/html/rfc768
      */
     public static final int UDP_HEADER_LEN = 8;
-
+    public static final int UDP_LENGTH_OFFSET = 4;
+    public static final int UDP_CHECKSUM_OFFSET = 6;
 
     /**
      * DHCP constants.
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
new file mode 100644
index 0000000..41a9428
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2021 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;
+
+import android.app.usage.NetworkStats;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Various utilities used for NetworkStats related code.
+ *
+ * @hide
+ */
+public class NetworkStatsUtils {
+    // These constants must be synced with the definition in android.net.NetworkStats.
+    // TODO: update to formal APIs once all downstreams have these APIs.
+    private static final int SET_ALL = -1;
+    private static final int METERED_ALL = -1;
+    private static final int ROAMING_ALL = -1;
+    private static final int DEFAULT_NETWORK_ALL = -1;
+
+    /**
+     * Safely multiple a value by a rational.
+     * <p>
+     * Internally it uses integer-based math whenever possible, but switches
+     * over to double-based math if values would overflow.
+     * @hide
+     */
+    public static long multiplySafeByRational(long value, long num, long den) {
+        if (den == 0) {
+            throw new ArithmeticException("Invalid Denominator");
+        }
+        long x = value;
+        long y = num;
+
+        // Logic shamelessly borrowed from Math.multiplyExact()
+        long r = x * y;
+        long ax = Math.abs(x);
+        long ay = Math.abs(y);
+        if (((ax | ay) >>> 31 != 0)) {
+            // Some bits greater than 2^31 that might cause overflow
+            // Check the result using the divide operator
+            // and check for the special case of Long.MIN_VALUE * -1
+            if (((y != 0) && (r / y != x))
+                    || (x == Long.MIN_VALUE && y == -1)) {
+                // Use double math to avoid overflowing
+                return (long) (((double) num / den) * value);
+            }
+        }
+        return r / den;
+    }
+
+    /**
+     * Value of the match rule of the subscriberId to match networks with specific subscriberId.
+     *
+     * @hide
+     */
+    public static final int SUBSCRIBER_ID_MATCH_RULE_EXACT = 0;
+    /**
+     * Value of the match rule of the subscriberId to match networks with any subscriberId which
+     * includes null and non-null.
+     *
+     * @hide
+     */
+    public static final int SUBSCRIBER_ID_MATCH_RULE_ALL = 1;
+
+    /**
+     * Name representing {@link #bandwidthSetGlobalAlert(long)} limit when delivered to
+     * {@link AlertObserver#onQuotaLimitReached(String, String)}.
+     */
+    public static final String LIMIT_GLOBAL_ALERT = "globalAlert";
+
+    /**
+     * Return the constrained value by given the lower and upper bounds.
+     */
+    public static int constrain(int amount, int low, int high) {
+        if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    /**
+     * Return the constrained value by given the lower and upper bounds.
+     */
+    public static long constrain(long amount, long low, long high) {
+        if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
+        return amount < low ? low : (amount > high ? high : amount);
+    }
+
+    /**
+     * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
+     */
+    public static android.net.NetworkStats fromPublicNetworkStats(
+            NetworkStats publiceNetworkStats) {
+        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
+        while (publiceNetworkStats.hasNextBucket()) {
+            NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+            publiceNetworkStats.getNextBucket(bucket);
+            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
+            stats = stats.addEntry(entry);
+        }
+        return stats;
+    }
+
+    @VisibleForTesting
+    public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
+        return new android.net.NetworkStats.Entry(
+                null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
+                convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
+                convertBucketRoaming(bucket.getRoaming()),
+                convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
+                bucket.getRxBytes(), bucket.getRxPackets(),
+                bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
+    }
+
+    private static int convertBucketState(int networkStatsSet) {
+        switch (networkStatsSet) {
+            case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
+            case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
+            case NetworkStats.Bucket.STATE_FOREGROUND:
+                return android.net.NetworkStats.SET_FOREGROUND;
+        }
+        return 0;
+    }
+
+    private static int convertBucketTag(int tag) {
+        switch (tag) {
+            case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
+        }
+        return tag;
+    }
+
+    private static int convertBucketMetered(int metered) {
+        switch (metered) {
+            case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
+            case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
+            case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketRoaming(int roaming) {
+        switch (roaming) {
+            case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
+            case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
+            case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
+        }
+        return 0;
+    }
+
+    private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
+        switch (defaultNetworkStatus) {
+            case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
+                return DEFAULT_NETWORK_ALL;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
+                return android.net.NetworkStats.DEFAULT_NETWORK_NO;
+            case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
+                return android.net.NetworkStats.DEFAULT_NETWORK_YES;
+        }
+        return 0;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 10eda57..0f3dc15 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -16,13 +16,18 @@
 
 package com.android.net.module.util;
 
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
+import android.os.Binder;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 
@@ -80,4 +85,63 @@
         permissions.add(PERMISSION_MAINLINE_NETWORK_STACK);
         enforceAnyPermissionOf(context, permissions.toArray(new String[0]));
     }
+
+    /**
+     * If the CONNECTIVITY_USE_RESTRICTED_NETWORKS is not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param message A message to include in the exception if it is thrown.
+     */
+    public static void enforceRestrictedNetworkPermission(
+            final @NonNull Context context, final @Nullable String message) {
+        context.enforceCallingOrSelfPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, message);
+    }
+
+    /**
+     * If the ACCESS_NETWORK_STATE is not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param message A message to include in the exception if it is thrown.
+     */
+    public static void enforceAccessNetworkStatePermission(
+            final @NonNull Context context, final @Nullable String message) {
+        context.enforceCallingOrSelfPermission(ACCESS_NETWORK_STATE, message);
+    }
+
+    /**
+     * Return true if the context has DUMP permission.
+     */
+    public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+        if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump " + tag + " from from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " due to missing android.permission.DUMP permission");
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Enforce that a given feature is available and if not, throw an
+     * {@link UnsupportedOperationException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param feature the feature name to enforce.
+     * @param errorMessage an optional error message to include.
+     */
+    public static void enforceSystemFeature(final @NonNull Context context,
+            final @NonNull String feature, final @Nullable String errorMessage) {
+        final boolean hasSystemFeature =
+                context.getPackageManager().hasSystemFeature(feature);
+        if (!hasSystemFeature) {
+            if (null == errorMessage) {
+                throw new UnsupportedOperationException();
+            }
+            throw new UnsupportedOperationException(errorMessage);
+        }
+    }
 }
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
index 18d19c4..1f505c4 100644
--- a/staticlibs/native/README.md
+++ b/staticlibs/native/README.md
@@ -22,6 +22,9 @@
 
 * Each module creates a native library in their directory, which statically links against the
   common native library (e.g. libnet_utils_device_common_bpf), and calls the native registered
-  function by hardcoding the post-jarjar class_name.
-
-
+  function by hardcoding the post-jarjar class_name. Linkage *MUST* be static because common
+  functions in the file (e.g., `register_com_android_net_module_util_BpfMap`) will appear in the
+  library (`.so`) file, and different versions of the library loaded in the same process by
+  different modules will in general have different versions. It's important that each of these
+  libraries loads the common function from its own library. Static linkage should guarantee this
+  because static linkage resolves symbols at build time, not runtime.
\ No newline at end of file
diff --git a/staticlibs/native/bpf_map_utils/Android.bp b/staticlibs/native/bpf_headers/Android.bp
similarity index 83%
rename from staticlibs/native/bpf_map_utils/Android.bp
rename to staticlibs/native/bpf_headers/Android.bp
index e291bbc..834ef02 100644
--- a/staticlibs/native/bpf_map_utils/Android.bp
+++ b/staticlibs/native/bpf_headers/Android.bp
@@ -12,16 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 cc_library_headers {
-    name: "bpf_map_utils",
+    name: "bpf_headers",
     vendor_available: true,
     host_supported: true,
     native_bridge_supported: true,
+    header_libs: ["bpf_syscall_wrappers"],
+    export_header_lib_headers: ["bpf_syscall_wrappers"],
     export_include_dirs: ["include"],
     cflags: [
         "-Wall",
         "-Werror",
     ],
+    sdk_version: "30",
     min_sdk_version: "30",
     apex_available: [
         "//apex_available:platform",
@@ -30,19 +37,22 @@
     ],
     visibility: [
         "//bootable/libbootloader/vts",
+        "//cts/tests/tests/net/native",
         "//frameworks/base/services/core/jni",
         "//frameworks/native/libs/cputimeinstate",
         "//frameworks/native/services/gpuservice",
         "//frameworks/native/services/gpuservice/gpumem",
         "//frameworks/native/services/gpuservice/tests/unittests",
         "//frameworks/native/services/gpuservice/tracing",
+        "//packages/modules/Connectivity/bpf_progs",
         "//packages/modules/Connectivity/netd",
+        "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/tests/unit/jni",
         "//packages/modules/DnsResolver/tests",
         "//system/bpf/bpfloader",
         "//system/bpf/libbpf_android",
         "//system/memory/libmeminfo",
-        "//system/netd/libnetdbpf",
         "//system/netd/server",
         "//system/netd/tests",
         "//system/netd/tests/benchmarks",
diff --git a/staticlibs/native/bpf_map_utils/BpfMapTest.cpp b/staticlibs/native/bpf_headers/BpfMapTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_map_utils/BpfMapTest.cpp
rename to staticlibs/native/bpf_headers/BpfMapTest.cpp
diff --git a/staticlibs/native/bpf_map_utils/TEST_MAPPING b/staticlibs/native/bpf_headers/TEST_MAPPING
similarity index 100%
rename from staticlibs/native/bpf_map_utils/TEST_MAPPING
rename to staticlibs/native/bpf_headers/TEST_MAPPING
diff --git a/staticlibs/native/bpf_map_utils/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
similarity index 100%
rename from staticlibs/native/bpf_map_utils/include/bpf/BpfMap.h
rename to staticlibs/native/bpf_headers/include/bpf/BpfMap.h
diff --git a/staticlibs/native/bpf_map_utils/include/bpf/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
similarity index 100%
rename from staticlibs/native/bpf_map_utils/include/bpf/BpfUtils.h
rename to staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
diff --git a/staticlibs/native/bpf_map_utils/include/bpf/WaitForProgsLoaded.h b/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
similarity index 100%
rename from staticlibs/native/bpf_map_utils/include/bpf/WaitForProgsLoaded.h
rename to staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
new file mode 100644
index 0000000..ac9f9bc
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -0,0 +1,226 @@
+/* Common BPF helpers to be used by all BPF programs loaded by Android */
+
+#include <linux/bpf.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_map_def.h"
+
+/******************************************************************************
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK   *
+ * DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE.             *
+ *                                                                            *
+ * THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME   *
+ *                                                                            *
+ * THAT GOES ESPECIALLY FOR THE 'SECTION' 'LICENSE' AND 'CRITICAL' MACROS     *
+ *                                                                            *
+ * We strongly suggest that if you need changes to bpfloader functionality    *
+ * you get your changes reviewed and accepted into aosp/master.               *
+ *                                                                            *
+ ******************************************************************************/
+
+/* 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.
+ *
+ * While this will work outside of mainline too, there just is no point to
+ * using it when the .o and the bpfloader ship in sync with each other.
+ */
+#ifndef BPFLOADER_MIN_VER
+#define BPFLOADER_MIN_VER DEFAULT_BPFLOADER_MIN_VER
+#endif
+
+#ifndef BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#endif
+
+/* place things in different elf sections */
+#define SECTION(NAME) __attribute__((section(NAME), used))
+
+/* Must be present in every program, example usage:
+ *   LICENSE("GPL"); or LICENSE("Apache 2.0");
+ *
+ * We also take this opportunity to embed a bunch of other useful values in
+ * the resulting .o (This is to enable some limited forward compatibility
+ * with mainline module shipped ebpf programs)
+ *
+ * The bpfloader_{min/max}_ver defines the [min, max) range of bpfloader
+ * versions that should load this .o file (bpfloaders outside of this range
+ * will simply ignore/skip this *entire* .o)
+ * The [inclusive,exclusive) matches what we do for kernel ver dependencies.
+ *
+ * The size_of_bpf_{map,prog}_def allow the bpfloader to load programs where
+ * these structures have been extended with additional fields (they will of
+ * course simply be ignored then).
+ *
+ * If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
+ * while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
+ */
+#define LICENSE(NAME)                                                                           \
+    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;           \
+    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;           \
+    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);    \
+    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
+    char _license[] SECTION("license") = (NAME)
+
+/* flag the resulting bpf .o file as critical to system functionality,
+ * loading all kernel version appropriate programs in it must succeed
+ * for bpfloader success
+ */
+#define CRITICAL(REASON) char _critical[] SECTION("critical") = (REASON)
+
+/*
+ * Helper functions called from eBPF programs written in C. These are
+ * implemented in the kernel sources.
+ */
+
+#define KVER_NONE 0
+#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
+#define KVER_INF 0xFFFFFFFFu
+
+/* generic functions */
+
+/*
+ * Type-unsafe bpf map functions - avoid if possible.
+ *
+ * Using these it is possible to pass in keys/values of the wrong type/size,
+ * or, for 'bpf_map_lookup_elem_unsafe' receive into a pointer to the wrong type.
+ * You will not get a compile time failure, and for certain types of errors you
+ * might not even get a failure from the kernel's ebpf verifier during program load,
+ * instead stuff might just not work right at runtime.
+ *
+ * Instead please use:
+ *   DEFINE_BPF_MAP(foo_map, TYPE, KeyType, ValueType, num_entries)
+ * where TYPE can be something like HASH or ARRAY, and num_entries is an integer.
+ *
+ * This defines the map (hence this should not be used in a header file included
+ * from multiple locations) and provides type safe accessors:
+ *   ValueType * bpf_foo_map_lookup_elem(const KeyType *)
+ *   int bpf_foo_map_update_elem(const KeyType *, const ValueType *, flags)
+ *   int bpf_foo_map_delete_elem(const KeyType *)
+ *
+ * This will make sure that if you change the type of a map you'll get compile
+ * errors at any spots you forget to update with the new type.
+ *
+ * Note: these all take pointers to const map because from the C/eBPF point of view
+ * the map struct is really just a readonly map definition of the in kernel object.
+ * Runtime modification of the map defining struct is meaningless, since
+ * the contents is only ever used during bpf program loading & map creation
+ * by the bpf loader, and not by the eBPF program itself.
+ */
+static void* (*bpf_map_lookup_elem_unsafe)(const struct bpf_map_def* map,
+                                           const void* key) = (void*)BPF_FUNC_map_lookup_elem;
+static int (*bpf_map_update_elem_unsafe)(const struct bpf_map_def* map, const void* key,
+                                         const void* value, unsigned long long flags) = (void*)
+        BPF_FUNC_map_update_elem;
+static int (*bpf_map_delete_elem_unsafe)(const struct bpf_map_def* map,
+                                         const void* key) = (void*)BPF_FUNC_map_delete_elem;
+
+#define BPF_ANNOTATE_KV_PAIR(name, type_key, type_val)  \
+        struct ____btf_map_##name {                     \
+                type_key key;                           \
+                type_val value;                         \
+        };                                              \
+        struct ____btf_map_##name                       \
+        __attribute__ ((section(".maps." #name), used)) \
+                ____btf_map_##name = { }
+
+/* type safe macro to declare a map and related accessor functions */
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, usr, grp, md)     \
+    const struct bpf_map_def SECTION("maps") the_map = {                                         \
+            .type = BPF_MAP_TYPE_##TYPE,                                                         \
+            .key_size = sizeof(TypeOfKey),                                                       \
+            .value_size = sizeof(TypeOfValue),                                                   \
+            .max_entries = (num_entries),                                                        \
+            .map_flags = 0,                                                                      \
+            .uid = (usr),                                                                        \
+            .gid = (grp),                                                                        \
+            .mode = (md),                                                                        \
+            .bpfloader_min_ver = DEFAULT_BPFLOADER_MIN_VER,                                      \
+            .bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER,                                      \
+            .min_kver = KVER_NONE,                                                               \
+            .max_kver = KVER_INF,                                                                \
+    };                                                                                           \
+    BPF_ANNOTATE_KV_PAIR(the_map, TypeOfKey, TypeOfValue);                                       \
+                                                                                                 \
+    static inline __always_inline __unused TypeOfValue* bpf_##the_map##_lookup_elem(             \
+            const TypeOfKey* k) {                                                                \
+        return bpf_map_lookup_elem_unsafe(&the_map, k);                                          \
+    };                                                                                           \
+                                                                                                 \
+    static inline __always_inline __unused int bpf_##the_map##_update_elem(                      \
+            const TypeOfKey* k, const TypeOfValue* v, unsigned long long flags) {                \
+        return bpf_map_update_elem_unsafe(&the_map, k, v, flags);                                \
+    };                                                                                           \
+                                                                                                 \
+    static inline __always_inline __unused int bpf_##the_map##_delete_elem(const TypeOfKey* k) { \
+        return bpf_map_delete_elem_unsafe(&the_map, k);                                          \
+    };
+
+#define DEFINE_BPF_MAP(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_ROOT, 0600)
+
+#define DEFINE_BPF_MAP_GWO(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, gid, 0620)
+
+#define DEFINE_BPF_MAP_GRO(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, gid, 0640)
+
+#define DEFINE_BPF_MAP_GRW(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, gid) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, gid, 0660)
+
+static int (*bpf_probe_read)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read;
+static int (*bpf_probe_read_str)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_str;
+static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
+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;
+static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
+static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
+static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
+
+#define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                       opt)                                                        \
+    const struct bpf_prog_def SECTION("progs") the_prog##_def = {                                  \
+            .uid = (prog_uid),                                                                     \
+            .gid = (prog_gid),                                                                     \
+            .min_kver = (min_kv),                                                                  \
+            .max_kver = (max_kv),                                                                  \
+            .optional = (opt),                                                                     \
+            .bpfloader_min_ver = DEFAULT_BPFLOADER_MIN_VER,                                        \
+            .bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER,                                        \
+    };                                                                                             \
+    SECTION(SECTION_NAME)                                                                          \
+    int the_prog
+
+// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
+// to load (for example due to missing kernel patches).
+// The bpfloader will just ignore these failures and continue processing the next section.
+//
+// A non-optional program (function/section) failing to load causes a failure and aborts
+// processing of the entire .o, if the .o is additionally marked critical, this will result
+// in the entire bpfloader process terminating with a failure and not setting the bpf.progs_loaded
+// system property.  This in turn results in waitForProgsLoaded() never finishing.
+//
+// ie. a non-optional program in a critical .o is mandatory for kernels matching the min/max kver.
+
+// programs requiring a kernel version >= min_kv && < max_kv
+#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+                                   false)
+#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
+                                            max_kv)                                             \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
+
+// programs requiring a kernel version >= min_kv
+#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)                 \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
+                                   false)
+#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv)        \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
+                                   true)
+
+// programs with no kernel version requirements
+#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
+#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
new file mode 100644
index 0000000..1371668
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2020 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
+
+/* This file is separate because it's included both by eBPF programs (via include
+ * in bpf_helpers.h) and directly by the boot time bpfloader (Loader.cpp).
+ */
+
+#include <linux/bpf.h>
+
+// Pull in AID_* constants from //system/core/libcutils/include/private/android_filesystem_config.h
+#include <cutils/android_filesystem_config.h>
+
+/******************************************************************************
+ *                                                                            *
+ *                          ! ! ! W A R N I N G ! ! !                         *
+ *                                                                            *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * MAINLINE MODULE COMPATIBILITY                                              *
+ *                                                                            *
+ * AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN     *
+ * THE FUTURE                                                                 *
+ *                                                                            *
+ * (and even in aosp/master you may only append new fields at the very end,   *
+ *  you may *never* delete fields, change their types, ordering, insert in    *
+ *  the middle, etc.  If a mainline module using the old definition has       *
+ *  already shipped (which happens roughly monthly), then it's set in stone)  *
+ *                                                                            *
+ ******************************************************************************/
+
+// These are the values used if these fields are missing
+#define DEFAULT_BPFLOADER_MIN_VER 0u        // v0.0 (this is inclusive ie. >= v0.0)
+#define DEFAULT_BPFLOADER_MAX_VER 0x10000u  // v1.0 (this is exclusive ie. < v1.0)
+#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
+
+/*
+ * The bpf_{map,prog}_def structures are compiled for different architectures.
+ * Once by the BPF compiler for the BPF architecture, and once by a C++
+ * compiler for the native Android architecture for the bpfloader.
+ *
+ * For things to work, their layout must be the same between the two.
+ * The BPF architecture is platform independent ('64-bit LSB bpf').
+ * So this effectively means these structures must be the same layout
+ * on 5 architectures, all of them little endian:
+ *   64-bit BPF, x86_64, arm  and  32-bit x86 and arm
+ *
+ * As such for any types we use inside of these structs we must make sure that
+ * the size and alignment are the same, so the same amount of padding is used.
+ *
+ * Currently we only use: bool, enum bpf_map_type and unsigned int.
+ * Additionally we use char for padding.
+ *
+ * !!! WARNING: HERE BE DRAGONS !!!
+ *
+ * Be particularly careful with 64-bit integers.
+ * You will need to manually override their alignment to 8 bytes.
+ *
+ * To quote some parts of https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69560
+ *
+ * Some types have weaker alignment requirements when they are structure members.
+ *
+ * unsigned long long on x86 is such a type.
+ *
+ * C distinguishes C11 _Alignof (the minimum alignment the type is guaranteed
+ * to have in all contexts, so 4, see min_align_of_type) from GNU C __alignof
+ * (the normal alignment of the type, so 8).
+ *
+ * alignof / _Alignof == minimum alignment required by target ABI
+ * __alignof / __alignof__ == preferred alignment
+ *
+ * When in a struct, apparently the minimum alignment is used.
+ */
+
+_Static_assert(sizeof(bool) == 1, "sizeof bool != 1");
+_Static_assert(__alignof__(bool) == 1, "__alignof__ bool != 1");
+_Static_assert(_Alignof(bool) == 1, "_Alignof bool != 1");
+
+_Static_assert(sizeof(char) == 1, "sizeof char != 1");
+_Static_assert(__alignof__(char) == 1, "__alignof__ char != 1");
+_Static_assert(_Alignof(char) == 1, "_Alignof char != 1");
+
+// This basically verifies that an enum is 'just' a 32-bit int
+_Static_assert(sizeof(enum bpf_map_type) == 4, "sizeof enum bpf_map_type != 4");
+_Static_assert(__alignof__(enum bpf_map_type) == 4, "__alignof__ enum bpf_map_type != 4");
+_Static_assert(_Alignof(enum bpf_map_type) == 4, "_Alignof enum bpf_map_type != 4");
+
+// Linux kernel requires sizeof(int) == 4, sizeof(void*) == sizeof(long), sizeof(long long) == 8
+_Static_assert(sizeof(unsigned int) == 4, "sizeof unsigned int != 4");
+_Static_assert(__alignof__(unsigned int) == 4, "__alignof__ unsigned int != 4");
+_Static_assert(_Alignof(unsigned int) == 4, "_Alignof unsigned int != 4");
+
+// We don't currently use any 64-bit types in these structs, so this is purely to document issue.
+// Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
+_Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
+_Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
+// BPF wants 8, but 32-bit x86 wants 4
+//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+
+/*
+ * Map structure to be used by Android eBPF C programs. The Android eBPF loader
+ * uses this structure from eBPF object to create maps at boot time.
+ *
+ * The eBPF C program should define structure in the maps section using
+ * SECTION("maps") otherwise it will be ignored by the eBPF loader.
+ *
+ * For example:
+ *   const struct bpf_map_def SECTION("maps") mymap { .type=... , .key_size=... }
+ *
+ * See 'bpf_helpers.h' for helpful macros for eBPF program use.
+ */
+struct bpf_map_def {
+    enum bpf_map_type type;
+    unsigned int key_size;
+    unsigned int value_size;
+    unsigned int max_entries;
+    unsigned int map_flags;
+
+    // The following are not supported by the Android bpfloader:
+    //   unsigned int inner_map_idx;
+    //   unsigned int numa_node;
+
+    unsigned int uid;   // uid_t
+    unsigned int gid;   // gid_t
+    unsigned int mode;  // mode_t
+
+    // The following fields were added in version 0.1
+    unsigned int bpfloader_min_ver;  // if missing, defaults to 0, ie. v0.0
+    unsigned int bpfloader_max_ver;  // if missing, defaults to 0x10000, ie. v1.0
+
+    // The following fields were added in version 0.2
+    // kernelVersion() must be >= min_kver and < max_kver
+    unsigned int min_kver;
+    unsigned int max_kver;
+};
+
+// This needs to be updated whenever the above structure definition is expanded.
+_Static_assert(sizeof(struct bpf_map_def) == 48, "sizeof struct bpf_map_def != 48");
+_Static_assert(__alignof__(struct bpf_map_def) == 4, "__alignof__ struct bpf_map_def != 4");
+_Static_assert(_Alignof(struct bpf_map_def) == 4, "_Alignof struct bpf_map_def != 4");
+
+struct bpf_prog_def {
+    unsigned int uid;
+    unsigned int gid;
+
+    // kernelVersion() must be >= min_kver and < max_kver
+    unsigned int min_kver;
+    unsigned int max_kver;
+
+    bool optional;  // program section (ie. function) may fail to load, continue onto next func.
+    char pad0[3];
+
+    // The following fields were added in version 0.1
+    unsigned int bpfloader_min_ver;  // if missing, defaults to 0, ie. v0.0
+    unsigned int bpfloader_max_ver;  // if missing, defaults to 0x10000, ie. v1.0
+
+    // No new fields in version 0.2
+};
+
+// This needs to be updated whenever the above structure definition is expanded.
+_Static_assert(sizeof(struct bpf_prog_def) == 28, "sizeof struct bpf_prog_def != 28");
+_Static_assert(__alignof__(struct bpf_prog_def) == 4, "__alignof__ struct bpf_prog_def != 4");
+_Static_assert(_Alignof(struct bpf_prog_def) == 4, "_Alignof struct bpf_prog_def != 4");
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index 037e10d..a20eed3 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -18,8 +18,8 @@
 
 cc_library_headers {
     name: "bpf_syscall_wrappers",
-    vendor_available: false,
-    host_supported: false,
+    vendor_available: true,
+    host_supported: true,
     native_bridge_supported: true,
     export_include_dirs: ["include"],
     cflags: [
@@ -34,9 +34,13 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//frameworks/libs/net/common/native/bpf_headers",
         "//frameworks/libs/net/common/native/bpfmapjni",
+        "//frameworks/libs/net/common/native/tcutils",
         "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
+        "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/providers/MediaProvider/jni",
         "//system/bpf/libbpf_android",
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index b7af22d..cd254d4 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -18,7 +18,10 @@
 
 cc_library_static {
     name: "libnet_utils_device_common_bpfjni",
-    srcs: ["com_android_net_module_util_BpfMap.cpp"],
+    srcs: [
+        "com_android_net_module_util_BpfMap.cpp",
+        "com_android_net_module_util_TcUtils.cpp",
+    ],
     header_libs: [
         "bpf_syscall_wrappers",
         "jni_headers",
@@ -27,6 +30,9 @@
         "liblog",
         "libnativehelper_compat_libc++",
     ],
+    whole_static_libs: [
+        "libtcutils",
+    ],
     cflags: [
         "-Wall",
         "-Werror",
@@ -40,5 +46,7 @@
     ],
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
+        // TODO: remove after NetworkStatsService moves to the module.
+        "//frameworks/base/packages/ConnectivityT/service",
     ],
 }
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
new file mode 100644
index 0000000..2307a6b
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+#include <tcutils/tcutils.h>
+
+namespace android {
+
+static void throwIOException(JNIEnv *env, const char *msg, int error) {
+  jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg,
+                       strerror(error));
+}
+
+static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env,
+                                                               jobject clazz,
+                                                               jstring iface) {
+  ScopedUtfChars interface(env, iface);
+  bool result = false;
+  int error = isEthernet(interface.c_str(), result);
+  if (error) {
+    throwIOException(
+        env, "com_android_net_module_util_TcUtils_isEthernet error: ", error);
+  }
+  // result is not touched when error is returned; leave false.
+  return result;
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
+// /sys/fs/bpf/... direct-action
+static void com_android_net_module_util_TcUtils_tcFilterAddDevBpf(
+    JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+    jshort proto, jstring bpfProgPath) {
+  ScopedUtfChars pathname(env, bpfProgPath);
+  int error = tcAddBpfFilter(ifIndex, ingress, prio, proto, pathname.c_str());
+  if (error) {
+    throwIOException(
+        env,
+        "com_android_net_module_util_TcUtils_tcFilterAddDevBpf error: ", error);
+  }
+}
+
+// tc filter add dev .. ingress prio .. protocol .. matchall \
+//     action police rate .. burst .. conform-exceed pipe/continue \
+//     action bpf object-pinned .. \
+//     drop
+static void com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice(
+    JNIEnv *env, jobject clazz, jint ifIndex, jshort prio, jshort proto,
+    jint rateInBytesPerSec, jstring bpfProgPath) {
+  ScopedUtfChars pathname(env, bpfProgPath);
+  int error = tcAddIngressPoliceFilter(ifIndex, prio, proto, rateInBytesPerSec,
+                                       pathname.c_str());
+  if (error) {
+    throwIOException(env,
+                     "com_android_net_module_util_TcUtils_"
+                     "tcFilterAddDevIngressPolice error: ",
+                     error);
+  }
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+static void com_android_net_module_util_TcUtils_tcFilterDelDev(
+    JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+    jshort proto) {
+  int error = tcDeleteFilter(ifIndex, ingress, prio, proto);
+  if (error) {
+    throwIOException(
+        env,
+        "com_android_net_module_util_TcUtils_tcFilterDelDev error: ", error);
+  }
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    {"isEthernet", "(Ljava/lang/String;)Z",
+     (void *)com_android_net_module_util_TcUtils_isEthernet},
+    {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V",
+     (void *)com_android_net_module_util_TcUtils_tcFilterAddDevBpf},
+    {"tcFilterAddDevIngressPolice", "(ISSILjava/lang/String;)V",
+     (void *)com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice},
+    {"tcFilterDelDev", "(IZSS)V",
+     (void *)com_android_net_module_util_TcUtils_tcFilterDelDev},
+};
+
+int register_com_android_net_module_util_TcUtils(JNIEnv *env,
+                                                 char const *class_name) {
+  return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/ip_checksum/Android.bp b/staticlibs/native/ip_checksum/Android.bp
new file mode 100644
index 0000000..9878d73
--- /dev/null
+++ b/staticlibs/native/ip_checksum/Android.bp
@@ -0,0 +1,46 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libip_checksum",
+
+    srcs: [
+        "checksum.c",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+
+    export_include_dirs: ["."],
+
+    // Needed because libnetutils depends on libip_checksum, and libnetutils has
+    // vendor_available = true. Making this library vendor_available does not create any maintenance
+    // burden or version skew issues because this library is only static, not dynamic, and thus is
+    // not installed on the device.
+    //
+    // TODO: delete libnetutils from the VNDK in T, and remove this.
+    vendor_available: true,
+
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+}
diff --git a/staticlibs/native/ip_checksum/checksum.c b/staticlibs/native/ip_checksum/checksum.c
new file mode 100644
index 0000000..04217a7
--- /dev/null
+++ b/staticlibs/native/ip_checksum/checksum.c
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * checksum.c - ipv4/ipv6 checksum calculation
+ */
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+#include "checksum.h"
+
+/* function: ip_checksum_add
+ * adds data to a checksum. only known to work on little-endian hosts
+ * current - the current checksum (or 0 to start a new checksum)
+ *   data        - the data to add to the checksum
+ *   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;
+        data_16++;
+        left -= 2;
+    }
+    if (left) {
+        checksum += *(uint8_t*)data_16;
+    }
+
+    return checksum;
+}
+
+/* function: ip_checksum_fold
+ * folds a 32-bit partial checksum into 16 bits
+ *   temp_sum - sum from ip_checksum_add
+ *   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);
+    }
+    return temp_sum;
+}
+
+/* function: ip_checksum_finish
+ * folds and closes the checksum
+ *   temp_sum - sum from ip_checksum_add
+ *   returns: a header checksum value in network byte order
+ */
+uint16_t ip_checksum_finish(uint32_t temp_sum) {
+    return ~ip_checksum_fold(temp_sum);
+}
+
+/* function: ip_checksum
+ * combined ip_checksum_add and ip_checksum_finish
+ *   data - data to checksum
+ *   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);
+}
+
+/* function: ipv6_pseudo_header_checksum
+ * calculate the pseudo header checksum for use in tcp/udp/icmp headers
+ *   ip6      - the ipv6 header
+ *   len      - the transport length (transport header + payload)
+ *   protocol - the transport layer protocol, can be different from ip6->ip6_nxt for fragments
+ */
+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));
+    current = ip_checksum_add(current, &(ip6->ip6_dst), sizeof(struct in6_addr));
+    current = ip_checksum_add(current, &checksum_len, sizeof(checksum_len));
+    current = ip_checksum_add(current, &checksum_next, sizeof(checksum_next));
+
+    return current;
+}
+
+/* function: ipv4_pseudo_header_checksum
+ * calculate the pseudo header checksum for use in tcp/udp headers
+ *   ip      - the ipv4 header
+ *   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);
+
+    uint32_t current = 0;
+
+    current = ip_checksum_add(current, &(ip->saddr), sizeof(uint32_t));
+    current = ip_checksum_add(current, &(ip->daddr), sizeof(uint32_t));
+    current = ip_checksum_add(current, &temp_protocol, sizeof(uint16_t));
+    current = ip_checksum_add(current, &temp_length, sizeof(uint16_t));
+
+    return current;
+}
+
+/* function: ip_checksum_adjust
+ * calculates a new checksum given a previous checksum and the old and new pseudo-header checksums
+ *   checksum    - the header checksum in the original packet in network byte order
+ *   old_hdr_sum - the pseudo-header checksum of the original packet
+ *   new_hdr_sum - the pseudo-header checksum of the translated packet
+ *   returns: the new header checksum in network byte order
+ */
+uint16_t ip_checksum_adjust(uint16_t checksum, uint32_t old_hdr_sum, uint32_t new_hdr_sum) {
+    // 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_old = ip_checksum_fold(old_hdr_sum);
+    if (folded_sum > folded_old) {
+        return ~(folded_sum - folded_old);
+    } else {
+        return ~(folded_sum - folded_old - 1);  // end-around borrow
+    }
+}
diff --git a/staticlibs/native/ip_checksum/checksum.h b/staticlibs/native/ip_checksum/checksum.h
new file mode 100644
index 0000000..868217c
--- /dev/null
+++ b/staticlibs/native/ip_checksum/checksum.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 Daniel Drown
+ *
+ * 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.
+ *
+ * checksum.h - checksum functions
+ */
+#ifndef __CHECKSUM_H__
+#define __CHECKSUM_H__
+
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <stdint.h>
+
+uint32_t ip_checksum_add(uint32_t current, const void* data, int len);
+uint16_t ip_checksum_finish(uint32_t temp_sum);
+uint16_t ip_checksum(const void* data, int len);
+
+uint32_t ipv6_pseudo_header_checksum(const struct ip6_hdr* ip6, uint32_t len, uint8_t protocol);
+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/native/nettestutils/Android.bp b/staticlibs/native/nettestutils/Android.bp
new file mode 100644
index 0000000..42df8e0
--- /dev/null
+++ b/staticlibs/native/nettestutils/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnettestutils",
+    export_include_dirs: ["include"],
+    srcs: ["DumpService.cpp"],
+
+    shared_libs: [
+        "libbinder",
+        "libutils",
+    ],
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+}
diff --git a/staticlibs/native/nettestutils/DumpService.cpp b/staticlibs/native/nettestutils/DumpService.cpp
new file mode 100644
index 0000000..ba3d77e
--- /dev/null
+++ b/staticlibs/native/nettestutils/DumpService.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#include "nettestutils/DumpService.h"
+
+#include <android-base/file.h>
+
+#include <sstream>
+#include <thread>
+
+android::status_t dumpService(const android::sp<android::IBinder>& binder,
+                              const std::vector<std::string>& args,
+                              std::vector<std::string>& outputLines) {
+  if (!outputLines.empty()) return -EUCLEAN;
+
+  android::base::unique_fd localFd, remoteFd;
+  if (!Pipe(&localFd, &remoteFd)) return -errno;
+
+  android::Vector<android::String16> str16Args;
+  for (const auto& arg : args) {
+    str16Args.push(android::String16(arg.c_str()));
+  }
+  android::status_t ret;
+  // dump() blocks until another thread has consumed all its output.
+  std::thread dumpThread =
+      std::thread([&ret, binder, remoteFd{std::move(remoteFd)}, str16Args]() {
+        ret = binder->dump(remoteFd, str16Args);
+      });
+
+  std::string dumpContent;
+  if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
+    return -errno;
+  }
+  dumpThread.join();
+  if (ret != android::OK) return ret;
+
+  std::stringstream dumpStream(std::move(dumpContent));
+  std::string line;
+  while (std::getline(dumpStream, line)) {
+    outputLines.push_back(line);
+  }
+
+  return android::OK;
+}
diff --git a/staticlibs/native/nettestutils/include/nettestutils/DumpService.h b/staticlibs/native/nettestutils/include/nettestutils/DumpService.h
new file mode 100644
index 0000000..2a72181
--- /dev/null
+++ b/staticlibs/native/nettestutils/include/nettestutils/DumpService.h
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+#include <binder/Binder.h>
+
+#include <vector>
+
+android::status_t dumpService(const android::sp<android::IBinder>& binder,
+                              const std::vector<std::string>& args,
+                              std::vector<std::string>& outputLines);
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
new file mode 100644
index 0000000..e819e4c
--- /dev/null
+++ b/staticlibs/native/tcutils/Android.bp
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libtcutils",
+    srcs: ["tcutils.cpp"],
+    export_include_dirs: ["include"],
+    header_libs: ["bpf_syscall_wrappers"],
+    shared_libs: [
+        "liblog",
+    ],
+    stl: "libc++_static",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    sdk_version: "30",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//frameworks/libs/net/common/native/bpfmapjni",
+        "//packages/modules/Connectivity:__subpackages__",
+        "//system/netd/server",
+    ],
+}
+
+cc_test {
+    name: "libtcutils_test",
+    srcs: [
+        "tests/tcutils_test.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-error=unused-variable",
+    ],
+    header_libs: ["bpf_syscall_wrappers"],
+    static_libs: [
+        "libgmock",
+        "libtcutils",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    min_sdk_version: "30",
+    require_root: true,
+    test_suites: ["general-tests"],
+}
diff --git a/staticlibs/native/tcutils/include/tcutils/tcutils.h b/staticlibs/native/tcutils/include/tcutils/tcutils.h
new file mode 100644
index 0000000..a8ec2e8
--- /dev/null
+++ b/staticlibs/native/tcutils/include/tcutils/tcutils.h
@@ -0,0 +1,47 @@
+/*
+ * 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 <cstdint>
+#include <linux/rtnetlink.h>
+
+namespace android {
+
+int isEthernet(const char *iface, bool &isEthernet);
+
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
+
+static inline int tcAddQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
+}
+
+static inline int tcReplaceQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
+}
+
+static inline int tcDeleteQdiscClsact(int ifIndex) {
+  return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
+}
+
+int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto,
+                   const char *bpfProgPath);
+int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto,
+                             unsigned rateInBytesPerSec,
+                             const char *bpfProgPath);
+int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
+
+} // namespace android
diff --git a/staticlibs/native/tcutils/kernelversion.h b/staticlibs/native/tcutils/kernelversion.h
new file mode 100644
index 0000000..3be1ad2
--- /dev/null
+++ b/staticlibs/native/tcutils/kernelversion.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// -----------------------------------------------------------------------------
+// TODO - This should be replaced with BpfUtils in bpf_headers.
+// Currently, bpf_headers contains a bunch requirements it doesn't actually provide, such as a
+// non-ndk liblog version, and some version of libbase. libtcutils does not have access to either of
+// these, so I think this will have to wait until we figure out a way around this.
+//
+// In the mean time copying verbatim from:
+//   frameworks/libs/net/common/native/bpf_headers
+
+#pragma once
+
+#include <stdio.h>
+#include <sys/utsname.h>
+
+#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
+
+namespace android {
+
+static inline unsigned kernelVersion() {
+  struct utsname buf;
+  int ret = uname(&buf);
+  if (ret)
+    return 0;
+
+  unsigned kver_major;
+  unsigned kver_minor;
+  unsigned kver_sub;
+  char discard;
+  ret = sscanf(buf.release, "%u.%u.%u%c", &kver_major, &kver_minor, &kver_sub,
+               &discard);
+  // Check the device kernel version
+  if (ret < 3)
+    return 0;
+
+  return KVER(kver_major, kver_minor, kver_sub);
+}
+
+static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor,
+                                          unsigned sub) {
+  return kernelVersion() >= KVER(major, minor, sub);
+}
+
+} // namespace android
diff --git a/staticlibs/native/tcutils/logging.h b/staticlibs/native/tcutils/logging.h
new file mode 100644
index 0000000..70604b3
--- /dev/null
+++ b/staticlibs/native/tcutils/logging.h
@@ -0,0 +1,35 @@
+/*
+ * 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/log.h>
+#include <stdarg.h>
+
+#ifndef LOG_TAG
+#define LOG_TAG "TcUtils_Undef"
+#endif
+
+namespace android {
+
+static inline void ALOGE(const char *fmt...) {
+  va_list args;
+  va_start(args, fmt);
+  __android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args);
+  va_end(args);
+}
+
+}
diff --git a/staticlibs/native/tcutils/scopeguard.h b/staticlibs/native/tcutils/scopeguard.h
new file mode 100644
index 0000000..76bbb93
--- /dev/null
+++ b/staticlibs/native/tcutils/scopeguard.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// -----------------------------------------------------------------------------
+// TODO: figure out a way to use libbase_ndk. This is currently not working
+// because of missing apex availability. For now, we can use a copy of
+// ScopeGuard which is very lean compared to unique_fd. This code has been
+// copied verbatim from:
+// https://cs.android.com/android/platform/superproject/+/master:system/libbase/include/android-base/scopeguard.h
+
+#pragma once
+
+#include <utility> // for std::move, std::forward
+
+namespace android {
+namespace base {
+
+// ScopeGuard ensures that the specified functor is executed no matter how the
+// current scope exits.
+template <typename F> class ScopeGuard {
+public:
+  ScopeGuard(F &&f) : f_(std::forward<F>(f)), active_(true) {}
+
+  ScopeGuard(ScopeGuard &&that) noexcept
+      : f_(std::move(that.f_)), active_(that.active_) {
+    that.active_ = false;
+  }
+
+  template <typename Functor>
+  ScopeGuard(ScopeGuard<Functor> &&that)
+      : f_(std::move(that.f_)), active_(that.active_) {
+    that.active_ = false;
+  }
+
+  ~ScopeGuard() {
+    if (active_)
+      f_();
+  }
+
+  ScopeGuard() = delete;
+  ScopeGuard(const ScopeGuard &) = delete;
+  void operator=(const ScopeGuard &) = delete;
+  void operator=(ScopeGuard &&that) = delete;
+
+  void Disable() { active_ = false; }
+
+  bool active() const { return active_; }
+
+private:
+  template <typename Functor> friend class ScopeGuard;
+
+  F f_;
+  bool active_;
+};
+
+template <typename F> ScopeGuard<F> make_scope_guard(F &&f) {
+  return ScopeGuard<F>(std::forward<F>(f));
+}
+
+} // namespace base
+} // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
new file mode 100644
index 0000000..0e17f67
--- /dev/null
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -0,0 +1,736 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "TcUtils"
+
+#include "tcutils/tcutils.h"
+
+#include "logging.h"
+#include "kernelversion.h"
+#include "scopeguard.h"
+
+#include <arpa/inet.h>
+#include <cerrno>
+#include <cstring>
+#include <libgen.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
+#include <linux/tc_act/tc_bpf.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <utility>
+
+#define BPF_FD_JUST_USE_INT
+#include <BpfSyscallWrappers.h>
+#undef BPF_FD_JUST_USE_INT
+
+// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
+#define CLS_BPF_NAME_LEN 256
+
+// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+#define CLS_BPF_KIND_NAME "bpf"
+
+namespace android {
+namespace {
+
+/**
+ * IngressPoliceFilterBuilder builds a nlmsg request equivalent to the following
+ * tc command:
+ *
+ * tc filter add dev .. ingress prio .. protocol .. matchall \
+ *     action police rate .. burst .. conform-exceed pipe/continue \
+ *     action bpf object-pinned .. \
+ *     drop
+ */
+class IngressPoliceFilterBuilder final {
+  // default mtu is 2047, so the cell logarithm factor (cell_log) is 3.
+  // 0x7FF >> 0x3FF x 2^1 >> 0x1FF x 2^2 >> 0xFF x 2^3
+  static constexpr int RTAB_CELL_LOGARITHM = 3;
+  static constexpr size_t RTAB_SIZE = 256;
+  static constexpr unsigned TIME_UNITS_PER_SEC = 1000000;
+
+  struct Request {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      char str[NLMSG_ALIGN(sizeof("matchall"))];
+    } kind;
+    struct {
+      nlattr attr;
+      struct {
+        nlattr attr;
+        struct {
+          nlattr attr;
+          struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(sizeof("police"))];
+          } kind;
+          struct {
+            nlattr attr;
+            struct {
+              nlattr attr;
+              struct tc_police obj;
+            } police;
+            struct {
+              nlattr attr;
+              uint32_t u32[RTAB_SIZE];
+            } rtab;
+            struct {
+              nlattr attr;
+              int32_t s32;
+            } notexceedact;
+          } opt;
+        } act1;
+        struct {
+          nlattr attr;
+          struct {
+            nlattr attr;
+            char str[NLMSG_ALIGN(sizeof("bpf"))];
+          } kind;
+          struct {
+            nlattr attr;
+            struct {
+              nlattr attr;
+              uint32_t u32;
+            } fd;
+            struct {
+              nlattr attr;
+              char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+            } name;
+            struct {
+              nlattr attr;
+              struct tc_act_bpf obj;
+            } parms;
+          } opt;
+        } act2;
+      } acts;
+    } opt;
+  };
+
+  // class members
+  const unsigned mBurstInBytes;
+  const char *mBpfProgPath;
+  int mBpfFd;
+  Request mRequest;
+
+  static double getTickInUsec() {
+    FILE *fp = fopen("/proc/net/psched", "re");
+    if (!fp) {
+      ALOGE("fopen(\"/proc/net/psched\"): %s", strerror(errno));
+      return 0.0;
+    }
+    auto scopeGuard = base::make_scope_guard([fp] { fclose(fp); });
+
+    uint32_t t2us;
+    uint32_t us2t;
+    uint32_t clockRes;
+    const bool isError =
+        fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clockRes) != 3;
+
+    if (isError) {
+      ALOGE("fscanf(/proc/net/psched, \"%%08x%%08x%%08x\"): %s",
+               strerror(errno));
+      return 0.0;
+    }
+
+    const double clockFactor =
+        static_cast<double>(clockRes) / TIME_UNITS_PER_SEC;
+    return static_cast<double>(t2us) / static_cast<double>(us2t) * clockFactor;
+  }
+
+  static inline const double kTickInUsec = getTickInUsec();
+
+public:
+  // clang-format off
+  IngressPoliceFilterBuilder(int ifIndex, uint16_t prio, uint16_t proto, unsigned rateInBytesPerSec,
+                      unsigned burstInBytes, const char* bpfProgPath)
+      : mBurstInBytes(burstInBytes),
+        mBpfProgPath(bpfProgPath),
+        mBpfFd(-1),
+        mRequest{
+            .n = {
+                .nlmsg_len = sizeof(mRequest),
+                .nlmsg_type = RTM_NEWTFILTER,
+                .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE,
+            },
+            .t = {
+                .tcm_family = AF_UNSPEC,
+                .tcm_ifindex = ifIndex,
+                .tcm_handle = TC_H_UNSPEC,
+                .tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS),
+                .tcm_info = (static_cast<uint32_t>(prio) << 16)
+                            | static_cast<uint32_t>(htons(proto)),
+            },
+            .kind = {
+                .attr = {
+                    .nla_len = sizeof(mRequest.kind),
+                    .nla_type = TCA_KIND,
+                },
+                .str = "matchall",
+            },
+            .opt = {
+                .attr = {
+                    .nla_len = sizeof(mRequest.opt),
+                    .nla_type = TCA_OPTIONS,
+                },
+                .acts = {
+                    .attr = {
+                        .nla_len = sizeof(mRequest.opt.acts),
+                        .nla_type = TCA_U32_ACT,
+                    },
+                    .act1 = {
+                        .attr = {
+                            .nla_len = sizeof(mRequest.opt.acts.act1),
+                            .nla_type = 1, // action priority
+                        },
+                        .kind = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act1.kind),
+                                .nla_type = TCA_ACT_KIND,
+                            },
+                            .str = "police",
+                        },
+                        .opt = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act1.opt),
+                                .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED,
+                            },
+                            .police = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.police),
+                                    .nla_type = TCA_POLICE_TBF,
+                                },
+                                .obj = {
+                                    .action = TC_ACT_PIPE,
+                                    .burst = 0,
+                                    .rate = {
+                                        .cell_log = RTAB_CELL_LOGARITHM,
+                                        .linklayer = TC_LINKLAYER_ETHERNET,
+                                        .cell_align = -1,
+                                        .rate = rateInBytesPerSec,
+                                    },
+                                },
+                            },
+                            .rtab = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.rtab),
+                                    .nla_type = TCA_POLICE_RATE,
+                                },
+                                .u32 = {},
+                            },
+                            .notexceedact = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act1.opt.notexceedact),
+                                    .nla_type = TCA_POLICE_RESULT,
+                                },
+                                .s32 = TC_ACT_UNSPEC,
+                            },
+                        },
+                    },
+                    .act2 = {
+                        .attr = {
+                            .nla_len = sizeof(mRequest.opt.acts.act2),
+                            .nla_type = 2, // action priority
+                        },
+                        .kind = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act2.kind),
+                                .nla_type = TCA_ACT_KIND,
+                            },
+                            .str = "bpf",
+                        },
+                        .opt = {
+                            .attr = {
+                                .nla_len = sizeof(mRequest.opt.acts.act2.opt),
+                                .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED,
+                            },
+                            .fd = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.fd),
+                                    .nla_type = TCA_ACT_BPF_FD,
+                                },
+                                .u32 = 0, // set during build()
+                            },
+                            .name = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.name),
+                                    .nla_type = TCA_ACT_BPF_NAME,
+                                },
+                                .str = "placeholder",
+                            },
+                            .parms = {
+                                .attr = {
+                                    .nla_len = sizeof(mRequest.opt.acts.act2.opt.parms),
+                                    .nla_type = TCA_ACT_BPF_PARMS,
+                                },
+                                .obj = {
+                                    // default action to be executed when bpf prog
+                                    // returns TC_ACT_UNSPEC.
+                                    .action = TC_ACT_SHOT,
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        } {
+      // constructor body
+  }
+  // clang-format on
+
+  ~IngressPoliceFilterBuilder() {
+    // TODO: use unique_fd
+    if (mBpfFd != -1) {
+      close(mBpfFd);
+    }
+  }
+
+  constexpr unsigned getRequestSize() const { return sizeof(Request); }
+
+private:
+  unsigned calculateXmitTime(unsigned size) {
+    const uint32_t rate = mRequest.opt.acts.act1.opt.police.obj.rate.rate;
+    return (static_cast<double>(size) / static_cast<double>(rate)) *
+           TIME_UNITS_PER_SEC * kTickInUsec;
+  }
+
+  void initBurstRate() {
+    mRequest.opt.acts.act1.opt.police.obj.burst =
+        calculateXmitTime(mBurstInBytes);
+  }
+
+  // Calculates a table with 256 transmission times for different packet sizes
+  // (all the way up to MTU). RTAB_CELL_LOGARITHM is used as a scaling factor.
+  // In this case, MTU size is always 2048, so RTAB_CELL_LOGARITHM is always
+  // 3. Therefore, this function generates the transmission times for packets
+  // of size 1..256 x 2^3.
+  void initRateTable() {
+    for (unsigned i = 0; i < RTAB_SIZE; ++i) {
+      unsigned adjustedSize = (i + 1) << RTAB_CELL_LOGARITHM;
+      mRequest.opt.acts.act1.opt.rtab.u32[i] = calculateXmitTime(adjustedSize);
+    }
+  }
+
+  int initBpfFd() {
+    mBpfFd = bpf::retrieveProgram(mBpfProgPath);
+    if (mBpfFd == -1) {
+      int error = errno;
+      ALOGE("retrieveProgram failed: %d", error);
+      return -error;
+    }
+
+    mRequest.opt.acts.act2.opt.fd.u32 = static_cast<uint32_t>(mBpfFd);
+    snprintf(mRequest.opt.acts.act2.opt.name.str,
+             sizeof(mRequest.opt.acts.act2.opt.name.str), "%s:[*fsobj]",
+             basename(mBpfProgPath));
+
+    return 0;
+  }
+
+public:
+  int build() {
+    if (kTickInUsec == 0.0) {
+      return -EINVAL;
+    }
+
+    initBurstRate();
+    initRateTable();
+    return initBpfFd();
+  }
+
+  const Request *getRequest() const {
+    // Make sure to call build() before calling this function. Otherwise, the
+    // request will be invalid.
+    return &mRequest;
+  }
+};
+
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+
+int sendAndProcessNetlinkResponse(const void *req, int len) {
+  // TODO: use unique_fd instead of ScopeGuard
+  int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+  if (fd == -1) {
+    int error = errno;
+    ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d",
+             error);
+    return -error;
+  }
+  auto scopeGuard = base::make_scope_guard([fd] { close(fd); });
+
+  static constexpr int on = 1;
+  if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
+    int error = errno;
+    ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error);
+    return -error;
+  }
+
+  // this is needed to get valid strace netlink parsing, it allocates the pid
+  if (bind(fd, (const struct sockaddr *)&KERNEL_NLADDR,
+           sizeof(KERNEL_NLADDR))) {
+    int error = errno;
+    ALOGE("bind(fd, {AF_NETLINK, 0, 0}: %d)", error);
+    return -error;
+  }
+
+  // we do not want to receive messages from anyone besides the kernel
+  if (connect(fd, (const struct sockaddr *)&KERNEL_NLADDR,
+              sizeof(KERNEL_NLADDR))) {
+    int error = errno;
+    ALOGE("connect(fd, {AF_NETLINK, 0, 0}): %d", error);
+    return -error;
+  }
+
+  int rv = send(fd, req, len, 0);
+
+  if (rv == -1) {
+    int error = errno;
+    ALOGE("send(fd, req, len, 0) failed: %d", error);
+    return -error;
+  }
+
+  if (rv != len) {
+    ALOGE("send(fd, req, len = %d, 0) returned invalid message size %d", len,
+             rv);
+    return -EMSGSIZE;
+  }
+
+  struct {
+    nlmsghdr h;
+    nlmsgerr e;
+    char buf[256];
+  } resp = {};
+
+  rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+  if (rv == -1) {
+    int error = errno;
+    ALOGE("recv() failed: %d", error);
+    return -error;
+  }
+
+  if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+    ALOGE("recv() returned short packet: %d", rv);
+    return -EBADMSG;
+  }
+
+  if (resp.h.nlmsg_len != (unsigned)rv) {
+    ALOGE("recv() returned invalid header length: %d != %d",
+             resp.h.nlmsg_len, rv);
+    return -EBADMSG;
+  }
+
+  if (resp.h.nlmsg_type != NLMSG_ERROR) {
+    ALOGE("recv() did not return NLMSG_ERROR message: %d",
+             resp.h.nlmsg_type);
+    return -ENOMSG;
+  }
+
+  if (resp.e.error) {
+    ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
+  }
+  return resp.e.error; // returns 0 on success
+}
+
+int hardwareAddressType(const char *interface) {
+  int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+  if (fd < 0)
+    return -errno;
+  auto scopeGuard = base::make_scope_guard([fd] { close(fd); });
+
+  struct ifreq ifr = {};
+  // We use strncpy() instead of strlcpy() since kernel has to be able
+  // to handle non-zero terminated junk passed in by userspace anyway,
+  // and this way too long interface names (more than IFNAMSIZ-1 = 15
+  // characters plus terminating NULL) will not get truncated to 15
+  // characters and zero-terminated and thus potentially erroneously
+  // match a truncated interface if one were to exist.
+  strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name));
+
+  if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) {
+    return -errno;
+  }
+  return ifr.ifr_hwaddr.sa_family;
+}
+
+} // namespace
+
+int isEthernet(const char *iface, bool &isEthernet) {
+  int rv = hardwareAddressType(iface);
+  if (rv < 0) {
+    ALOGE("Get hardware address type of interface %s failed: %s", iface,
+             strerror(-rv));
+    return rv;
+  }
+
+  // Backwards compatibility with pre-GKI kernels that use various custom
+  // ARPHRD_* for their cellular interface
+  switch (rv) {
+  // ARPHRD_PUREIP on at least some Mediatek Android kernels
+  // example: wembley with 4.19 kernel
+  case 520:
+  // in Linux 4.14+ rmnet support was upstreamed and ARHRD_RAWIP became 519,
+  // but it is 530 on at least some Qualcomm Android 4.9 kernels with rmnet
+  // example: Pixel 3 family
+  case 530:
+    // >5.4 kernels are GKI2.0 and thus upstream compatible, however 5.10
+    // shipped with Android S, so (for safety) let's limit ourselves to
+    // >5.10, ie. 5.11+ as a guarantee we're on Android T+ and thus no
+    // longer need this non-upstream compatibility logic
+    static bool is_pre_5_11_kernel = !isAtLeastKernelVersion(5, 11, 0);
+    if (is_pre_5_11_kernel)
+      return false;
+  }
+
+  switch (rv) {
+  case ARPHRD_ETHER:
+    isEthernet = true;
+    return 0;
+  case ARPHRD_NONE:
+  case ARPHRD_PPP:
+  case ARPHRD_RAWIP:
+    isEthernet = false;
+    return 0;
+  default:
+    ALOGE("Unknown hardware address type %d on interface %s", rv, iface);
+    return -EAFNOSUPPORT;
+  }
+}
+
+// ADD:     nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
+// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
+// DEL:     nlMsgType=RTM_DELQDISC nlMsgFlags=0
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
+  // This is the name of the qdisc we are attaching.
+  // Some hoop jumping to make this compile time constant with known size,
+  // so that the structure declaration is well defined at compile time.
+#define CLSACT "clsact"
+  // sizeof() includes the terminating NULL
+  static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
+
+  const struct {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
+    } kind;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = nlMsgType,
+              .nlmsg_flags =
+                  static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
+              .tcm_parent = TC_H_CLSACT,
+          },
+      .kind =
+          {
+              .attr =
+                  {
+                      .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
+                      .nla_type = TCA_KIND,
+                  },
+              .str = CLSACT,
+          },
+  };
+#undef CLSACT
+
+  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
+// /sys/fs/bpf/... direct-action
+int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto,
+                   const char *bpfProgPath) {
+  const int bpfFd = bpf::retrieveProgram(bpfProgPath);
+  if (bpfFd == -1) {
+    ALOGE("retrieveProgram failed: %d", errno);
+    return -errno;
+  }
+  auto scopeGuard = base::make_scope_guard([bpfFd] { close(bpfFd); });
+
+  struct {
+    nlmsghdr n;
+    tcmsg t;
+    struct {
+      nlattr attr;
+      // The maximum classifier name length is defined in
+      // tcf_proto_ops in include/net/sch_generic.h.
+      char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))];
+    } kind;
+    struct {
+      nlattr attr;
+      struct {
+        nlattr attr;
+        __u32 u32;
+      } fd;
+      struct {
+        nlattr attr;
+        char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+      } name;
+      struct {
+        nlattr attr;
+        __u32 u32;
+      } flags;
+    } options;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = RTM_NEWTFILTER,
+              .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_UNSPEC,
+              .tcm_parent = TC_H_MAKE(TC_H_CLSACT, ingress ? TC_H_MIN_INGRESS
+                                                           : TC_H_MIN_EGRESS),
+              .tcm_info =
+                  static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                     htons(static_cast<uint16_t>(proto))),
+          },
+      .kind =
+          {
+              .attr =
+                  {
+                      .nla_len = sizeof(req.kind),
+                      .nla_type = TCA_KIND,
+                  },
+              .str = CLS_BPF_KIND_NAME,
+          },
+      .options =
+          {
+              .attr =
+                  {
+                      .nla_len = sizeof(req.options),
+                      .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+                  },
+              .fd =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.fd),
+                              .nla_type = TCA_BPF_FD,
+                          },
+                      .u32 = static_cast<__u32>(bpfFd),
+                  },
+              .name =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.name),
+                              .nla_type = TCA_BPF_NAME,
+                          },
+                      // Visible via 'tc filter show', but
+                      // is overwritten by strncpy below
+                      .str = "placeholder",
+                  },
+              .flags =
+                  {
+                      .attr =
+                          {
+                              .nla_len = sizeof(req.options.flags),
+                              .nla_type = TCA_BPF_FLAGS,
+                          },
+                      .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+                  },
+          },
+  };
+
+  snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
+           basename(bpfProgPath));
+
+  int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+  return error;
+}
+
+// tc filter add dev .. ingress prio .. protocol .. matchall \
+//     action police rate .. burst .. conform-exceed pipe/continue \
+//     action bpf object-pinned .. \
+//     drop
+//
+// TODO: tc-police does not do ECN marking, so in the future, we should consider
+// adding a second tc-police filter at a lower priority that rate limits traffic
+// at something like 0.8 times the global rate limit and ecn marks exceeding
+// packets inside a bpf program (but does not drop them).
+int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto,
+                             unsigned rateInBytesPerSec,
+                             const char *bpfProgPath) {
+  // TODO: this value needs to be validated.
+  // TCP IW10 (initial congestion window) means servers will send 10 mtus worth
+  // of data on initial connect.
+  // If nic is LRO capable it could aggregate up to 64KiB, so again probably a
+  // bad idea to set burst below that, because ingress packets could get
+  // aggregated to 64KiB at the nic.
+  // I don't know, but I wonder whether we shouldn't just do 128KiB and not do
+  // any math.
+  static constexpr unsigned BURST_SIZE_IN_BYTES = 128 * 1024; // 128KiB
+  IngressPoliceFilterBuilder filter(ifIndex, prio, proto, rateInBytesPerSec,
+                                    BURST_SIZE_IN_BYTES, bpfProgPath);
+  const int error = filter.build();
+  if (error) {
+    return error;
+  }
+  return sendAndProcessNetlinkResponse(filter.getRequest(),
+                                       filter.getRequestSize());
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
+  const struct {
+    nlmsghdr n;
+    tcmsg t;
+  } req = {
+      .n =
+          {
+              .nlmsg_len = sizeof(req),
+              .nlmsg_type = RTM_DELTFILTER,
+              .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+          },
+      .t =
+          {
+              .tcm_family = AF_UNSPEC,
+              .tcm_ifindex = ifIndex,
+              .tcm_handle = TC_H_UNSPEC,
+              .tcm_parent = TC_H_MAKE(TC_H_CLSACT, ingress ? TC_H_MIN_INGRESS
+                                                           : TC_H_MIN_EGRESS),
+              .tcm_info =
+                  static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+                                     htons(static_cast<uint16_t>(proto))),
+          },
+  };
+
+  return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+} // namespace android
diff --git a/staticlibs/native/tcutils/tests/tcutils_test.cpp b/staticlibs/native/tcutils/tests/tcutils_test.cpp
new file mode 100644
index 0000000..32736d6
--- /dev/null
+++ b/staticlibs/native/tcutils/tests/tcutils_test.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright 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.
+ *
+ * TcUtilsTest.cpp - unit tests for TcUtils.cpp
+ */
+
+#include <gtest/gtest.h>
+
+#include "kernelversion.h"
+#include <tcutils/tcutils.h>
+
+#include <BpfSyscallWrappers.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+
+namespace android {
+
+TEST(LibTcUtilsTest, IsEthernetOfNonExistingIf) {
+  bool result = false;
+  int error = isEthernet("not_existing_if", result);
+  ASSERT_FALSE(result);
+  ASSERT_EQ(-ENODEV, error);
+}
+
+TEST(LibTcUtilsTest, IsEthernetOfLoopback) {
+  bool result = false;
+  int error = isEthernet("lo", result);
+  ASSERT_FALSE(result);
+  ASSERT_EQ(-EAFNOSUPPORT, error);
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+// See also HardwareAddressTypeOfWireless.
+TEST(LibTcUtilsTest, IsEthernetOfWireless) {
+  bool result = false;
+  int error = isEthernet("wlan0", result);
+  if (!result && error == -ENODEV)
+    return;
+
+  ASSERT_EQ(0, error);
+  ASSERT_TRUE(result);
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+// See also HardwareAddressTypeOfCellular.
+TEST(LibTcUtilsTest, IsEthernetOfCellular) {
+  bool result = false;
+  int error = isEthernet("rmnet_data0", result);
+  if (!result && error == -ENODEV)
+    return;
+
+  ASSERT_EQ(0, error);
+  ASSERT_FALSE(result);
+}
+
+// See Linux kernel source in include/net/flow.h
+static constexpr int LOOPBACK_IFINDEX = 1;
+
+TEST(LibTcUtilsTest, AttachReplaceDetachClsactLo) {
+  // This attaches and detaches a configuration-less and thus no-op clsact
+  // qdisc to loopback interface (and it takes fractions of a second)
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(0, tcReplaceQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  EXPECT_EQ(-EINVAL, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+}
+
+TEST(LibTcUtilsTest, AddAndDeleteBpfFilter) {
+  // TODO: this should use bpf_shared.h rather than hardcoding the path
+  static constexpr char bpfProgPath[] =
+      "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether";
+  const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+  // static test values
+  static constexpr bool ingress = true;
+  static constexpr uint16_t prio = 17;
+  static constexpr uint16_t proto = ETH_P_ALL;
+
+  // try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // try to attach bpf filter to missing qdisc
+  EXPECT_EQ(-EINVAL, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto,
+                                    bpfProgPath));
+  // add the clsact qdisc
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  // try to delete missing filter when there is a qdisc attached
+  EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // add and delete a bpf filter
+  EXPECT_EQ(
+      0, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto, bpfProgPath));
+  EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // try to remove the same filter a second time
+  EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+  // remove the clsact qdisc
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  // once again, try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto));
+}
+
+TEST(LibTcUtilsTest, AddAndDeleteIngressPoliceFilter) {
+  // TODO: this should use bpf_shared.h rather than hardcoding the path
+  static constexpr char bpfProgPath[] =
+      "/sys/fs/bpf/prog_netd_schedact_ingress_account";
+  int fd = bpf::retrieveProgram(bpfProgPath);
+  if (fd == -1) {
+    // ingress policing is not supported.
+    return;
+  }
+  close(fd);
+
+  const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+  // static test values
+  static constexpr unsigned rateInBytesPerSec =
+      1024 * 1024; // 8mbit/s => 1mbyte/s => 1024*1024 bytes/s.
+  static constexpr uint16_t prio = 17;
+  static constexpr uint16_t proto = ETH_P_ALL;
+
+  // try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // try to attach bpf filter to missing qdisc
+  EXPECT_EQ(-EINVAL, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto,
+                                              rateInBytesPerSec, bpfProgPath));
+  // add the clsact qdisc
+  EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX));
+  // try to delete missing filter when there is a qdisc attached
+  EXPECT_EQ(-errNOENT,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // add and delete a bpf filter
+  EXPECT_EQ(0, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto,
+                                        rateInBytesPerSec, bpfProgPath));
+  EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // try to remove the same filter a second time
+  EXPECT_EQ(-errNOENT,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+  // remove the clsact qdisc
+  EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX));
+  // once again, try to delete missing filter from missing qdisc
+  EXPECT_EQ(-EINVAL,
+            tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto));
+}
+
+} // namespace android
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 530ccd3..e249e19 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -21,11 +21,10 @@
     sdk_version: "system_current",
     min_sdk_version: "29",
     static_libs: [
-        "netd_aidl_interface-V7-java",
+        "netd_aidl_interface-V8-java",
     ],
     apex_available: [
         "//apex_available:platform", // used from services.net
-        "com.android.bluetooth.updatable",
         "com.android.tethering",
         "com.android.wifi",
     ],
@@ -45,10 +44,11 @@
 cc_library_static {
     name: "netd_aidl_interface-lateststable-ndk",
     whole_static_libs: [
-        "netd_aidl_interface-V7-ndk",
+        "netd_aidl_interface-V8-ndk",
     ],
     apex_available: [
         "com.android.resolv",
+        "com.android.tethering",
     ],
     min_sdk_version: "29",
 }
@@ -56,7 +56,7 @@
 cc_library_static {
     name: "netd_aidl_interface-lateststable-cpp",
     whole_static_libs: [
-        "netd_aidl_interface-V7-cpp",
+        "netd_aidl_interface-V8-cpp",
     ],
 }
 
@@ -89,7 +89,6 @@
             // either outside the system server or use jarjar to rename the generated AIDL classes.
             apex_available: [
                 "//apex_available:platform", // used from services.net
-                "com.android.bluetooth.updatable",
                 "com.android.tethering",
                 "com.android.wifi",
             ],
@@ -99,6 +98,7 @@
         ndk: {
             apex_available: [
                 "//apex_available:platform",
+                "com.android.tethering",
             ],
             // This is necessary for the DnsResovler tests to run in Android Q.
             // Soong would recognize this value and produce the Q compatible aidl library.
@@ -113,6 +113,7 @@
         "5",
         "6",
         "7",
+        "8",
     ],
 }
 
@@ -125,7 +126,6 @@
     ],
     apex_available: [
         "//apex_available:platform",
-        "com.android.bluetooth.updatable",
         "com.android.wifi",
         "com.android.tethering",
     ],
@@ -149,7 +149,6 @@
         java: {
             apex_available: [
                 "//apex_available:platform",
-                "com.android.bluetooth.updatable",
                 "com.android.wifi",
                 "com.android.tethering",
             ],
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/8/.hash
new file mode 100644
index 0000000..0933816
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/.hash
@@ -0,0 +1 @@
+e8cf8586fc5da9063818d8775e9a21c4b0addb5b
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl
new file mode 100644
index 0000000..ec03d86
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetd.aidl
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2016, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+  boolean isAlive();
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreatePhysical(int netId, int permission);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreateVpn(int netId, boolean secure);
+  void networkDestroy(int netId);
+  void networkAddInterface(int netId, in @utf8InCpp String iface);
+  void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+  void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+  void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+  boolean tetherApplyDnsInterfaces();
+  android.net.TetherStatsParcel[] tetherGetStats();
+  void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+  void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+  void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+  int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+  void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+  void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+  void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+  void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+  void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void strictUidCleartextPenalty(int uid, int policyPenalty);
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  void clatdStop(in @utf8InCpp String ifName);
+  boolean ipfwdEnabled();
+  @utf8InCpp String[] ipfwdGetRequesterList();
+  void ipfwdEnableForwarding(in @utf8InCpp String requester);
+  void ipfwdDisableForwarding(in @utf8InCpp String requester);
+  void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+  void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+  void bandwidthSetGlobalAlert(long bytes);
+  void bandwidthAddNaughtyApp(int uid);
+  void bandwidthRemoveNaughtyApp(int uid);
+  void bandwidthAddNiceApp(int uid);
+  void bandwidthRemoveNiceApp(int uid);
+  void tetherStart(in @utf8InCpp String[] dhcpRanges);
+  void tetherStop();
+  boolean tetherIsEnabled();
+  void tetherInterfaceAdd(in @utf8InCpp String ifName);
+  void tetherInterfaceRemove(in @utf8InCpp String ifName);
+  @utf8InCpp String[] tetherInterfaceList();
+  void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+  @utf8InCpp String[] tetherDnsList();
+  void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  int networkGetDefault();
+  void networkSetDefault(int netId);
+  void networkClearDefault();
+  void networkSetPermissionForNetwork(int netId, int permission);
+  void networkSetPermissionForUser(int permission, in int[] uids);
+  void networkClearPermissionForUser(in int[] uids);
+  void trafficSetNetPermForUids(int permission, in int[] uids);
+  void networkSetProtectAllow(int uid);
+  void networkSetProtectDeny(int uid);
+  boolean networkCanProtect(int uid);
+  void firewallSetFirewallType(int firewalltype);
+  void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  void firewallEnableChildChain(int childChain, boolean enable);
+  @utf8InCpp String[] interfaceGetList();
+  android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+  void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+  void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+  void interfaceClearAddrs(in @utf8InCpp String ifName);
+  void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+  void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+  void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+  void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  void trafficSwapActiveStatsMap();
+  IBinder getOemNetd();
+  void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+  android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+  void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  void networkCreate(in android.net.NativeNetworkConfig config);
+  void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  const int IPV4 = 4;
+  const int IPV6 = 6;
+  const int CONF = 1;
+  const int NEIGH = 2;
+  const String IPSEC_INTERFACE_PREFIX = "ipsec";
+  const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+  const int IPV6_ADDR_GEN_MODE_NONE = 1;
+  const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+  const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+  const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+  const int PENALTY_POLICY_ACCEPT = 1;
+  const int PENALTY_POLICY_LOG = 2;
+  const int PENALTY_POLICY_REJECT = 3;
+  const int LOCAL_NET_ID = 99;
+  const int DUMMY_NET_ID = 51;
+  const int UNREACHABLE_NET_ID = 52;
+  const String NEXTHOP_NONE = "";
+  const String NEXTHOP_UNREACHABLE = "unreachable";
+  const String NEXTHOP_THROW = "throw";
+  const int PERMISSION_NONE = 0;
+  const int PERMISSION_NETWORK = 1;
+  const int PERMISSION_SYSTEM = 2;
+  const int NO_PERMISSIONS = 0;
+  const int PERMISSION_INTERNET = 4;
+  const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+  const int PERMISSION_UNINSTALLED = -1;
+  /**
+   * @deprecated use FIREWALL_ALLOWLIST.
+   */
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_ALLOWLIST = 0;
+  /**
+   * @deprecated use FIREWALL_DENYLIST.
+   */
+  const int FIREWALL_BLACKLIST = 1;
+  const int FIREWALL_DENYLIST = 1;
+  const int FIREWALL_RULE_ALLOW = 1;
+  const int FIREWALL_RULE_DENY = 2;
+  const int FIREWALL_CHAIN_NONE = 0;
+  const int FIREWALL_CHAIN_DOZABLE = 1;
+  const int FIREWALL_CHAIN_STANDBY = 2;
+  const int FIREWALL_CHAIN_POWERSAVE = 3;
+  const int FIREWALL_CHAIN_RESTRICTED = 4;
+  const String IF_STATE_UP = "up";
+  const String IF_STATE_DOWN = "down";
+  const String IF_FLAG_BROADCAST = "broadcast";
+  const String IF_FLAG_LOOPBACK = "loopback";
+  const String IF_FLAG_POINTOPOINT = "point-to-point";
+  const String IF_FLAG_RUNNING = "running";
+  const String IF_FLAG_MULTICAST = "multicast";
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+  oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+  oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+  oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+  oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAdded(@utf8InCpp String ifName);
+  oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+  oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+  oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+  @utf8InCpp String ifName;
+  @utf8InCpp String hwAddr;
+  @utf8InCpp String ipv4Addr;
+  int prefixLength;
+  @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+  int mark;
+  int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+  int netId;
+  android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+  int permission;
+  boolean secure;
+  android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+  boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..06c8979
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeNetworkType.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+  PHYSICAL = 0,
+  VIRTUAL = 1,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+  SERVICE = 1,
+  PLATFORM = 2,
+  LEGACY = 3,
+  OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+  @utf8InCpp String destination;
+  @utf8InCpp String ifName;
+  @utf8InCpp String nextHop;
+  int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+  boolean usingLegacyDnsProxy;
+  @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+  int inputInterfaceIndex;
+  int outputInterfaceIndex;
+  byte[] destination;
+  int prefixLength;
+  byte[] srcL2Address;
+  byte[] dstL2Address;
+  int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+  int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/8/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+  int netId;
+  android.net.UidRangeParcel[] uidRanges;
+  int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkConfig.aidl
index 76562b2..77d814b 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkConfig.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/NativeNetworkConfig.aidl
@@ -40,4 +40,5 @@
   int permission;
   boolean secure;
   android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+  boolean excludeLocalRoutes = false;
 }
diff --git a/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl
index 2c4f83a..e654a1f 100644
--- a/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl
+++ b/staticlibs/netd/binder/android/net/NativeNetworkConfig.aidl
@@ -49,4 +49,9 @@
 
     /** For virtual networks. The type of VPN to create.  Ignored for all other network types. */
     NativeVpnType vpnType = NativeVpnType.PLATFORM;
+
+    /**
+     * For virtual networks. Whether local traffic is excluded from the VPN.
+     */
+    boolean excludeLocalRoutes = false;
 }
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index 9f4d6e3..08d5412 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -11,6 +11,7 @@
         "Log.cpp",
         "Netfilter.cpp",
         "Netlink.cpp",
+        "NetlinkListener.cpp",
         "Slice.cpp",
         "Socket.cpp",
         "SocketOption.cpp",
@@ -18,6 +19,7 @@
         "Syscalls.cpp",
         "UniqueFd.cpp",
         "UniqueFile.cpp",
+        "Utils.cpp",
     ],
     defaults: ["netd_defaults"],
     cflags: ["-Wall", "-Werror"],
diff --git a/staticlibs/netd/libnetdutils/NetlinkListener.cpp b/staticlibs/netd/libnetdutils/NetlinkListener.cpp
new file mode 100644
index 0000000..decaa9c
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/NetlinkListener.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#define LOG_TAG "NetlinkListener"
+
+#include <sstream>
+#include <vector>
+
+#include <linux/netfilter/nfnetlink.h>
+
+#include <log/log.h>
+#include <netdutils/Misc.h>
+#include <netdutils/NetlinkListener.h>
+#include <netdutils/Syscalls.h>
+
+namespace android {
+namespace netdutils {
+
+using netdutils::Fd;
+using netdutils::Slice;
+using netdutils::Status;
+using netdutils::UniqueFd;
+using netdutils::findWithDefault;
+using netdutils::forEachNetlinkMessage;
+using netdutils::makeSlice;
+using netdutils::sSyscalls;
+using netdutils::status::ok;
+using netdutils::statusFromErrno;
+
+namespace {
+
+constexpr int kNetlinkMsgErrorType = (NFNL_SUBSYS_NONE << 8) | NLMSG_ERROR;
+
+constexpr sockaddr_nl kKernelAddr = {
+    .nl_family = AF_NETLINK, .nl_pad = 0, .nl_pid = 0, .nl_groups = 0,
+};
+
+const NetlinkListener::DispatchFn kDefaultDispatchFn = [](const nlmsghdr& nlmsg, const Slice) {
+    std::stringstream ss;
+    ss << nlmsg;
+    ALOGE("unhandled netlink message: %s", ss.str().c_str());
+};
+
+}  // namespace
+
+NetlinkListener::NetlinkListener(UniqueFd event, UniqueFd sock, const std::string& name)
+    : mEvent(std::move(event)), mSock(std::move(sock)), mThreadName(name) {
+    const auto rxErrorHandler = [](const nlmsghdr& nlmsg, const Slice msg) {
+        std::stringstream ss;
+        ss << nlmsg << " " << msg << " " << netdutils::toHex(msg, 32);
+        ALOGE("unhandled netlink message: %s", ss.str().c_str());
+    };
+    expectOk(NetlinkListener::subscribe(kNetlinkMsgErrorType, rxErrorHandler));
+
+    mErrorHandler = [& name = mThreadName](const int fd, const int err) {
+        ALOGE("Error on NetlinkListener(%s) fd=%d: %s", name.c_str(), fd, strerror(err));
+    };
+
+    // Start the thread
+    mWorker = std::thread([this]() { run().ignoreError(); });
+}
+
+NetlinkListener::~NetlinkListener() {
+    const auto& sys = sSyscalls.get();
+    const uint64_t data = 1;
+    // eventfd should never enter an error state unexpectedly
+    expectOk(sys.write(mEvent, makeSlice(data)).status());
+    mWorker.join();
+}
+
+Status NetlinkListener::send(const Slice msg) {
+    const auto& sys = sSyscalls.get();
+    ASSIGN_OR_RETURN(auto sent, sys.sendto(mSock, msg, 0, kKernelAddr));
+    if (sent != msg.size()) {
+        return statusFromErrno(EMSGSIZE, "unexpect message size");
+    }
+    return ok;
+}
+
+Status NetlinkListener::subscribe(uint16_t type, const DispatchFn& fn) {
+    std::lock_guard guard(mMutex);
+    mDispatchMap[type] = fn;
+    return ok;
+}
+
+Status NetlinkListener::unsubscribe(uint16_t type) {
+    std::lock_guard guard(mMutex);
+    mDispatchMap.erase(type);
+    return ok;
+}
+
+void NetlinkListener::registerSkErrorHandler(const SkErrorHandler& handler) {
+    mErrorHandler = handler;
+}
+
+Status NetlinkListener::run() {
+    std::vector<char> rxbuf(4096);
+
+    const auto rxHandler = [this](const nlmsghdr& nlmsg, const Slice& buf) {
+        std::lock_guard guard(mMutex);
+        const auto& fn = findWithDefault(mDispatchMap, nlmsg.nlmsg_type, kDefaultDispatchFn);
+        fn(nlmsg, buf);
+    };
+
+    if (mThreadName.length() > 0) {
+        int ret = pthread_setname_np(pthread_self(), mThreadName.c_str());
+        if (ret) {
+            ALOGE("thread name set failed, name: %s, ret: %s", mThreadName.c_str(), strerror(ret));
+        }
+    }
+    const auto& sys = sSyscalls.get();
+    const std::array<Fd, 2> fds{{{mEvent}, {mSock}}};
+    const int events = POLLIN;
+    const double timeout = 3600;
+    while (true) {
+        ASSIGN_OR_RETURN(auto revents, sys.ppoll(fds, events, timeout));
+        // After mEvent becomes readable, we should stop servicing mSock and return
+        if (revents[0] & POLLIN) {
+            break;
+        }
+        if (revents[1] & (POLLIN|POLLERR)) {
+            auto rx = sys.recvfrom(mSock, makeSlice(rxbuf), 0);
+            int err = rx.status().code();
+            if (err) {
+                // Ignore errors. The only error we expect to see here is ENOBUFS, and there's
+                // nothing we can do about that. The recvfrom above will already have cleared the
+                // error indication and ensured we won't get EPOLLERR again.
+                // TODO: Consider using NETLINK_NO_ENOBUFS.
+                mErrorHandler(((Fd) mSock).get(), err);
+                continue;
+            }
+            forEachNetlinkMessage(rx.value(), rxHandler);
+        }
+    }
+    return ok;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/Utils.cpp b/staticlibs/netd/libnetdutils/Utils.cpp
new file mode 100644
index 0000000..16ec882
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/Utils.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include <map>
+
+#include <net/if.h>
+
+#include "dirent.h"
+#include "netdutils/Status.h"
+#include "netdutils/Utils.h"
+
+namespace android {
+namespace netdutils {
+
+StatusOr<std::vector<std::string>> getIfaceNames() {
+    std::vector<std::string> ifaceNames;
+    DIR* d;
+    struct dirent* de;
+
+    if (!(d = opendir("/sys/class/net"))) {
+        return statusFromErrno(errno, "Cannot open iface directory");
+    }
+    while ((de = readdir(d))) {
+        if ((de->d_type != DT_DIR) && (de->d_type != DT_LNK)) continue;
+        if (de->d_name[0] == '.') continue;
+        ifaceNames.push_back(std::string(de->d_name));
+    }
+    closedir(d);
+    return ifaceNames;
+}
+
+StatusOr<std::map<std::string, uint32_t>> getIfaceList() {
+    std::map<std::string, uint32_t> ifacePairs;
+
+    ASSIGN_OR_RETURN(auto ifaceNames, getIfaceNames());
+
+    for (const auto& name : ifaceNames) {
+        uint32_t ifaceIndex = if_nametoindex(name.c_str());
+        if (ifaceIndex) {
+            ifacePairs.insert(std::pair<std::string, uint32_t>(name, ifaceIndex));
+        }
+    }
+    return ifacePairs;
+}
+
+}  // namespace netdutils
+}  // namespace android
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h b/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h
new file mode 100644
index 0000000..97f7bb2
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/NetlinkListener.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#ifndef NETLINK_LISTENER_H
+#define NETLINK_LISTENER_H
+
+#include <functional>
+#include <map>
+#include <mutex>
+#include <thread>
+
+#include <android-base/thread_annotations.h>
+#include <netdutils/Netlink.h>
+#include <netdutils/Slice.h>
+#include <netdutils/Status.h>
+#include <netdutils/UniqueFd.h>
+
+namespace android {
+namespace netdutils {
+
+class NetlinkListenerInterface {
+  public:
+    using DispatchFn = std::function<void(const nlmsghdr& nlmsg, const netdutils::Slice msg)>;
+
+    using SkErrorHandler = std::function<void(const int fd, const int err)>;
+
+    virtual ~NetlinkListenerInterface() = default;
+
+    // Send message to the kernel using the underlying netlink socket
+    virtual netdutils::Status send(const netdutils::Slice msg) = 0;
+
+    // Deliver future messages with nlmsghdr.nlmsg_type == type to fn.
+    //
+    // Threadsafe.
+    // All dispatch functions invoked on a single service thread.
+    // subscribe() and join() must not be called from the stack of fn().
+    virtual netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) = 0;
+
+    // Halt delivery of future messages with nlmsghdr.nlmsg_type == type.
+    // Threadsafe.
+    virtual netdutils::Status unsubscribe(uint16_t type) = 0;
+
+    virtual void registerSkErrorHandler(const SkErrorHandler& handler) = 0;
+};
+
+// NetlinkListener manages a netlink socket and associated blocking
+// service thread.
+//
+// This class is written in a generic way to allow multiple different
+// netlink subsystems to share this common infrastructure. If multiple
+// subsystems share the same message delivery requirements (drops ok,
+// no drops) they may share a single listener by calling subscribe()
+// with multiple types.
+//
+// This class is suitable for moderate performance message
+// processing. In particular it avoids extra copies of received
+// message data and allows client code to control which message
+// attributes are processed.
+//
+// Note that NetlinkListener is capable of processing multiple batched
+// netlink messages in a single system call. This is useful to
+// netfilter extensions that allow batching of events like NFLOG.
+class NetlinkListener : public NetlinkListenerInterface {
+  public:
+    NetlinkListener(netdutils::UniqueFd event, netdutils::UniqueFd sock, const std::string& name);
+
+    ~NetlinkListener() override;
+
+    netdutils::Status send(const netdutils::Slice msg) override;
+
+    netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) override EXCLUDES(mMutex);
+
+    netdutils::Status unsubscribe(uint16_t type) override EXCLUDES(mMutex);
+
+    void registerSkErrorHandler(const SkErrorHandler& handler) override;
+
+  private:
+    netdutils::Status run();
+
+    const netdutils::UniqueFd mEvent;
+    const netdutils::UniqueFd mSock;
+    const std::string mThreadName;
+    std::mutex mMutex;
+    std::map<uint16_t, DispatchFn> mDispatchMap GUARDED_BY(mMutex);
+    std::thread mWorker;
+    SkErrorHandler mErrorHandler;
+};
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETLINK_LISTENER_H */
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Status.h b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
index bc347d5..7b0bd47 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Status.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Status.h
@@ -42,7 +42,7 @@
     Status(int code, std::string msg) : mCode(code), mMsg(std::move(msg)) { assert(!ok()); }
 
     Status(android::base::Result<void> result)
-        : mCode(result.ok() ? 0 : result.error().code()),
+        : mCode(result.ok() ? 0 : static_cast<int>(result.error().code())),
           mMsg(result.ok() ? "" : result.error().message()) {}
 
     int code() const { return mCode; }
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Utils.h b/staticlibs/netd/libnetdutils/include/netdutils/Utils.h
new file mode 100644
index 0000000..83c583b
--- /dev/null
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Utils.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#ifndef NETUTILS_UTILS_H
+#define NETUTILS_UTILS_H
+
+#include "netdutils/StatusOr.h"
+
+namespace android {
+namespace netdutils {
+
+StatusOr<std::vector<std::string>> getIfaceNames();
+
+StatusOr<std::map<std::string, uint32_t>> getIfaceList();
+
+}  // namespace netdutils
+}  // namespace android
+
+#endif /* NETUTILS_UTILS_H */
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 96648a5..911483a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -77,4 +77,13 @@
         assertFalse(CollectionUtils.contains(arrayOf("A", "B", "C"), "D"))
         assertFalse(CollectionUtils.contains(null, "A"))
     }
+
+    @Test
+    fun testTotal() {
+        assertEquals(10, CollectionUtils.total(longArrayOf(3, 6, 1)))
+        assertEquals(10, CollectionUtils.total(longArrayOf(6, 1, 3)))
+        assertEquals(10, CollectionUtils.total(longArrayOf(1, 3, 6)))
+        assertEquals(3, CollectionUtils.total(longArrayOf(1, 1, 1)))
+        assertEquals(0, CollectionUtils.total(null))
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java
index f44b17d..20bbd4a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/IpRangeTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.annotation.SuppressLint;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 
@@ -92,6 +93,7 @@
         }
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testConstructor() {
         IpRange r = new IpRange(new IpPrefix(IPV4_ADDR, 32));
@@ -119,6 +121,7 @@
         assertEquals(IPV6_RANGE_END, r.getEndAddr());
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testContainsRangeEqualRanges() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
@@ -129,6 +132,7 @@
         assertEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testContainsRangeSubset() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 64));
@@ -139,6 +143,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testContainsRangeTruncatesLowerOrderBits() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 100));
@@ -149,6 +154,7 @@
         assertEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testContainsRangeSubsetSameStartAddr() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
@@ -159,6 +165,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testContainsRangeOverlapping() {
         final IpRange r1 = new IpRange(new IpPrefix(address("2001:db9::"), 32));
@@ -169,6 +176,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testOverlapsRangeEqualRanges() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
@@ -179,6 +187,7 @@
         assertEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testOverlapsRangeSubset() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 35));
@@ -189,6 +198,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testOverlapsRangeDisjoint() {
         final IpRange r1 = new IpRange(new IpPrefix(IPV6_ADDR, 32));
@@ -199,6 +209,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testOverlapsRangePartialOverlapLow() {
         final IpRange r1 = new IpRange(new IpPrefix(address("2001:db9::"), 32));
@@ -209,6 +220,7 @@
         assertNotEquals(r1, r2);
     }
 
+    @SuppressLint("NewApi")
     @Test
     public void testOverlapsRangePartialOverlapHigh() {
         final IpRange r1 = new IpRange(new IpPrefix(address("2001:db7::"), 32));
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 3d2d6eb..09f0490 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
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.SuppressLint;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
@@ -46,6 +47,7 @@
 
 @RunWith(AndroidJUnit4.class)
 public final class LinkPropertiesUtilsTest {
+    @SuppressLint("NewApi")
     private static final IpPrefix PREFIX = new IpPrefix(toInetAddress("75.208.6.0"), 24);
     private static final InetAddress V4_ADDR = toInetAddress("75.208.6.1");
     private static final InetAddress V6_ADDR  = toInetAddress(
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
index f78c74e..256ea1e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkCapabilitiesUtilsTest.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.TargetApi
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_BIP
 import android.net.NetworkCapabilities.NET_CAPABILITY_CBS
 import android.net.NetworkCapabilities.NET_CAPABILITY_EIMS
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -33,6 +34,7 @@
 import android.os.Build
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.NetworkCapabilitiesUtils.RESTRICTED_CAPABILITIES
 import com.android.net.module.util.NetworkCapabilitiesUtils.UNRESTRICTED_CAPABILITIES
 import com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport
@@ -110,6 +112,12 @@
         // as restricted when there is no any unrestricted capability.
         nc.removeCapability(NET_CAPABILITY_INTERNET)
         assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
+        if (!SdkLevel.isAtLeastS()) return
+        // BIP deserves its specific test because it's the first capability over 30, meaning the
+        // shift will overflow
+        nc.removeCapability(NET_CAPABILITY_CBS)
+        nc.addCapability(NET_CAPABILITY_BIP)
+        assertTrue(NetworkCapabilitiesUtils.inferRestrictedCapability(nc))
     }
 
     @Test
@@ -122,9 +130,17 @@
         assertEquals((1 shl NET_CAPABILITY_CBS).toLong() and RESTRICTED_CAPABILITIES,
                 (1 shl NET_CAPABILITY_CBS).toLong())
 
+        // verify BIP is also restricted
+        // BIP is not available in R and before, but the BIP constant is inlined so
+        // this test can still run on R.
+        assertEquals((1L shl NET_CAPABILITY_BIP) and RESTRICTED_CAPABILITIES,
+                (1L shl NET_CAPABILITY_BIP))
+
         // verify default is not restricted
         assertEquals((1 shl NET_CAPABILITY_INTERNET).toLong() and RESTRICTED_CAPABILITIES, 0)
 
+        assertTrue(RESTRICTED_CAPABILITIES > 0)
+
         // just to see
         assertEquals(RESTRICTED_CAPABILITIES and UNRESTRICTED_CAPABILITIES, 0)
     }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..2785ea9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.net.NetworkStats
+import android.text.TextUtils
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkStatsUtilsTest {
+    @Test
+    fun testMultiplySafeByRational() {
+        // Verify basic cases that the method equals to a * b / c.
+        assertEquals(3 * 5 / 2, NetworkStatsUtils.multiplySafeByRational(3, 5, 2))
+
+        // Verify input with zeros.
+        assertEquals(0 * 7 / 3, NetworkStatsUtils.multiplySafeByRational(0, 7, 3))
+        assertEquals(7 * 0 / 3, NetworkStatsUtils.multiplySafeByRational(7, 0, 3))
+        assertEquals(0 * 0 / 1, NetworkStatsUtils.multiplySafeByRational(0, 0, 1))
+        assertEquals(0, NetworkStatsUtils.multiplySafeByRational(0, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertEquals(0, NetworkStatsUtils.multiplySafeByRational(Long.MAX_VALUE, 0, Long.MAX_VALUE))
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(7, 3, 0)
+        }
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(0, 0, 0)
+        }
+
+        // Verify cases where a * b overflows.
+        assertEquals(101, NetworkStatsUtils.multiplySafeByRational(
+                101, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertEquals(721, NetworkStatsUtils.multiplySafeByRational(
+                Long.MAX_VALUE, 721, Long.MAX_VALUE))
+        assertEquals(Long.MAX_VALUE, NetworkStatsUtils.multiplySafeByRational(
+                Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE))
+        assertFailsWith<ArithmeticException> {
+            NetworkStatsUtils.multiplySafeByRational(Long.MAX_VALUE, Long.MAX_VALUE, 0)
+        }
+    }
+
+    @Test
+    fun testConstrain() {
+        assertFailsWith<IllegalArgumentException> {
+            NetworkStatsUtils.constrain(5, 6, 3) // low > high
+        }
+        assertEquals(3, NetworkStatsUtils.constrain(5, 1, 3))
+        assertEquals(3, NetworkStatsUtils.constrain(3, 1, 3))
+        assertEquals(2, NetworkStatsUtils.constrain(2, 1, 3))
+        assertEquals(1, NetworkStatsUtils.constrain(1, 1, 3))
+        assertEquals(1, NetworkStatsUtils.constrain(0, 1, 3))
+
+        assertEquals(11, NetworkStatsUtils.constrain(15, 11, 11))
+        assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
+        assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
+    }
+
+    @Test
+    fun testBucketToEntry() {
+        val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
+                android.app.usage.NetworkStats.Bucket.TAG_NONE,
+                android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
+                android.app.usage.NetworkStats.Bucket.METERED_YES,
+                android.app.usage.NetworkStats.Bucket.ROAMING_NO,
+                android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
+        val entry = NetworkStatsUtils.fromBucket(bucket)
+        val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
+            NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
+            NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
+            0 /* operations */)
+
+        // TODO: Use assertEquals once all downstreams accept null iface in
+        // NetworkStats.Entry#equals.
+        assertEntryEquals(expectedEntry, entry)
+    }
+
+    private fun makeMockBucket(
+        uid: Int,
+        tag: Int,
+        state: Int,
+        metered: Int,
+        roaming: Int,
+        defaultNetwork: Int,
+        rxBytes: Long,
+        rxPackets: Long,
+        txBytes: Long,
+        txPackets: Long
+    ): android.app.usage.NetworkStats.Bucket {
+        val ret: android.app.usage.NetworkStats.Bucket =
+                mock(android.app.usage.NetworkStats.Bucket::class.java)
+        doReturn(uid).`when`(ret).getUid()
+        doReturn(tag).`when`(ret).getTag()
+        doReturn(state).`when`(ret).getState()
+        doReturn(metered).`when`(ret).getMetered()
+        doReturn(roaming).`when`(ret).getRoaming()
+        doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
+        doReturn(rxBytes).`when`(ret).getRxBytes()
+        doReturn(rxPackets).`when`(ret).getRxPackets()
+        doReturn(txBytes).`when`(ret).getTxBytes()
+        doReturn(txPackets).`when`(ret).getTxPackets()
+        return ret
+    }
+
+    /**
+     * Assert that the two {@link NetworkStats.Entry} are equals.
+     */
+    private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+        TextUtils.equals(left.iface, right.iface)
+        assertEquals(left.uid, right.uid)
+        assertEquals(left.set, right.set)
+        assertEquals(left.tag, right.tag)
+        assertEquals(left.metered, right.metered)
+        assertEquals(left.roaming, right.roaming)
+        assertEquals(left.defaultNetwork, right.defaultNetwork)
+        assertEquals(left.rxBytes, right.rxBytes)
+        assertEquals(left.rxPackets, right.rxPackets)
+        assertEquals(left.txBytes, right.txBytes)
+        assertEquals(left.txPackets, right.txPackets)
+        assertEquals(left.operations, right.operations)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
new file mode 100644
index 0000000..8f9a1f9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2021 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;
+
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
+import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.TcpHeader;
+import com.android.net.module.util.structs.UdpHeader;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PacketBuilderTest {
+    private static final MacAddress SRC_MAC = MacAddress.fromString("11:22:33:44:55:66");
+    private static final MacAddress DST_MAC = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+    private static final Inet4Address IPV4_SRC_ADDR = addr("192.0.2.1");
+    private static final Inet4Address IPV4_DST_ADDR = addr("198.51.100.1");
+    private static final short SRC_PORT = 9876;
+    private static final short DST_PORT = 433;
+    private static final short SEQ_NO = 13579;
+    private static final short ACK_NO = 24680;
+    private static final byte TYPE_OF_SERVICE = 0;
+    private static final short ID = 27149;
+    private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
+    private static final byte TIME_TO_LIVE = (byte) 0x40;
+    private static final short WINDOW = (short) 0x2000;
+    private static final short URGENT_POINTER = 0;
+    private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[] {
+            (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+    });
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x08, (byte) 0x00,
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                       type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x08, (byte) 0x00,
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0))
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                    tos=0, id=27149, flags='DF') /
+                //           scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680,
+                //                     flags='A', window=8192, urgptr=0) /
+                //           b'\xde\xad\xbe\xef')
+                // IPv4 header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // TCP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b,
+                (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68,
+                (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00,
+                (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                 type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433))
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x08, (byte) 0x00,
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x8d,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0xeb, (byte) 0x62
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff",
+                //                 type='IPv4') /
+                //           scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x08, (byte) 0x00,
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x20,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x89,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x4d, (byte) 0xbd,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_UDPHDR =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433))
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x8d,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x08, (byte) 0xeb, (byte) 0x62
+            };
+
+    private static final byte[] TEST_PACKET_IPV4HDR_UDPHDR_DATA =
+            new byte[] {
+                // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1",
+                //                 tos=0, id=27149, flags='DF') /
+                //           scapy.UDP(sport=9876, dport=433) /
+                //           b'\xde\xad\xbe\xef')
+                // IP header
+                (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x20,
+                (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00,
+                (byte) 0x40, (byte) 0x11, (byte) 0xe4, (byte) 0x89,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+                (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x00, (byte) 0x0c, (byte) 0x4d, (byte) 0xbd,
+                // Data
+                (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
+            };
+
+    /**
+     * Build an IPv4 packet which has ether header, IPv4 header, TCP/UDP header and data.
+     * The ethernet header and data are optional. Note that both source mac address and
+     * destination mac address are required for ethernet header.
+     *
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                Layer 2 header (EthernetHeader)                | (optional)
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                  Layer 3 header (Ipv4Header)                  |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |           Layer 4 header (TcpHeader, UdpHeader)               |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                          Payload                              | (optional)
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     *
+     * @param srcMac source MAC address. used by L2 ether header.
+     * @param dstMac destination MAC address. used by L2 ether header.
+     * @param l4proto the layer 4 protocol. support either IPPROTO_TCP or IPPROTO_UDP.
+     * @param payload the payload.
+     */
+    @NonNull
+    private ByteBuffer buildIpv4Packet(@Nullable final MacAddress srcMac,
+            @Nullable final MacAddress dstMac, final int l4proto,
+            @Nullable final ByteBuffer payload)
+            throws Exception {
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        final boolean hasEther = (srcMac != null && dstMac != null);
+        final int payloadLen = (payload == null) ? 0 : payload.limit();
+        final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, l4proto,
+                payloadLen);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+
+        if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
+        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                TIME_TO_LIVE, (byte) l4proto, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        if (l4proto == IPPROTO_TCP) {
+            packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                    TCPHDR_ACK, WINDOW, URGENT_POINTER);
+        } else if (l4proto == IPPROTO_UDP) {
+            packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT);
+        }
+        if (payload != null) {
+            buffer.put(payload);
+            // in case data might be reused by caller, restore the position and
+            // limit of bytebuffer.
+            payload.clear();
+        }
+
+        return packetBuilder.finalizePacket();
+    }
+
+    /**
+     * Check ethernet header.
+     *
+     * @param actual the packet to check.
+     */
+    private void checkEtherHeader(final ByteBuffer actual) {
+        final EthernetHeader eth = Struct.parse(EthernetHeader.class, actual);
+        assertEquals(SRC_MAC, eth.srcMac);
+        assertEquals(DST_MAC, eth.dstMac);
+        assertEquals(ETHER_TYPE_IPV4, eth.etherType);
+    }
+
+    /**
+     * Check IPv4 header.
+     *
+     * @param l4proto the layer 4 protocol. support either IPPROTO_TCP or IPPROTO_UDP.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkIpv4Header(final int l4proto, final boolean hasData,
+            final ByteBuffer actual) {
+        if (l4proto != IPPROTO_TCP && l4proto != IPPROTO_UDP) {
+            fail("Unsupported layer 4 protocol " + l4proto);
+        }
+
+        final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, actual);
+        assertEquals(Ipv4Header.IPHDR_VERSION_IHL, ipv4Header.vi);
+        assertEquals(TYPE_OF_SERVICE, ipv4Header.tos);
+        assertEquals(ID, ipv4Header.id);
+        assertEquals(FLAGS_AND_FRAGMENT_OFFSET, ipv4Header.flagsAndFragmentOffset);
+        assertEquals(TIME_TO_LIVE, ipv4Header.ttl);
+        assertEquals(IPV4_SRC_ADDR, ipv4Header.srcIp);
+        assertEquals(IPV4_DST_ADDR, ipv4Header.dstIp);
+
+        final int dataLength = hasData ? DATA.limit() : 0;
+        if (l4proto == IPPROTO_TCP) {
+            assertEquals(IPV4_HEADER_MIN_LEN + TCP_HEADER_MIN_LEN + dataLength,
+                    ipv4Header.totalLength);
+            assertEquals((byte) IPPROTO_TCP, ipv4Header.protocol);
+            assertEquals(hasData ? (short) 0xe488 : (short) 0xe48c, ipv4Header.checksum);
+        } else if (l4proto == IPPROTO_UDP) {
+            assertEquals(IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN + dataLength,
+                    ipv4Header.totalLength);
+            assertEquals((byte) IPPROTO_UDP, ipv4Header.protocol);
+            assertEquals(hasData ? (short) 0xe489 : (short) 0xe48d, ipv4Header.checksum);
+        }
+    }
+
+    /**
+     * Check TCPv4 packet.
+     *
+     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkTcpv4Packet(final boolean hasEther, final boolean hasData,
+            final ByteBuffer actual) {
+        if (hasEther) {
+            checkEtherHeader(actual);
+        }
+        checkIpv4Header(IPPROTO_TCP, hasData, actual);
+
+        final TcpHeader tcpHeader = Struct.parse(TcpHeader.class, actual);
+        assertEquals(SRC_PORT, tcpHeader.srcPort);
+        assertEquals(DST_PORT, tcpHeader.dstPort);
+        assertEquals(SEQ_NO, tcpHeader.seq);
+        assertEquals(ACK_NO, tcpHeader.ack);
+        assertEquals((short) 0x5010 /* offset=5(*4bytes), control bits=ACK */,
+                tcpHeader.dataOffsetAndControlBits);
+        assertEquals(WINDOW, tcpHeader.window);
+        assertEquals(hasData ? (short) 0x4844 : (short) 0xe5e5, tcpHeader.checksum);
+        assertEquals(URGENT_POINTER, tcpHeader.urgentPointer);
+
+        if (hasData) {
+            assertEquals(0xdeadbeef, actual.getInt());
+        }
+    }
+
+    /**
+     * Check UDPv4 packet.
+     *
+     * @param hasEther true if the packet has ether header; false otherwise.
+     * @param hasData true if the packet has data payload; false otherwise.
+     * @param actual the packet to check.
+     */
+    private void checkUdpv4Packet(final boolean hasEther, final boolean hasData,
+            final ByteBuffer actual) {
+        if (hasEther) {
+            checkEtherHeader(actual);
+        }
+        checkIpv4Header(IPPROTO_UDP, hasData, actual);
+
+        final UdpHeader udpHeader = Struct.parse(UdpHeader.class, actual);
+        assertEquals(SRC_PORT, udpHeader.srcPort);
+        assertEquals(DST_PORT, udpHeader.dstPort);
+        final int dataLength = hasData ? DATA.limit() : 0;
+        assertEquals(UDP_HEADER_LEN + dataLength, udpHeader.length);
+        assertEquals(hasData ? (short) 0x4dbd : (short) 0xeb62, udpHeader.checksum);
+
+        if (hasData) {
+            assertEquals(0xdeadbeef, actual.getInt());
+        }
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4Tcp() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_TCP, null /* data */);
+        checkTcpv4Packet(true /* hasEther */, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4TcpData() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_TCP, DATA);
+        checkTcpv4Packet(true /* hasEther */, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA,
+                packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4Tcp() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
+                IPPROTO_TCP, null /* data */);
+        checkTcpv4Packet(false /* hasEther */, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4TcpData() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
+                IPPROTO_TCP, DATA);
+        checkTcpv4Packet(false /* hasEther */, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4Udp() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_UDP, null /* data */);
+        checkUdpv4Packet(true /* hasEther */, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketEtherIPv4UdpData() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(SRC_MAC, DST_MAC, IPPROTO_UDP, DATA);
+        checkUdpv4Packet(true /* hasEther */, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4Udp() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
+                IPPROTO_UDP, null /*data*/);
+        checkUdpv4Packet(false /* hasEther */, false /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR, packet.array());
+    }
+
+    @Test
+    public void testBuildPacketIPv4UdpData() throws Exception {
+        final ByteBuffer packet = buildIpv4Packet(null /* srcMac */, null /* dstMac */,
+                IPPROTO_UDP, DATA);
+        checkUdpv4Packet(false /* hasEther */, true /* hasData */, packet);
+        assertArrayEquals(TEST_PACKET_IPV4HDR_UDPHDR_DATA, packet.array());
+    }
+
+    @Test
+    public void testFinalizePacketWithoutIpv4Header() throws Exception {
+        final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
+                IPPROTO_TCP, 0 /* payloadLen */);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+        packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                TCPHDR_ACK, WINDOW, URGENT_POINTER);
+        assertThrows("java.io.IOException: Packet is missing IPv4 header", IOException.class,
+                () -> packetBuilder.finalizePacket());
+    }
+
+    @Test
+    public void testFinalizePacketWithoutL4Header() throws Exception {
+        final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
+                IPPROTO_TCP, 0 /* payloadLen */);
+        final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+        packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR);
+        assertThrows("java.io.IOException: Packet is missing neither TCP nor UDP header",
+                IOException.class, () -> packetBuilder.finalizePacket());
+    }
+
+    @Test
+    public void testWriteL2HeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeL2Header(SRC_MAC, DST_MAC, (short) ETHER_TYPE_IPV4));
+    }
+
+    @Test
+    public void testWriteIpv4HeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+                        TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR));
+    }
+
+    @Test
+    public void testWriteTcpHeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class,
+                () -> packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO,
+                        TCPHDR_ACK, WINDOW, URGENT_POINTER));
+    }
+
+    @Test
+    public void testWriteUdpHeaderToInsufficientBuffer() throws Exception {
+        final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1));
+        assertThrows(IOException.class, () -> packetBuilder.writeUdpHeader(SRC_PORT, DST_PORT));
+    }
+
+    private static Inet4Address addr(String addr) {
+        return (Inet4Address) InetAddresses.parseNumericAddress(addr);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
index 6da5e7d..1b6cbcb 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
@@ -18,22 +18,28 @@
 
 import android.Manifest.permission.NETWORK_STACK
 import android.content.Context
+import android.content.pm.PackageManager
 import android.content.pm.PackageManager.PERMISSION_DENIED
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.net.module.util.PermissionUtils.checkAnyPermissionOf
+import com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf
 import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission
 import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr
-import com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf
+import com.android.net.module.util.PermissionUtils.enforceSystemFeature
+import org.junit.Assert
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 
 /** Tests for PermissionUtils */
@@ -43,6 +49,12 @@
     private val TEST_PERMISSION1 = "android.permission.TEST_PERMISSION1"
     private val TEST_PERMISSION2 = "android.permission.TEST_PERMISSION2"
     private val context = mock(Context::class.java)
+    private val packageManager = mock(PackageManager::class.java)
+
+    @Before
+    fun setup() {
+        doReturn(packageManager).`when`(context).packageManager
+    }
 
     @Test
     fun testEnforceAnyPermissionOf() {
@@ -90,4 +102,26 @@
         assertFailsWith<SecurityException>("Expect fail but permission granted.") {
             enforceNetworkStackPermissionOr(context, TEST_PERMISSION2) }
     }
+
+    private fun mockHasSystemFeature(featureName: String, hasFeature: Boolean) {
+        doReturn(hasFeature).`when`(packageManager)
+                .hasSystemFeature(ArgumentMatchers.eq(featureName))
+    }
+
+    @Test
+    fun testEnforceSystemFeature() {
+        val systemFeature = "test.system.feature"
+        val exceptionMessage = "test exception message"
+        mockHasSystemFeature(featureName = systemFeature, hasFeature = false)
+        val e = assertFailsWith<UnsupportedOperationException>("Should fail without feature") {
+            enforceSystemFeature(context, systemFeature, exceptionMessage) }
+        assertEquals(exceptionMessage, e.message)
+
+        mockHasSystemFeature(featureName = systemFeature, hasFeature = true)
+        try {
+            enforceSystemFeature(context, systemFeature, "")
+        } catch (e: UnsupportedOperationException) {
+            Assert.fail("Exception should have not been thrown with system feature enabled")
+        }
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index eabc14b..4e46210 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.SuppressLint;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.MacAddress;
@@ -453,6 +454,7 @@
         }
     }
 
+    @SuppressLint("NewApi")
     private void verifyPrefixByteArrayParsing(final PrefixMessage msg) throws Exception {
         // The original PREF64 option message has just 12 bytes for prefix byte array
         // (Highest 96 bits of the Prefix), copyOf pads the 128-bits IPv6 address with
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 5d446b8..9db63db 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -47,7 +47,7 @@
     private static final String RTM_NEWLINK_HEX =
             "64000000100000000000000000000000"   // struct nlmsghr
             + "000001001E0000000210000000000000" // struct ifinfo
-            + "0A000300776C616E30000000"         // IFLA_IFNAME
+            + "0A000300776C616E30000000"         // IFLA_IFNAME(wlan0)
             + "08000D00B80B0000"                 // IFLA_PROTINFO
             + "0500100002000000"                 // IFLA_OPERSTATE
             + "0500110001000000"                 // IFLA_LINKMODE
@@ -88,12 +88,47 @@
         assertTrue(linkMsg.getInterfaceName().equals("wlan0"));
     }
 
+    /**
+     * Example:
+     * # adb shell ip tunnel add トン0 mode sit local any remote 8.8.8.8
+     * # adb shell ip link show | grep トン
+     * 33: トン0@NONE: <POINTOPOINT,NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group
+     *     default qlen 1000
+     *
+     * IFLA_IFNAME attribute: \x0c\x00\x03\x00\xe3\x83\x88\xe3\x83\xb3\x30\x00
+     *     length: 0x000c
+     *     type: 0x0003
+     *     value: \xe3\x83\x88\xe3\x83\xb3\x30\x00
+     *            ト (\xe3\x83\x88)
+     *            ン (\xe3\x83\xb3)
+     *            0  (\x30)
+     *            null terminated (\x00)
+     */
+    private static final String RTM_NEWLINK_UTF8_HEX =
+            "34000000100000000000000000000000"   // struct nlmsghr
+            + "000001001E0000000210000000000000" // struct ifinfo
+            + "08000400DC050000"                 // IFLA_MTU
+            + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
+            + "0C000300E38388E383B33000";        // IFLA_IFNAME(トン0)
+
+    @Test
+    public void testParseRtmNewLink_utf8Ifname() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_UTF8_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkLinkMessage);
+        final RtNetlinkLinkMessage linkMsg = (RtNetlinkLinkMessage) msg;
+
+        assertTrue(linkMsg.getInterfaceName().equals("トン0"));
+    }
+
     private static final String RTM_NEWLINK_PACK_HEX =
             "34000000100000000000000000000000"   // struct nlmsghr
             + "000001001E0000000210000000000000" // struct ifinfo
             + "08000400DC050000"                 // IFLA_MTU
             + "0A00010092C3E3C9374E0000"         // IFLA_ADDRESS
-            + "0A000300776C616E30000000";        // IFLA_IFNAME
+            + "0A000300776C616E30000000";        // IFLA_IFNAME(wlan0)
 
     @Test
     public void testPackRtmNewLink() {
@@ -117,7 +152,7 @@
             + "0500100002000000"                 // IFLA_OPERSTATE
             + "0800010092C3E3C9"                 // IFLA_ADDRESS(truncated)
             + "0500110001000000"                 // IFLA_LINKMODE
-            + "0A000300776C616E30000000"         // IFLA_IFNAME
+            + "0A000300776C616E30000000"         // IFLA_IFNAME(wlan0)
             + "08000400DC050000";                // IFLA_MTU
 
     @Test
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java
index 57248ea..beed838 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPref64Test.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import android.annotation.SuppressLint;
 import android.net.IpPrefix;
 
 import androidx.test.filters.SmallTest;
@@ -51,6 +52,7 @@
         return prefixBytes;
     }
 
+    @SuppressLint("NewApi")
     private static IpPrefix prefix(String addrString, int prefixLength) throws Exception {
         return new IpPrefix(InetAddress.getByName(addrString), prefixLength);
     }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index 72e179b..af3fac2 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -16,6 +16,7 @@
 
 package com.android.net.module.util.netlink;
 
+import static com.android.net.module.util.netlink.RtNetlinkAddressMessage.IFA_FLAGS;
 import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_ADDRESS;
 import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_IFNAME;
 
@@ -35,6 +36,7 @@
 public class StructNlAttrTest {
     private static final MacAddress TEST_MAC_ADDRESS = MacAddress.fromString("00:11:22:33:44:55");
     private static final String TEST_INTERFACE_NAME = "wlan0";
+    private static final int TEST_ADDR_FLAGS = 0x80;
 
     @Test
     public void testGetValueAsMacAddress() {
@@ -65,4 +67,29 @@
         final String str2 = attr2.getValueAsString();
         assertEquals(str2, TEST_INTERFACE_NAME);
     }
+
+    @Test
+    public void testGetValueAsIntger() {
+        final StructNlAttr attr1 = new StructNlAttr(IFA_FLAGS, TEST_ADDR_FLAGS);
+        final Integer integer1 = attr1.getValueAsInteger();
+        final int int1 = attr1.getValueAsInt(0x08 /* default value */);
+        assertEquals(integer1, new Integer(TEST_ADDR_FLAGS));
+        assertEquals(int1, TEST_ADDR_FLAGS);
+
+        // Malformed attribute.
+        final byte[] malformed_int = new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0x80, };
+        final StructNlAttr attr2 = new StructNlAttr(IFA_FLAGS, malformed_int);
+        final Integer integer2 = attr2.getValueAsInteger();
+        final int int2 = attr2.getValueAsInt(0x08 /* default value */);
+        assertNull(integer2);
+        assertEquals(int2, 0x08 /* default value */);
+
+        // Null attribute value.
+        final byte[] null_int = null;
+        final StructNlAttr attr3 = new StructNlAttr(IFA_FLAGS, null_int);
+        final Integer integer3 = attr3.getValueAsInteger();
+        final int int3 = attr3.getValueAsInt(0x08 /* default value */);
+        assertNull(integer3);
+        assertEquals(int3, 0x08 /* default value */);
+    }
 }
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 1be64c1..1a1328f 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -28,9 +28,11 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "net-utils-device-common-bpf",  // TestBpfMap extends IBpfMap.
     ],
     static_libs: [
         "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
         "kotlin-reflect",
         "libnanohttpd",
         "net-tests-utils-host-device-common",
@@ -79,6 +81,6 @@
         "host/**/*.kt",
     ],
     libs: ["tradefed"],
-    test_suites: ["device-tests", "general-tests", "cts", "mts"],
+    test_suites: ["device-tests", "general-tests", "cts", "mts-networking"],
     data: [":ConnectivityChecker"],
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 201bf2d..8b58e71 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -18,11 +18,15 @@
 
 import android.os.Build
 import com.android.modules.utils.build.SdkLevel
+import kotlin.test.fail
 import org.junit.Assume.assumeTrue
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
+// TODO: Remove it when Build.VERSION_CODES.SC_V2 is available
+const val SC_V2 = 32
+
 /**
  * Returns true if the development SDK version of the device is in the provided range.
  *
@@ -40,8 +44,10 @@
     // For recent SDKs that still have development builds used for testing, use SdkLevel utilities
     // instead of SDK_INT.
     return when (minExclusive) {
-        // TODO: use Build.VERSION_CODES.S when it is not CURRENT_DEVELOPMENT
-        31 -> SdkLevel.isAtLeastT()
+        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
+        SC_V2 -> SdkLevel.isAtLeastT()
+        // TODO: To use SdkLevel.isAtLeastSv2 when available
+        Build.VERSION_CODES.S -> fail("Do you expect to ignore the test until T? Use SC_V2 instead")
         Build.VERSION_CODES.R -> SdkLevel.isAtLeastS()
         // Development builds of SDK versions <= R are not used anymore
         else -> Build.VERSION.SDK_INT > minExclusive
@@ -50,8 +56,11 @@
 
 private fun isDevSdkUpTo(maxInclusive: Int): Boolean {
     return when (maxInclusive) {
-        // TODO: use Build.VERSION_CODES.S when it is not CURRENT_DEVELOPMENT
-        31 -> !SdkLevel.isAtLeastT()
+        // TODO: Use Build.VERSION_CODES.SC_V2 when it is available
+        SC_V2 -> !SdkLevel.isAtLeastT()
+        // TODO: To use SdkLevel.isAtLeastSv2 when available
+        Build.VERSION_CODES.S ->
+                fail("Do you expect to ignore the test before T? Use SC_V2 instead")
         Build.VERSION_CODES.R -> !SdkLevel.isAtLeastS()
         // Development builds of SDK versions <= R are not used anymore
         else -> Build.VERSION.SDK_INT <= maxInclusive
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
new file mode 100644
index 0000000..f2ad1e2
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Utilities for testing output of service dumps.
+ */
+public class DumpTestUtils {
+
+    private static String dumpService(String serviceName, boolean adoptPermission, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        final IBinder ib = ServiceManager.getService(serviceName);
+        FileDescriptor[] pipe = Os.pipe();
+
+        // Start a thread to read the dump output, or dump might block if it fills the pipe.
+        final CountDownLatch latch = new CountDownLatch(1);
+        AtomicReference<String> output = new AtomicReference<>();
+        // Used to send exceptions back to the main thread to ensure that the test fails cleanly.
+        AtomicReference<Exception> exception = new AtomicReference<>();
+        new Thread(() -> {
+            try {
+                output.set(Streams.readFully(
+                        new InputStreamReader(new FileInputStream(pipe[0]),
+                                StandardCharsets.UTF_8)));
+                latch.countDown();
+            } catch (Exception e) {
+                exception.set(e);
+                latch.countDown();
+            }
+        }).start();
+
+        final int timeoutMs = 5_000;
+        final String what = "service '" + serviceName + "' with args: " + Arrays.toString(args);
+        try {
+            if (adoptPermission) {
+                runWithShellPermissionIdentity(() -> ib.dump(pipe[1], args),
+                        android.Manifest.permission.DUMP);
+            } else {
+                ib.dump(pipe[1], args);
+            }
+            IoUtils.closeQuietly(pipe[1]);
+            assertTrue("Dump of " + what + " timed out after " + timeoutMs + "ms",
+                    latch.await(timeoutMs, TimeUnit.MILLISECONDS));
+        } finally {
+            // Closing the fds will terminate the thread if it's blocked on read.
+            IoUtils.closeQuietly(pipe[0]);
+            if (pipe[1].valid()) IoUtils.closeQuietly(pipe[1]);
+        }
+        if (exception.get() != null) {
+            fail("Exception dumping " + what + ": " + exception.get());
+        }
+        return output.get();
+    }
+
+    /**
+     * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+     * with the specified args and a pipe, then reads from the pipe in a separate thread.
+     * The current process must already have the DUMP permission.
+     *
+     * @param serviceName the service to dump.
+     * @param args the arguments to pass to the dump function.
+     * @return The dump text.
+     * @throws RemoteException dumping the service failed.
+     * @throws InterruptedException the dump timed out.
+     * @throws ErrnoException opening or closing the pipe for the dump failed.
+     */
+    public static String dumpService(String serviceName, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        return dumpService(serviceName, false, args);
+    }
+
+    /**
+     * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+     * with the specified args and a pipe, then reads from the pipe in a separate thread.
+     * Adopts the {@code DUMP} permission via {@code adoptShellPermissionIdentity} and then releases
+     * it. This method should not be used if the caller already has the shell permission identity.
+     * TODO: when Q and R are no longer supported, use
+     * {@link android.app.UiAutomation#getAdoptedShellPermissions} to automatically acquire the
+     * shell permission if the caller does not already have it.
+     *
+     * @param serviceName the service to dump.
+     * @param args the arguments to pass to the dump function.
+     * @return The dump text.
+     * @throws RemoteException dumping the service failed.
+     * @throws InterruptedException the dump timed out.
+     * @throws ErrnoException opening or closing the pipe for the dump failed.
+     */
+    public static String dumpServiceWithShellPermission(String serviceName, String... args)
+            throws RemoteException, InterruptedException, ErrnoException {
+        return dumpService(serviceName, true, args);
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
new file mode 100644
index 0000000..5614a99
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+/**
+ *
+ * Fake BPF map class for tests that have no no privilege to access real BPF maps. All member
+ * functions which eventually call JNI to access the real native BPF map are overridden.
+ *
+ * Inherits from BpfMap instead of implementing IBpfMap so that any class using a BpfMap can use
+ * this class in its tests.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+    private final HashMap<K, V> mMap = new HashMap<K, V>();
+
+    public TestBpfMap(final Class<K> key, final Class<V> value) {
+        super(key, value);
+    }
+
+    @Override
+    public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+        // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+        // implement the entry deletion in the iteration if required.
+        for (Map.Entry<K, V> entry : mMap.entrySet()) {
+            action.accept(entry.getKey(), entry.getValue());
+        }
+    }
+
+    @Override
+    public void updateEntry(K key, V value) throws ErrnoException {
+        mMap.put(key, value);
+    }
+
+    @Override
+    public void insertEntry(K key, V value) throws ErrnoException,
+            IllegalArgumentException {
+        // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+        if (mMap.get(key) != null) {
+            throw new IllegalArgumentException(key + " already exist");
+        }
+        mMap.put(key, value);
+    }
+
+    @Override
+    public void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException {
+        if (!mMap.containsKey(key)) throw new NoSuchElementException();
+        mMap.put(key, value);
+    }
+
+    @Override
+    public boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+        // Returns true if inserted, false if replaced.
+        boolean ret = !mMap.containsKey(key);
+        mMap.put(key, value);
+        return ret;
+    }
+
+    @Override
+    public boolean deleteEntry(Struct key) throws ErrnoException {
+        return mMap.remove(key) != null;
+    }
+
+    @Override
+    public boolean isEmpty() throws ErrnoException {
+        return mMap.isEmpty();
+    }
+
+    @Override
+    public K getNextKey(@NonNull K key) {
+        // Expensive, but since this is only for tests...
+        Iterator<K> it = mMap.keySet().iterator();
+        while (it.hasNext()) {
+            if (Objects.equals(it.next(), key)) {
+                return it.hasNext() ? it.next() : null;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public K getFirstKey() {
+        for (K key : mMap.keySet()) {
+            return key;
+        }
+        return null;
+    }
+
+    @Override
+    public boolean containsKey(@NonNull K key) throws ErrnoException {
+        return mMap.containsKey(key);
+    }
+
+    @Override
+    public V getValue(@NonNull K key) throws ErrnoException {
+        // Return value for a given key. Otherwise, return null without an error ENOENT.
+        // BpfMap#getValue treats that the entry is not found as no error.
+        return mMap.get(key);
+    }
+
+    @Override
+    public void clear() throws ErrnoException {
+        // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+        mMap.clear();
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
new file mode 100644
index 0000000..8dc1bc4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 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.KeepalivePacketData
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.QosFilter
+import android.net.Uri
+import android.os.Looper
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRegisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Assert.assertArrayEquals
+
+// Any legal score (0~99) for the test network would do, as it is going to be kept up by the
+// requests filed by the test and should never match normal internet requests. 70 is the default
+// score of Ethernet networks, it's as good a value as any other.
+private const val TEST_NETWORK_SCORE = 70
+
+private class Provider(context: Context, looper: Looper) :
+            NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
+
+public open class TestableNetworkAgent(
+    context: Context,
+    looper: Looper,
+    val nc: NetworkCapabilities,
+    val lp: LinkProperties,
+    conf: NetworkAgentConfig
+) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
+        nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+
+    val DEFAULT_TIMEOUT_MS = 5000L
+
+    val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+    sealed class CallbackEntry {
+        object OnBandwidthUpdateRequested : CallbackEntry()
+        object OnNetworkUnwanted : CallbackEntry()
+        data class OnAddKeepalivePacketFilter(
+            val slot: Int,
+            val packet: KeepalivePacketData
+        ) : CallbackEntry()
+        data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry()
+        data class OnStartSocketKeepalive(
+            val slot: Int,
+            val interval: Int,
+            val packet: KeepalivePacketData
+        ) : CallbackEntry()
+        data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry()
+        data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry()
+        object OnAutomaticReconnectDisabled : CallbackEntry()
+        data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
+        data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+        object OnNetworkCreated : CallbackEntry()
+        object OnNetworkDestroyed : CallbackEntry()
+        data class OnDscpPolicyStatusUpdated(val policyId: Int, val status: Int) : CallbackEntry()
+        data class OnRegisterQosCallback(
+            val callbackId: Int,
+            val filter: QosFilter
+        ) : CallbackEntry()
+        data class OnUnregisterQosCallback(val callbackId: Int) : CallbackEntry()
+    }
+
+    override fun onBandwidthUpdateRequested() {
+        history.add(OnBandwidthUpdateRequested)
+    }
+
+    override fun onNetworkUnwanted() {
+        history.add(OnNetworkUnwanted)
+    }
+
+    override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) {
+        history.add(OnAddKeepalivePacketFilter(slot, packet))
+    }
+
+    override fun onRemoveKeepalivePacketFilter(slot: Int) {
+        history.add(OnRemoveKeepalivePacketFilter(slot))
+    }
+
+    override fun onStartSocketKeepalive(
+        slot: Int,
+        interval: Duration,
+        packet: KeepalivePacketData
+    ) {
+        history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet))
+    }
+
+    override fun onStopSocketKeepalive(slot: Int) {
+        history.add(OnStopSocketKeepalive(slot))
+    }
+
+    override fun onSaveAcceptUnvalidated(accept: Boolean) {
+        history.add(OnSaveAcceptUnvalidated(accept))
+    }
+
+    override fun onAutomaticReconnectDisabled() {
+        history.add(OnAutomaticReconnectDisabled)
+    }
+
+    override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) {
+        history.add(OnSignalStrengthThresholdsUpdated(thresholds))
+    }
+
+    fun expectSignalStrengths(thresholds: IntArray? = intArrayOf()) {
+        expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+            assertArrayEquals(thresholds, it.thresholds)
+        }
+    }
+
+    override fun onQosCallbackRegistered(qosCallbackId: Int, filter: QosFilter) {
+        history.add(OnRegisterQosCallback(qosCallbackId, filter))
+    }
+
+    override fun onQosCallbackUnregistered(qosCallbackId: Int) {
+        history.add(OnUnregisterQosCallback(qosCallbackId))
+    }
+
+    override fun onValidationStatus(status: Int, uri: Uri?) {
+        history.add(OnValidationStatus(status, uri))
+    }
+
+    override fun onNetworkCreated() {
+        history.add(OnNetworkCreated)
+    }
+
+    override fun onNetworkDestroyed() {
+        history.add(OnNetworkDestroyed)
+    }
+
+    override fun onDscpPolicyStatusUpdated(policyId: Int, status: Int) {
+        history.add(OnDscpPolicyStatusUpdated(policyId, status))
+    }
+
+    // Expects the initial validation event that always occurs immediately after registering
+    // a NetworkAgent whose network does not require validation (which test networks do
+    // not, since they lack the INTERNET capability). It always contains the default argument
+    // for the URI.
+    fun expectValidationBypassedStatus() = expectCallback<OnValidationStatus>().let {
+        assertEquals(it.status, VALID_NETWORK)
+        // The returned Uri is parsed from the empty string, which means it's an
+        // instance of the (private) Uri.StringUri. There are no real good ways
+        // to check this, the least bad is to just convert it to a string and
+        // make sure it's empty.
+        assertEquals("", it.uri.toString())
+    }
+
+    inline fun <reified T : CallbackEntry> expectCallback(): T {
+        val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+        return foundCallback
+    }
+
+    inline fun <reified T : CallbackEntry> expectCallback(valid: (T) -> Boolean) {
+        val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+        assertTrue(valid(foundCallback), "Unexpected callback : $foundCallback")
+    }
+
+    inline fun <reified T : CallbackEntry> eventuallyExpect() =
+            history.poll(DEFAULT_TIMEOUT_MS) { it is T }.also {
+                assertNotNull(it, "Callback ${T::class} not received")
+    } as T
+
+    fun assertNoCallback() {
+        assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
+                "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
+        assertNull(history.peek())
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 7d851f1..c2b5a5c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -330,13 +330,13 @@
 
     fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) {
         expectCallback<BlockedStatus>(net, tmt).also {
-            assertEquals(it.blocked, blocked, "Unexpected blocked status ${it.blocked}")
+            assertEquals(blocked, it.blocked, "Unexpected blocked status ${it.blocked}")
         }
     }
 
     fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) {
         expectCallback<BlockedStatusInt>(net, tmt).also {
-            assertEquals(it.blocked, blocked, "Unexpected blocked status ${it.blocked}")
+            assertEquals(blocked, it.blocked, "Unexpected blocked status ${it.blocked}")
         }
     }
 
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
index be5c9b2..4a7b351 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
@@ -17,6 +17,7 @@
 package com.android.testutils
 
 import android.net.netstats.provider.NetworkStatsProvider
+import android.util.Log
 import com.android.net.module.util.ArrayTrackRecord
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
@@ -43,23 +44,28 @@
         data class OnSetAlert(val quotaBytes: Long) : CallbackType()
     }
 
+    private val TAG = this::class.simpleName
     val history = ArrayTrackRecord<CallbackType>().newReadHead()
     // See ReadHead#mark
     val mark get() = history.mark
 
     override fun onRequestStatsUpdate(token: Int) {
+        Log.d(TAG, "onRequestStatsUpdate $token")
         history.add(CallbackType.OnRequestStatsUpdate(token))
     }
 
     override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        Log.d(TAG, "onSetWarningAndLimit $iface $warningBytes $limitBytes")
         history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
     }
 
     override fun onSetLimit(iface: String, quotaBytes: Long) {
+        Log.d(TAG, "onSetLimit $iface $quotaBytes")
         history.add(CallbackType.OnSetLimit(iface, quotaBytes))
     }
 
     override fun onSetAlert(quotaBytes: Long) {
+        Log.d(TAG, "onSetAlert $quotaBytes")
         history.add(CallbackType.OnSetAlert(quotaBytes))
     }