Snap for 8164116 from 7256ac389c31e7e71ac86b016eaa909f055fa637 to mainline-resolv-release

Change-Id: I91e7293d68444fbe11f4f79ca31e892863612d55
diff --git a/common/Android.bp b/common/Android.bp
index 50cfb02..047d51e 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -111,21 +111,24 @@
     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",
     ],
-    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",
@@ -143,7 +146,7 @@
         "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__",
@@ -155,6 +158,7 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
         "com.android.tethering",
@@ -168,7 +172,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__",
@@ -180,6 +184,7 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
         "com.android.tethering",
@@ -223,11 +228,12 @@
     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",
+    ],
     jarjar_rules: "jarjar-rules-shared.txt",
     visibility: [
         "//cts/tests/tests/net",
@@ -246,6 +252,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 },
 }
@@ -277,8 +284,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 },
 }
diff --git a/common/device/com/android/net/module/util/BpfMap.java b/common/device/com/android/net/module/util/BpfMap.java
index 5f05c7c..b42c388 100644
--- a/common/device/com/android/net/module/util/BpfMap.java
+++ b/common/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/common/device/com/android/net/module/util/IBpfMap.java b/common/device/com/android/net/module/util/IBpfMap.java
new file mode 100644
index 0000000..708cf61
--- /dev/null
+++ b/common/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/common/device/com/android/net/module/util/Struct.java b/common/device/com/android/net/module/util/Struct.java
index b43e2c4..d717bc7 100644
--- a/common/device/com/android/net/module/util/Struct.java
+++ b/common/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/common/device/com/android/net/module/util/TcUtils.java b/common/device/com/android/net/module/util/TcUtils.java
new file mode 100644
index 0000000..cf01490
--- /dev/null
+++ b/common/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/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
index 71a0c96..26c24f8 100644
--- a/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
+++ b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -85,7 +85,7 @@
       * and {@code FORCE_RESTRICTED_CAPABILITIES}.
      */
     @VisibleForTesting
