diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..27357f88d612d1078010ebee00d8d2840dee1c34
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "bpf_tethering.h"
+
+namespace android {
+
+static jobjectArray getBpfCounterNames(JNIEnv *env) {
+    size_t size = BPF_TETHER_ERR__MAX;
+    jobjectArray ret = env->NewObjectArray(size, env->FindClass("java/lang/String"), nullptr);
+    for (int i = 0; i < size; i++) {
+        env->SetObjectArrayElement(ret, i, env->NewStringUTF(bpf_tether_errors[i]));
+    }
+    return ret;
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "getBpfCounterNames", "()[Ljava/lang/String;", (void*) getBpfCounterNames },
+};
+
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env) {
+    return jniRegisterNativeMethods(env,
+            "com/android/networkstack/tethering/BpfCoordinator",
+            gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp
index 3766de90762de5979b31cbbd679e839de4476b9e..e31da60ef5a17a1110bbe8ec1a51bae0a12b824c 100644
--- a/Tethering/jni/onload.cpp
+++ b/Tethering/jni/onload.cpp
@@ -24,6 +24,7 @@ namespace android {
 
 int register_android_net_util_TetheringUtils(JNIEnv* env);
 int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
@@ -36,6 +37,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
 
     if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR;
 
+    if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR;
+
     return JNI_VERSION_1_6;
 }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 4ba51308b84adbb4a47e5ce26df46339c99736d9..985328fe604be49a751d838661a05575e0db814c 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -59,6 +59,7 @@ import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
 import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 
 import java.net.Inet4Address;
@@ -84,6 +85,13 @@ import java.util.Set;
  * @hide
  */
 public class BpfCoordinator {
+    // Ensure the JNI code is loaded. In production this will already have been loaded by
+    // TetherService, but for tests it needs to be either loaded here or loaded by every test.
+    // TODO: is there a better way?
+    static {
+        System.loadLibrary("tetherutilsjni");
+    }
+
     static final boolean DOWNSTREAM = true;
     static final boolean UPSTREAM = false;
 
@@ -97,6 +105,10 @@ public class BpfCoordinator {
     private static final String TETHER_UPSTREAM6_FS_PATH = makeMapPath(UPSTREAM, 6);
     private static final String TETHER_STATS_MAP_PATH = makeMapPath("stats");
     private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
+    private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
+
+    /** The names of all the BPF counters defined in bpf_tethering.h. */
+    public static final String[] sBpfCounterNames = getBpfCounterNames();
 
     private static String makeMapPath(String which) {
         return "/sys/fs/bpf/tethering/map_offload_tether_" + which + "_map";
@@ -717,6 +729,12 @@ public class BpfCoordinator {
             dumpIpv4ForwardingRules(pw);
             pw.decreaseIndent();
 
+            pw.println();
+            pw.println("Forwarding counters:");
+            pw.increaseIndent();
+            dumpCounters(pw);
+            pw.decreaseIndent();
+
             dumpDone.open();
         });
         if (!dumpDone.block(DUMP_TIMEOUT_MS)) {
@@ -814,6 +832,36 @@ public class BpfCoordinator {
         pw.decreaseIndent();
     }
 
+    /**
+     * Simple struct that only contains a u32. Must be public because Struct needs access to it.
+     * TODO: make this a public inner class of Struct so anyone can use it as, e.g., Struct.U32?
+     */
+    public static class U32Struct extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.U32)
+        public long val;
+    }
+
+    private void dumpCounters(@NonNull IndentingPrintWriter pw) {
+        try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH,
+                BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) {
+
+            map.forEach((k, v) -> {
+                String counterName;
+                try {
+                    counterName = sBpfCounterNames[(int) k.val];
+                } catch (IndexOutOfBoundsException e) {
+                    // Should never happen because this code gets the counter name from the same
+                    // include file as the BPF program that increments the counter.
+                    Log.wtf(TAG, "Unknown tethering counter type " + k.val);
+                    counterName = Long.toString(k.val);
+                }
+                if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
+            });
+        } catch (ErrnoException e) {
+            pw.println("Error dumping counter map: " + e);
+        }
+    }
+
     /** IPv6 forwarding rule class. */
     public static class Ipv6ForwardingRule {
         public final int upstreamIfindex;
@@ -1296,4 +1344,6 @@ public class BpfCoordinator {
     final SparseArray<String> getInterfaceNamesForTesting() {
         return mInterfaceNames;
     }
+
+    private static native String[] getBpfCounterNames();
 }
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 5e4fe524f269180506a32b00140055e7fc06e3b6..6c479a0971e95db919aa3e8c3314b2401bdb214c 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -69,6 +69,7 @@ java_defaults {
         // For mockito extended
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
+        "libtetherutilsjni",
     ],
 }