-    static final long RESTRICTED_CAPABILITIES = packBitList(
+    public static final long RESTRICTED_CAPABILITIES = packBitList(
             NET_CAPABILITY_BIP,
             NET_CAPABILITY_CBS,
             NET_CAPABILITY_DUN,
@@ -115,7 +115,7 @@
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
      */
     @VisibleForTesting
-    static final long UNRESTRICTED_CAPABILITIES = packBitList(
+    public static final long UNRESTRICTED_CAPABILITIES = packBitList(
             NET_CAPABILITY_INTERNET,
             NET_CAPABILITY_MMS,
             NET_CAPABILITY_SUPL,
diff --git a/common/framework/com/android/net/module/util/NetworkStatsUtils.java b/common/framework/com/android/net/module/util/NetworkStatsUtils.java
index 28ff770..41a9428 100644
--- a/common/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/common/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -16,12 +16,23 @@
 
 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>
@@ -88,4 +99,77 @@
         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/common/native/bpf_headers/Android.bp b/common/native/bpf_headers/Android.bp
index 06ba1b0..834ef02 100644
--- a/common/native/bpf_headers/Android.bp
+++ b/common/native/bpf_headers/Android.bp
@@ -18,7 +18,7 @@
 
 cc_library_headers {
     name: "bpf_headers",
-    vendor_available: false,
+    vendor_available: true,
     host_supported: true,
     native_bridge_supported: true,
     header_libs: ["bpf_syscall_wrappers"],
@@ -37,6 +37,7 @@
     ],
     visibility: [
         "//bootable/libbootloader/vts",
+        "//cts/tests/tests/net/native",
         "//frameworks/base/services/core/jni",
         "//frameworks/native/libs/cputimeinstate",
         "//frameworks/native/services/gpuservice",
@@ -45,12 +46,13 @@
         "//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/common/native/bpf_headers/include/bpf/bpf_helpers.h b/common/native/bpf_headers/include/bpf/bpf_helpers.h
index 878bb10..ac9f9bc 100644
--- a/common/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/common/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -116,6 +116,15 @@
 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 = {                                         \
@@ -132,6 +141,7 @@
             .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) {                                                                \
diff --git a/common/native/bpf_headers/include/bpf/bpf_map_def.h b/common/native/bpf_headers/include/bpf/bpf_map_def.h
index 02b2096..1371668 100644
--- a/common/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/common/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -23,7 +23,7 @@
 #include <linux/bpf.h>
 
 // Pull in AID_* constants from //system/core/libcutils/include/private/android_filesystem_config.h
-#include <private/android_filesystem_config.h>
+#include <cutils/android_filesystem_config.h>
 
 /******************************************************************************
  *                                                                            *
diff --git a/common/native/bpf_syscall_wrappers/Android.bp b/common/native/bpf_syscall_wrappers/Android.bp
index 088bcbb..a20eed3 100644
--- a/common/native/bpf_syscall_wrappers/Android.bp
+++ b/common/native/bpf_syscall_wrappers/Android.bp
@@ -18,7 +18,7 @@
 
 cc_library_headers {
     name: "bpf_syscall_wrappers",
-    vendor_available: false,
+    vendor_available: true,
     host_supported: true,
     native_bridge_supported: true,
     export_include_dirs: ["include"],
@@ -39,6 +39,8 @@
         "//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/common/native/bpfmapjni/Android.bp b/common/native/bpfmapjni/Android.bp
index b7af22d..cd254d4 100644
--- a/common/native/bpfmapjni/Android.bp
+++ b/common/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/common/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/common/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
new file mode 100644
index 0000000..2307a6b
--- /dev/null
+++ b/common/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/common/native/nettestutils/Android.bp b/common/native/nettestutils/Android.bp
new file mode 100644
index 0000000..42df8e0
--- /dev/null
+++ b/common/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/common/native/nettestutils/DumpService.cpp b/common/native/nettestutils/DumpService.cpp
new file mode 100644
index 0000000..ba3d77e
--- /dev/null
+++ b/common/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/common/native/nettestutils/include/nettestutils/DumpService.h b/common/native/nettestutils/include/nettestutils/DumpService.h
new file mode 100644
index 0000000..2a72181
--- /dev/null
+++ b/common/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/common/native/tcutils/Android.bp b/common/native/tcutils/Android.bp
index 01b2424..e819e4c 100644
--- a/common/native/tcutils/Android.bp
+++ b/common/native/tcutils/Android.bp
@@ -38,6 +38,31 @@
     ],
     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/common/native/tcutils/include/tcutils/tcutils.h b/common/native/tcutils/include/tcutils/tcutils.h
index d1e1bb7..a8ec2e8 100644
--- a/common/native/tcutils/include/tcutils/tcutils.h
+++ b/common/native/tcutils/include/tcutils/tcutils.h
@@ -17,12 +17,31 @@
 #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/common/native/tcutils/kernelversion.h b/common/native/tcutils/kernelversion.h
new file mode 100644
index 0000000..3be1ad2
--- /dev/null
+++ b/common/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/common/native/tcutils/logging.h b/common/native/tcutils/logging.h
new file mode 100644
index 0000000..70604b3
--- /dev/null
+++ b/common/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/common/native/tcutils/tcutils.cpp b/common/native/tcutils/tcutils.cpp
index ca1c63c..0e17f67 100644
--- a/common/native/tcutils/tcutils.cpp
+++ b/common/native/tcutils/tcutils.cpp
@@ -18,12 +18,12 @@
 
 #include "tcutils/tcutils.h"
 
+#include "logging.h"
+#include "kernelversion.h"
 #include "scopeguard.h"
 
-#include <android/log.h>
 #include <arpa/inet.h>
 #include <cerrno>
-#include <cstdio>
 #include <cstring>
 #include <libgen.h>
 #include <linux/if_arp.h>
@@ -32,10 +32,10 @@
 #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 <stdarg.h>
+#include <stdio.h>
 #include <sys/socket.h>
-#include <sys/utsname.h>
 #include <unistd.h>
 #include <utility>
 
@@ -52,12 +52,318 @@
 namespace android {
 namespace {
 
-void logError(const char *fmt...) {
-  va_list args;
-  va_start(args, fmt);
-  __android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args);
-  va_end(args);
-}
+/**
+ * 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;
@@ -67,7 +373,7 @@
   int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
   if (fd == -1) {
     int error = errno;
-    logError("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d",
+    ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d",
              error);
     return -error;
   }
@@ -76,7 +382,7 @@
   static constexpr int on = 1;
   if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
     int error = errno;
-    logError("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error);
+    ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error);
     return -error;
   }
 
@@ -84,7 +390,7 @@
   if (bind(fd, (const struct sockaddr *)&KERNEL_NLADDR,
            sizeof(KERNEL_NLADDR))) {
     int error = errno;
-    logError("bind(fd, {AF_NETLINK, 0, 0}: %d)", error);
+    ALOGE("bind(fd, {AF_NETLINK, 0, 0}: %d)", error);
     return -error;
   }
 
@@ -92,7 +398,7 @@
   if (connect(fd, (const struct sockaddr *)&KERNEL_NLADDR,
               sizeof(KERNEL_NLADDR))) {
     int error = errno;
-    logError("connect(fd, {AF_NETLINK, 0, 0}): %d", error);
+    ALOGE("connect(fd, {AF_NETLINK, 0, 0}): %d", error);
     return -error;
   }
 
@@ -100,12 +406,12 @@
 
   if (rv == -1) {
     int error = errno;
-    logError("send(fd, req, len, 0) failed: %d", error);
+    ALOGE("send(fd, req, len, 0) failed: %d", error);
     return -error;
   }
 
   if (rv != len) {
-    logError("send(fd, req, len = %d, 0) returned invalid message size %d", len,
+    ALOGE("send(fd, req, len = %d, 0) returned invalid message size %d", len,
              rv);
     return -EMSGSIZE;
   }
@@ -120,29 +426,29 @@
 
   if (rv == -1) {
     int error = errno;
-    logError("recv() failed: %d", error);
+    ALOGE("recv() failed: %d", error);
     return -error;
   }
 
   if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
-    logError("recv() returned short packet: %d", rv);
+    ALOGE("recv() returned short packet: %d", rv);
     return -EBADMSG;
   }
 
   if (resp.h.nlmsg_len != (unsigned)rv) {
-    logError("recv() returned invalid header length: %d != %d",
+    ALOGE("recv() returned invalid header length: %d != %d",
              resp.h.nlmsg_len, rv);
     return -EBADMSG;
   }
 
   if (resp.h.nlmsg_type != NLMSG_ERROR) {
-    logError("recv() did not return NLMSG_ERROR message: %d",
+    ALOGE("recv() did not return NLMSG_ERROR message: %d",
              resp.h.nlmsg_type);
     return -ENOMSG;
   }
 
   if (resp.e.error) {
-    logError("NLMSG_ERROR message return error: %d", resp.e.error);
+    ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
   }
   return resp.e.error; // returns 0 on success
 }
@@ -168,49 +474,14 @@
   return ifr.ifr_hwaddr.sa_family;
 }
 
-// -----------------------------------------------------------------------------
-// TODO - just use BpfUtils.h once that is available in sc-mainline-prod and has
-// kernelVersion()
-//
-// In the mean time copying verbatim from:
-//   system/bpf/libbpf_android/include/bpf/BpfUtils.h
-// and
-//   system/bpf/libbpf_android/BpfUtils.cpp
-
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-
-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);
-}
-
-bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) {
-  return kernelVersion() >= KVER(major, minor, sub);
-}
-// -----------------------------------------------------------------------------
-
 } // namespace
 
 int isEthernet(const char *iface, bool &isEthernet) {
   int rv = hardwareAddressType(iface);
   if (rv < 0) {
-    logError("Get hardware address type of interface %s failed: %s", iface,
+    ALOGE("Get hardware address type of interface %s failed: %s", iface,
              strerror(-rv));
-    return -rv;
+    return rv;
   }
 
   // Backwards compatibility with pre-GKI kernels that use various custom
@@ -242,18 +513,66 @@
     isEthernet = false;
     return 0;
   default:
-    logError("Unknown hardware address type %d on interface %s", rv, iface);
-    return -ENOENT;
+    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) {
-    logError("retrieveProgram failed: %d", errno);
+    ALOGE("retrieveProgram failed: %d", errno);
     return -errno;
   }
   auto scopeGuard = base::make_scope_guard([bpfFd] { close(bpfFd); });
@@ -355,6 +674,37 @@
   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 {
diff --git a/common/native/tcutils/tests/tcutils_test.cpp b/common/native/tcutils/tests/tcutils_test.cpp
new file mode 100644
index 0000000..32736d6
--- /dev/null
+++ b/common/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/common/netd/Android.bp b/common/netd/Android.bp
index 6a12d64..e249e19 100644
--- a/common/netd/Android.bp
+++ b/common/netd/Android.bp
@@ -48,6 +48,7 @@
     ],
     apex_available: [
         "com.android.resolv",
+        "com.android.tethering",
     ],
     min_sdk_version: "29",
 }
@@ -97,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.
diff --git a/common/netd/libnetdutils/Android.bp b/common/netd/libnetdutils/Android.bp
index 732e37d..08d5412 100644
--- a/common/netd/libnetdutils/Android.bp
+++ b/common/netd/libnetdutils/Android.bp
@@ -11,6 +11,7 @@
         "Log.cpp",
         "Netfilter.cpp",
         "Netlink.cpp",
+        "NetlinkListener.cpp",
         "Slice.cpp",
         "Socket.cpp",
         "SocketOption.cpp",
diff --git a/common/netd/libnetdutils/NetlinkListener.cpp b/common/netd/libnetdutils/NetlinkListener.cpp
new file mode 100644
index 0000000..decaa9c
--- /dev/null
+++ b/common/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/common/netd/libnetdutils/include/netdutils/NetlinkListener.h b/common/netd/libnetdutils/include/netdutils/NetlinkListener.h
new file mode 100644
index 0000000..97f7bb2
--- /dev/null
+++ b/common/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/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index ff839fe..2785ea9 100644
--- a/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -16,12 +16,16 @@
 
 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
@@ -71,4 +75,68 @@
         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/common/testutils/Android.bp b/common/testutils/Android.bp
index 1be64c1..1a1328f 100644
--- a/common/testutils/Android.bp
+++ b/common/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/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 201bf2d..8b58e71 100644
--- a/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/common/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/common/testutils/devicetests/com/android/testutils/DumpTestUtils.java b/common/testutils/devicetests/com/android/testutils/DumpTestUtils.java
new file mode 100644
index 0000000..f2ad1e2
--- /dev/null
+++ b/common/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/common/testutils/devicetests/com/android/testutils/TestBpfMap.java b/common/testutils/devicetests/com/android/testutils/TestBpfMap.java
new file mode 100644
index 0000000..5614a99
--- /dev/null
+++ b/common/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/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 40fb773..8dc1bc4 100644
--- a/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -30,6 +30,7 @@
 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
@@ -89,6 +90,7 @@
         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
@@ -162,6 +164,10 @@
         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
@@ -197,4 +203,4 @@
                 "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
         assertNull(history.peek())
     }
-}
\ No newline at end of file
+}