Merge "Adding bt dependency"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ffbd1fc..780ba26 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -26,6 +26,9 @@
     },
     {
       "name": "traffic_controller_unit_test"
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "postsubmit": [
@@ -42,6 +45,9 @@
     {
       "name": "traffic_controller_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+    },
+    {
+      "name": "libnetworkstats_test"
     }
   ],
   "mainline-presubmit": [
@@ -61,6 +67,9 @@
     },
     {
       "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "mainline-postsubmit": [
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index a3ca90c..fa5af49 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -45,9 +45,8 @@
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
         "//packages/modules/Connectivity/service/native",
+        "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        // TODO: remove system/netd/* when all BPF code is moved out of Netd.
-        "//system/netd/libnetdbpf",
         "//system/netd/server",
         "//system/netd/tests",
     ],
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 8577d9d..f0df97b 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -20,7 +20,6 @@
 #include <linux/if_ether.h>
 #include <linux/in.h>
 #include <linux/in6.h>
-#include <netdutils/UidConstants.h>
 
 // This header file is shared by eBPF kernel programs (C) and netd (C++) and
 // some of the maps are also accessed directly from Java mainline module code.
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 72ee431..f0af8b4 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -25,6 +25,7 @@
 #include <linux/ipv6.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
+#include <netdutils/UidConstants.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include "bpf_net_helpers.h"
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
new file mode 100644
index 0000000..bceeefa
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -0,0 +1,71 @@
+//
+// 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.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libnetworkstats",
+    vendor_available: false,
+    host_supported: false,
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStats.cpp"
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    export_include_dirs: ["include"],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "libnetworkstats_test",
+    test_suites: ["general-tests"],
+    require_root: true,  // required by setrlimitForTest()
+    header_libs: ["bpf_connectivity_headers"],
+    srcs: [
+        "BpfNetworkStatsTest.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    static_libs: ["libgmock"],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libnetworkstats",
+        "libutils",
+    ],
+}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
new file mode 100644
index 0000000..4d605ce
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -0,0 +1,350 @@
+/*
+ * 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.
+ */
+
+#include <inttypes.h>
+#include <net/if.h>
+#include <string.h>
+#include <unordered_set>
+
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "android-base/file.h"
+#include "android-base/strings.h"
+#include "android-base/unique_fd.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+#ifdef LOG_TAG
+#undef LOG_TAG
+#endif
+
+#define LOG_TAG "BpfNetworkStats"
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+
+// The target map for stats reading should be the inactive map, which is opposite
+// from the config value.
+static constexpr char const* STATS_MAP_PATH[] = {STATS_MAP_B_PATH, STATS_MAP_A_PATH};
+
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
+    auto statsEntry = appUidStatsMap.readValue(uid);
+    if (statsEntry.ok()) {
+        stats->rxPackets = statsEntry.value().rxPackets;
+        stats->txPackets = statsEntry.value().txPackets;
+        stats->rxBytes = statsEntry.value().rxBytes;
+        stats->txBytes = statsEntry.value().txBytes;
+    }
+    return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
+                                                                    : -statsEntry.error().code();
+}
+
+int bpfGetUidStats(uid_t uid, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
+
+    if (!appUidStatsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
+        return ret;
+    }
+    return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
+}
+
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    stats->tcpRxPackets = -1;
+    stats->tcpTxPackets = -1;
+    const auto processIfaceStats =
+            [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+                    const uint32_t& key,
+                    const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        if (!iface || !strcmp(iface, ifname)) {
+            Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
+            if (!statsEntry.ok()) {
+                return statsEntry.error();
+            }
+            stats->rxPackets += statsEntry.value().rxPackets;
+            stats->txPackets += statsEntry.value().txPackets;
+            stats->rxBytes += statsEntry.value().rxBytes;
+            stats->txBytes += statsEntry.value().txBytes;
+        }
+        return Result<void>();
+    };
+    auto res = ifaceStatsMap.iterate(processIfaceStats);
+    return res.ok() ? 0 : -res.error().code();
+}
+
+int bpfGetIfaceStats(const char* iface, Stats* stats) {
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    int ret;
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
+                              const char* ifname) {
+    stats_line newLine;
+    strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+    newLine.uid = (int32_t)statsKey.uid;
+    newLine.set = (int32_t)statsKey.counterSet;
+    newLine.tag = (int32_t)statsKey.tag;
+    newLine.rxPackets = statsEntry.rxPackets;
+    newLine.txPackets = statsEntry.txPackets;
+    newLine.rxBytes = statsEntry.rxBytes;
+    newLine.txBytes = statsEntry.txBytes;
+    return newLine;
+}
+
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailUidStats =
+            [lines, &limitIfaces, &limitTag, &limitUid, &unknownIfaceBytesTotal, &ifaceMap](
+                    const StatsKey& key,
+                    const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
+                                &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        std::string ifnameStr(ifname);
+        if (limitIfaces.size() > 0 &&
+            std::find(limitIfaces.begin(), limitIfaces.end(), ifnameStr) == limitIfaces.end()) {
+            // Nothing matched; skip this line.
+            return Result<void>();
+        }
+        if (limitTag != TAG_ALL && uint32_t(limitTag) != key.tag) {
+            return Result<void>();
+        }
+        if (limitUid != UID_ALL && uint32_t(limitUid) != key.uid) {
+            return Result<void>();
+        }
+        Result<StatsValue> statsEntry = statsMap.readValue(key);
+        if (!statsEntry.ok()) {
+            return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
+        }
+        lines->push_back(populateStatsEntry(key, statsEntry.value(), ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterate(processDetailUidStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    // Since eBPF use hash map to record stats, network stats collected from
+    // eBPF will be out of order. And the performance of findIndexHinted in
+    // NetworkStats will also be impacted.
+    //
+    // Furthermore, since the StatsKey contains iface index, the network stats
+    // reported to framework would create items with the same iface, uid, tag
+    // and set, which causes NetworkStats maps wrong item to subtract.
+    //
+    // Thus, the stats needs to be properly sorted and grouped before reported.
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid) {
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+    if (!configurationMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get configuration map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
+    if (!configuration.ok()) {
+        ALOGE("Cannot read the old configuration from map: %s",
+              configuration.error().message().c_str());
+        return -configuration.error().code();
+    }
+    const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+    BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
+    if (!statsMap.isValid()) {
+        int ret = -errno;
+        ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
+        return ret;
+    }
+
+    // It is safe to read and clear the old map now since the
+    // networkStatsFactory should call netd to swap the map in advance already.
+    int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid, statsMap,
+                                                 ifaceIndexNameMap);
+    if (ret) {
+        ALOGE("parse detail network stats failed: %s", strerror(errno));
+        return ret;
+    }
+
+    Result<void> res = statsMap.clear();
+    if (!res.ok()) {
+        ALOGE("Clean up current stats map failed: %s", strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    return 0;
+}
+
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+    int64_t unknownIfaceBytesTotal = 0;
+    const auto processDetailIfaceStats = [lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+                                             const uint32_t& key, const StatsValue& value,
+                                             const BpfMap<uint32_t, StatsValue>&) {
+        char ifname[IFNAMSIZ];
+        if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+            return Result<void>();
+        }
+        StatsKey fakeKey = {
+                .uid = (uint32_t)UID_ALL,
+                .tag = (uint32_t)TAG_NONE,
+                .counterSet = (uint32_t)SET_ALL,
+        };
+        lines->push_back(populateStatsEntry(fakeKey, value, ifname));
+        return Result<void>();
+    };
+    Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
+    if (!res.ok()) {
+        ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+              strerror(res.error().code()));
+        return -res.error().code();
+    }
+
+    groupNetworkStats(lines);
+    return 0;
+}
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
+    int ret = 0;
+    BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+    if (!ifaceIndexNameMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+        return ret;
+    }
+
+    BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+    if (!ifaceStatsMap.isValid()) {
+        ret = -errno;
+        ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+        return ret;
+    }
+    return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+uint64_t combineUidTag(const uid_t uid, const uint32_t tag) {
+    return (uint64_t)uid << 32 | tag;
+}
+
+void groupNetworkStats(std::vector<stats_line>* lines) {
+    if (lines->size() <= 1) return;
+    std::sort(lines->begin(), lines->end());
+
+    // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
+    size_t nextOutput = 0;
+    for (size_t i = 1; i < lines->size(); i++) {
+        if (lines->at(nextOutput) == lines->at(i)) {
+            lines->at(nextOutput) += lines->at(i);
+        } else {
+            nextOutput++;
+            if (nextOutput != i) {
+                lines->at(nextOutput) = lines->at(i);
+            }
+        }
+    }
+
+    if (lines->size() != nextOutput + 1) {
+        lines->resize(nextOutput + 1);
+    }
+}
+
+// True if lhs equals to rhs, only compare iface, uid, tag and set.
+bool operator==(const stats_line& lhs, const stats_line& rhs) {
+    return ((lhs.uid == rhs.uid) && (lhs.tag == rhs.tag) && (lhs.set == rhs.set) &&
+            !strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface)));
+}
+
+// True if lhs is smaller than rhs, only compare iface, uid, tag and set.
+bool operator<(const stats_line& lhs, const stats_line& rhs) {
+    int ret = strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface));
+    if (ret != 0) return ret < 0;
+    if (lhs.uid < rhs.uid) return true;
+    if (lhs.uid > rhs.uid) return false;
+    if (lhs.tag < rhs.tag) return true;
+    if (lhs.tag > rhs.tag) return false;
+    if (lhs.set < rhs.set) return true;
+    if (lhs.set > rhs.set) return false;
+    return false;
+}
+
+stats_line& stats_line::operator=(const stats_line& rhs) {
+    if (this == &rhs) return *this;
+
+    strlcpy(iface, rhs.iface, sizeof(iface));
+    uid = rhs.uid;
+    set = rhs.set;
+    tag = rhs.tag;
+    rxPackets = rhs.rxPackets;
+    txPackets = rhs.txPackets;
+    rxBytes = rhs.rxBytes;
+    txBytes = rhs.txBytes;
+    return *this;
+}
+
+stats_line& stats_line::operator+=(const stats_line& rhs) {
+    rxPackets += rhs.rxPackets;
+    txPackets += rhs.txPackets;
+    rxBytes += rhs.rxBytes;
+    txBytes += rhs.txBytes;
+    return *this;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
new file mode 100644
index 0000000..4974b96
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID1 = 10086;
+constexpr uid_t TEST_UID2 = 12345;
+constexpr uint32_t TEST_TAG = 42;
+constexpr int TEST_COUNTERSET0 = 0;
+constexpr int TEST_COUNTERSET1 = 1;
+constexpr uint64_t TEST_BYTES0 = 1000;
+constexpr uint64_t TEST_BYTES1 = 2000;
+constexpr uint64_t TEST_PACKET0 = 100;
+constexpr uint64_t TEST_PACKET1 = 200;
+constexpr const char IFACE_NAME1[] = "lo";
+constexpr const char IFACE_NAME2[] = "wlan0";
+constexpr const char IFACE_NAME3[] = "rmnet_data0";
+// A iface name that the size is bigger than IFNAMSIZ
+constexpr const char LONG_IFACE_NAME[] = "wlanWithALongName";
+constexpr const char TRUNCATED_IFACE_NAME[] = "wlanWithALongNa";
+constexpr uint32_t IFACE_INDEX1 = 1;
+constexpr uint32_t IFACE_INDEX2 = 2;
+constexpr uint32_t IFACE_INDEX3 = 3;
+constexpr uint32_t IFACE_INDEX4 = 4;
+constexpr uint32_t UNKNOWN_IFACE = 0;
+
+class BpfNetworkStatsHelperTest : public testing::Test {
+  protected:
+    BpfNetworkStatsHelperTest() {}
+    BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+    BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+    BpfMap<StatsKey, StatsValue> mFakeStatsMap;
+    BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
+    BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+
+    void SetUp() {
+        ASSERT_EQ(0, setrlimitForTest());
+
+        mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+
+        mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+
+        mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeStatsMap.getMap());
+
+        mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+
+        mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+    }
+
+    void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
+        auto tagResult = mFakeCookieTagMap.readValue(cookie);
+        EXPECT_RESULT_OK(tagResult);
+        EXPECT_EQ(uid, tagResult.value().uid);
+        EXPECT_EQ(tag, tagResult.value().tag);
+    }
+
+    void populateFakeStats(uid_t uid, uint32_t tag, uint32_t ifaceIndex, uint32_t counterSet,
+                           StatsValue value, BpfMap<StatsKey, StatsValue>& map) {
+        StatsKey key = {
+            .uid = (uint32_t)uid, .tag = tag, .counterSet = counterSet, .ifaceIndex = ifaceIndex};
+        EXPECT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+    }
+
+    void updateIfaceMap(const char* ifaceName, uint32_t ifaceIndex) {
+        IfaceValue iface;
+        strlcpy(iface.name, ifaceName, IFNAMSIZ);
+        EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+    }
+
+    void expectStatsEqual(const StatsValue& target, const Stats& result) {
+        EXPECT_EQ(target.rxPackets, result.rxPackets);
+        EXPECT_EQ(target.rxBytes, result.rxBytes);
+        EXPECT_EQ(target.txPackets, result.txPackets);
+        EXPECT_EQ(target.txBytes, result.txBytes);
+    }
+
+    void expectStatsLineEqual(const StatsValue target, const char* iface, uint32_t uid,
+                              int counterSet, uint32_t tag, const stats_line& result) {
+        EXPECT_EQ(0, strcmp(iface, result.iface));
+        EXPECT_EQ(uid, (uint32_t)result.uid);
+        EXPECT_EQ((uint32_t) counterSet, result.set);
+        EXPECT_EQ(tag, (uint32_t)result.tag);
+        EXPECT_EQ(target.rxPackets, (uint64_t)result.rxPackets);
+        EXPECT_EQ(target.rxBytes, (uint64_t)result.rxBytes);
+        EXPECT_EQ(target.txPackets, (uint64_t)result.txPackets);
+        EXPECT_EQ(target.txBytes, (uint64_t)result.txBytes);
+    }
+};
+
+// TEST to verify the behavior of bpf map when cocurrent deletion happens when
+// iterating the same map.
+TEST_F(BpfNetworkStatsHelperTest, TestIterateMapWithDeletion) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    uint64_t curCookie = 0;
+    auto nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    uint64_t headOfMap = nextCookie.value();
+    curCookie = nextCookie.value();
+    // Find the second entry in the map, then immediately delete it.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Find the entry that is now immediately after headOfMap, then delete that.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+    // Attempting to read an entry that has been deleted fails with ENOENT.
+    curCookie = nextCookie.value();
+    auto tagResult = mFakeCookieTagMap.readValue(curCookie);
+    EXPECT_EQ(ENOENT, tagResult.error().code());
+    // Finding the entry after our deleted entry restarts iteration from the beginning of the map.
+    nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+    EXPECT_RESULT_OK(nextCookie);
+    EXPECT_EQ(headOfMap, nextCookie.value());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestBpfIterateMap) {
+    for (int i = 0; i < 5; i++) {
+        uint64_t cookie = i + 1;
+        UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+        EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+    }
+    int totalCount = 0;
+    int totalSum = 0;
+    const auto iterateWithoutDeletion =
+            [&totalCount, &totalSum](const uint64_t& key, const BpfMap<uint64_t, UidTagValue>&) {
+                EXPECT_GE((uint64_t)5, key);
+                totalCount++;
+                totalSum += key;
+                return Result<void>();
+            };
+    EXPECT_RESULT_OK(mFakeCookieTagMap.iterate(iterateWithoutDeletion));
+    EXPECT_EQ(5, totalCount);
+    EXPECT_EQ(1 + 2 + 3 + 4 + 5, totalSum);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUidStatsNoTraffic) {
+    StatsValue value1 = {
+            .rxPackets = 0,
+            .rxBytes = 0,
+            .txPackets = 0,
+            .txBytes = 0,
+    };
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetUidStatsTotal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
+    ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+    expectStatsEqual(value1, result1);
+
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
+    expectStatsEqual(value2, result2);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME3, TEST_UID2, TEST_COUNTERSET1, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsInternal) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    Stats result1 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value1, result1);
+    Stats result2 = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    expectStatsEqual(value2, result2);
+    Stats totalResult = {};
+    ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
+                                          mFakeIfaceIndexNameMap));
+    StatsValue totalValue = {
+            .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
+            .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
+            .txPackets = TEST_PACKET1 * 2 + TEST_PACKET0,
+            .txBytes = TEST_BYTES1 * 2 + TEST_BYTES0,
+    };
+    expectStatsEqual(totalValue, totalResult);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsWithSkippedIface) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    populateFakeStats(0, 0, 0, OVERFLOW_COUNTERSET, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)3, lines.size());
+    lines.clear();
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, 0, lines.front());
+    lines.clear();
+    ifaces.push_back(std::string(IFACE_NAME1));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+                                                    mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)2, lines.size());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUnknownIfaceError) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 20,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 20,
+    };
+    uint32_t ifaceIndex = UNKNOWN_IFACE;
+    populateFakeStats(TEST_UID1, 0, ifaceIndex, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0 * 40,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1 * 40,
+    };
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    StatsKey curKey = {
+            .uid = TEST_UID1,
+            .tag = 0,
+            .counterSet = TEST_COUNTERSET0,
+            .ifaceIndex = ifaceIndex,
+    };
+    char ifname[IFNAMSIZ];
+    int64_t unknownIfaceBytesTotal = 0;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
+    curKey.ifaceIndex = IFACE_INDEX2;
+    ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+                                           ifname, curKey, &unknownIfaceBytesTotal));
+    ASSERT_EQ(-1, unknownIfaceBytesTotal);
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    // TODO: find a way to test the total of unknown Iface Bytes go above limit.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsDetail) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+    updateIfaceMap(LONG_IFACE_NAME, IFACE_INDEX4);
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    uint32_t ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX4;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    std::vector<stats_line> lines;
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)4, lines.size());
+
+    expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME3, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[2]);
+    ASSERT_EQ(0, strcmp(TRUNCATED_IFACE_NAME, lines[3].iface));
+    expectStatsLineEqual(value2, TRUNCATED_IFACE_NAME, UID_ALL, SET_ALL, TAG_NONE, lines[3]);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortedAndGrouped) {
+    // Create iface indexes with duplicate iface name.
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+    updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX3);  // Duplicate!
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+    StatsValue value2 = {
+            .rxPackets = TEST_PACKET1,
+            .rxBytes = TEST_BYTES1,
+            .txPackets = TEST_PACKET0,
+            .txBytes = TEST_BYTES0,
+    };
+    StatsValue value3 = {
+            .rxPackets = TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES1 * 2,
+    };
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+
+    // Test empty stats.
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 0, lines.size());
+    lines.clear();
+
+    // Test 1 line stats.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 1, lines.size());
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    lines.clear();
+
+    // These items should not be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET1, value2, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
+                      mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+    lines.clear();
+
+    // These items should be grouped.
+    populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 5, lines.size());
+
+    // Verify Sorted & Grouped.
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+    lines.clear();
+
+    // Perform test on IfaceStats.
+    uint32_t ifaceStatsKey = IFACE_INDEX2;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+    ifaceStatsKey = IFACE_INDEX1;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    // This should be grouped.
+    ifaceStatsKey = IFACE_INDEX3;
+    EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+    ASSERT_EQ(0,
+              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 2, lines.size());
+
+    expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+    lines.clear();
+}
+
+// Test to verify that subtract overflow will not be triggered by the compare function invoked from
+// sorting. See http:/b/119193941.
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortAndOverflow) {
+    updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+
+    StatsValue value1 = {
+            .rxPackets = TEST_PACKET0,
+            .rxBytes = TEST_BYTES0,
+            .txPackets = TEST_PACKET1,
+            .txBytes = TEST_BYTES1,
+    };
+
+    // Mutate uid, 0 < TEST_UID1 < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(0, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(UINT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MIN, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // Mutate tag, 0 < TEST_TAG < INT_MAX < INT_MIN < UINT_MAX.
+    populateFakeStats(TEST_UID1, INT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, INT_MIN, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, UINT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+    // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
+
+    std::vector<stats_line> lines;
+    std::vector<std::string> ifaces;
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+                                                    mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 8, lines.size());
+
+    // Uid 0 first
+    expectStatsLineEqual(value1, IFACE_NAME1, 0, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+
+    // Test uid, mutate tag.
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[1]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX, lines[2]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN, lines[3]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[4]);
+
+    // Mutate uid.
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[5]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN, TEST_COUNTERSET0, TEST_TAG, lines[6]);
+    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[7]);
+    lines.clear();
+}
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
new file mode 100644
index 0000000..8ab7e25
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _BPF_NETWORKSTATS_H
+#define _BPF_NETWORKSTATS_H
+
+#include <bpf/BpfMap.h>
+#include "bpf_shared.h"
+
+namespace android {
+namespace bpf {
+
+// TODO: set this to a proper value based on the map size;
+constexpr int TAG_STATS_MAP_SOFT_LIMIT = 3;
+constexpr int UID_ALL = -1;
+constexpr int TAG_ALL = -1;
+constexpr int TAG_NONE = 0;
+constexpr int SET_ALL = -1;
+constexpr int SET_DEFAULT = 0;
+constexpr int SET_FOREGROUND = 1;
+
+// The limit for stats received by a unknown interface;
+constexpr const int64_t MAX_UNKNOWN_IFACE_BYTES = 100 * 1000;
+
+// This is used by
+// frameworks/base/core/jni/com_android_internal_net_NetworkStatsFactory.cpp
+// make sure it is consistent with the JNI code before changing this.
+struct stats_line {
+    char iface[32];
+    uint32_t uid;
+    uint32_t set;
+    uint32_t tag;
+    int64_t rxBytes;
+    int64_t rxPackets;
+    int64_t txBytes;
+    int64_t txPackets;
+
+    stats_line& operator=(const stats_line& rhs);
+    stats_line& operator+=(const stats_line& rhs);
+};
+
+bool operator==(const stats_line& lhs, const stats_line& rhs);
+bool operator<(const stats_line& lhs, const stats_line& rhs);
+
+// For test only
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+                           const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+// For test only
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+                             const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+                             const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+// For test only
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+                                       const std::vector<std::string>& limitIfaces, int limitTag,
+                                       int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+                                       const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+// For test only
+int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
+// For test only
+template <class Key>
+int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
+                        const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
+                        const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    auto iface = ifaceMap.readValue(ifaceIndex);
+    if (!iface.ok()) {
+        maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
+        return -ENODEV;
+    }
+    strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
+    return 0;
+}
+
+template <class Key>
+void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+                          const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+    // Have we already logged an error?
+    if (*unknownIfaceBytesTotal == -1) {
+        return;
+    }
+
+    // Are we undercounting enough data to be worth logging?
+    auto statsEntry = statsMap.readValue(curKey);
+    if (!statsEntry.ok()) {
+        // No data is being undercounted.
+        return;
+    }
+
+    *unknownIfaceBytesTotal += (statsEntry.value().rxBytes + statsEntry.value().txBytes);
+    if (*unknownIfaceBytesTotal >= MAX_UNKNOWN_IFACE_BYTES) {
+        ALOGE("Unknown name for ifindex %d with more than %" PRId64 " bytes of traffic", ifaceIndex,
+              *unknownIfaceBytesTotal);
+        *unknownIfaceBytesTotal = -1;
+    }
+}
+
+// For test only
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+                                    const BpfMap<uint32_t, StatsValue>& statsMap,
+                                    const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+
+int bpfGetUidStats(uid_t uid, Stats* stats);
+int bpfGetIfaceStats(const char* iface, Stats* stats);
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+                               const std::vector<std::string>& limitIfaces, int limitTag,
+                               int limitUid);
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>* lines);
+int cleanStatsMap();
+}  // namespace bpf
+}  // namespace android
+
+#endif  // _BPF_NETWORKSTATS_H
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 9603139..eeaad18 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#define LOG_TAG "jniClatCoordinator"
 
 #include <arpa/inet.h>
 #include <errno.h>
@@ -22,6 +23,8 @@
 #include <log/log.h>
 #include <nativehelper/JNIHelp.h>
 #include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
 #include <string>
 
 #include <netjniutils/netjniutils.h>
@@ -33,9 +36,16 @@
 // Sync from system/netd/include/netid_client.h
 #define MARK_UNSET 0u
 
+// Sync from system/netd/server/NetdConstants.h
+#define __INT_STRLEN(i) sizeof(#i)
+#define _INT_STRLEN(i) __INT_STRLEN(i)
+#define INT32_STRLEN _INT_STRLEN(INT32_MIN)
+
 #define DEVICEPREFIX "v4-"
 
 namespace android {
+static const char* kClatdPath = "/apex/com.android.tethering/bin/for-system/clatd";
+
 static void throwIOException(JNIEnv* env, const char* msg, int error) {
     jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
 }
@@ -282,8 +292,7 @@
     return 0;
 }
 
-// TODO: fork clatd and rename to .._startClatd.
-static jint com_android_server_connectivity_ClatCoordinator_maybeStartBpf(
+static jint com_android_server_connectivity_ClatCoordinator_startClatd(
         JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
         jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
     ScopedUtfChars ifaceStr(env, iface);
@@ -291,7 +300,121 @@
     ScopedUtfChars v4Str(env, v4);
     ScopedUtfChars v6Str(env, v6);
 
-    // Start BPF if any
+    int tunFd = netjniutils::GetNativeFileDescriptor(env, tunJavaFd);
+    if (tunFd < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid tun file descriptor");
+        return -1;
+    }
+
+    int readSock = netjniutils::GetNativeFileDescriptor(env, readSockJavaFd);
+    if (readSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid read socket");
+        return -1;
+    }
+
+    int writeSock = netjniutils::GetNativeFileDescriptor(env, writeSockJavaFd);
+    if (writeSock < 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid write socket");
+        return -1;
+    }
+
+    // 1. create a throwaway socket to reserve a file descriptor number
+    int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedTunFd == -1) {
+        throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
+        return -1;
+    }
+    int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockRead == -1) {
+        throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
+        return -1;
+    }
+    int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (passedSockWrite == -1) {
+        throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
+        return -1;
+    }
+
+    // these are the FD we'll pass to clatd on the cli, so need it as a string
+    char passedTunFdStr[INT32_STRLEN];
+    char passedSockReadStr[INT32_STRLEN];
+    char passedSockWriteStr[INT32_STRLEN];
+    snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
+    snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
+    snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+
+    // 2. we're going to use this as argv[0] to clatd to make ps output more useful
+    std::string progname("clatd-");
+    progname += ifaceStr.c_str();
+
+    // clang-format off
+    const char* args[] = {progname.c_str(),
+                          "-i", ifaceStr.c_str(),
+                          "-p", pfx96Str.c_str(),
+                          "-4", v4Str.c_str(),
+                          "-6", v6Str.c_str(),
+                          "-t", passedTunFdStr,
+                          "-r", passedSockReadStr,
+                          "-w", passedSockWriteStr,
+                          nullptr};
+    // clang-format on
+
+    // 3. register vfork requirement
+    posix_spawnattr_t attr;
+    if (int ret = posix_spawnattr_init(&attr)) {
+        throwIOException(env, "posix_spawnattr_init failed", ret);
+        return -1;
+    }
+
+    // TODO: use android::base::ScopeGuard.
+    if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawnattr_setflags failed", ret);
+        return -1;
+    }
+
+    // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
+    // on the tun fd that we want the child clatd process to inherit
+    // (this will happen after the vfork, and before the execve)
+    posix_spawn_file_actions_t fa;
+    if (int ret = posix_spawn_file_actions_init(&fa)) {
+        posix_spawnattr_destroy(&attr);
+        throwIOException(env, "posix_spawn_file_actions_init failed", ret);
+        return -1;
+    }
+
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
+        return -1;
+    }
+    if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
+        return -1;
+    }
+
+    // 5. actually perform vfork/dup2/execve
+    pid_t pid;
+    if (int ret = posix_spawn(&pid, kClatdPath, &fa, &attr, (char* const*)args, nullptr)) {
+        posix_spawnattr_destroy(&attr);
+        posix_spawn_file_actions_destroy(&fa);
+        throwIOException(env, "posix_spawn failed", ret);
+        return -1;
+    }
+
+    posix_spawnattr_destroy(&attr);
+    posix_spawn_file_actions_destroy(&fa);
+
+    // 5. Start BPF if any
     if (!net::clat::initMaps()) {
         net::clat::ClatdTracker tracker = {};
         if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -300,19 +423,23 @@
         }
     }
 
-    return 0; // TODO: return forked clatd pid.
+    return pid;
 }
 
-// TODO: stop clatd and rename to .._stopClatd.
-static void com_android_server_connectivity_ClatCoordinator_maybeStopBpf(JNIEnv* env, jobject clazz,
-                                                                       jstring iface, jstring pfx96,
-                                                                       jstring v4, jstring v6,
-                                                                       jint pid /* unused */) {
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jobject clazz,
+                                                                      jstring iface, jstring pfx96,
+                                                                      jstring v4, jstring v6,
+                                                                      jint pid) {
     ScopedUtfChars ifaceStr(env, iface);
     ScopedUtfChars pfx96Str(env, pfx96);
     ScopedUtfChars v4Str(env, v4);
     ScopedUtfChars v6Str(env, v6);
 
+    if (pid <= 0) {
+        jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
+        return;
+    }
+
     if (!net::clat::initMaps()) {
         net::clat::ClatdTracker tracker = {};
         if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
@@ -320,6 +447,9 @@
             net::clat::maybeStopBpf(tracker);
         }
     }
+
+    kill(pid, SIGTERM);
+    waitpid(pid, nullptr, 0);  // Should we block in JNI?
 }
 
 /*
@@ -344,13 +474,13 @@
          (void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt},
         {"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
          (void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket},
-        {"native_maybeStartBpf",
+        {"native_startClatd",
          "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
          "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
-         (void*)com_android_server_connectivity_ClatCoordinator_maybeStartBpf},
-        {"native_maybeStopBpf",
+         (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
+        {"native_stopClatd",
          "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
-          (void*)com_android_server_connectivity_ClatCoordinator_maybeStopBpf},
+         (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
 };
 
 int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index cac545d..97fcc43 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -40,6 +40,7 @@
 #include <android-base/unique_fd.h>
 #include <netdutils/StatusOr.h>
 #include <netdutils/Syscalls.h>
+#include <netdutils/UidConstants.h>
 #include <netdutils/Utils.h>
 #include <private/android_filesystem_config.h>
 
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 5a5e24a..7616457 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -170,20 +170,20 @@
         }
 
         /**
-         * Maybe start bpf.
+         * Start clatd.
          */
-        public int maybeStartBpf(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
                 @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
                 @NonNull String v4, @NonNull String v6) throws IOException {
-            return native_maybeStartBpf(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
+            return native_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
         }
 
         /**
-         * Maybe stop bpf.
+         * Stop clatd.
          */
-        public void maybeStopBpf(String iface, String pfx96, String v4, String v6, int pid)
+        public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
                 throws IOException {
-            native_maybeStopBpf(iface, pfx96, v4, v6, pid);
+            native_stopClatd(iface, pfx96, v4, v6, pid);
         }
     }
 
@@ -219,6 +219,9 @@
     public String clatStart(final String iface, final int netId,
             @NonNull final IpPrefix nat64Prefix)
             throws IOException {
+        if (mIface != null || mPid != INVALID_PID) {
+            throw new IOException("Clatd has started on " + mIface + " (pid " + mPid + ")");
+        }
         if (nat64Prefix.getPrefixLength() != 96) {
             throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
         }
@@ -254,6 +257,7 @@
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
         }
 
@@ -270,6 +274,7 @@
         try {
             mNetd.interfaceSetMtu(tunIface, mtu);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
         }
         final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -281,6 +286,7 @@
         try {
             mNetd.interfaceSetCfg(ifConfig);
         } catch (RemoteException | ServiceSpecificException e) {
+            tunFd.close();
             throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
                     + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
         }
@@ -290,11 +296,12 @@
         final ParcelFileDescriptor readSock6;
         try {
             // Use a JNI call to get native file descriptor instead of Os.socket() because we would
-            // like to use ParcelFileDescriptor to close file descriptor automatically. But ctor
+            // like to use ParcelFileDescriptor to manage file descriptor. But ctor
             // ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
             // descriptor to initialize ParcelFileDescriptor object instead.
             readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
         } catch (IOException e) {
+            tunFd.close();
             throw new IOException("Open packet socket failed: " + e);
         }
 
@@ -305,11 +312,16 @@
             // reason why we use jniOpenPacketSocket6().
             writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
             throw new IOException("Open raw socket failed: " + e);
         }
 
         final int ifaceIndex = mDeps.getInterfaceIndex(iface);
         if (ifaceIndex == INVALID_IFINDEX) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("Fail to get interface index for interface " + iface);
         }
 
@@ -317,6 +329,9 @@
         try {
             mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("add anycast sockopt failed: " + e);
         }
 
@@ -324,31 +339,41 @@
         try {
             mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
         } catch (IOException e) {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
             throw new IOException("configure packet socket failed: " + e);
         }
 
-        // [5] Maybe start bpf.
+        // [5] Start clatd.
         try {
-            mDeps.maybeStartBpf(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+            mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
                     writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
             mIface = iface;
             mNat64Prefix = pfx96;
             mXlatLocalAddress4 = v4;
             mXlatLocalAddress6 = v6;
         } catch (IOException e) {
-            throw new IOException("Error start bpf on " + iface + ": " + e);
+            throw new IOException("Error start clatd on " + iface + ": " + e);
+        } finally {
+            tunFd.close();
+            readSock6.close();
+            writeSock6.close();
         }
 
-        // TODO: start clatd and returns local xlat464 v6 address.
-        return null;
+        return v6;
     }
 
     /**
      * Stop clatd
      */
     public void clatStop() throws IOException {
-        mDeps.maybeStopBpf(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6,
-                mPid /* unused */);
+        if (mPid == INVALID_PID) {
+            throw new IOException("Clatd has not started");
+        }
+        Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+
+        mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
         // TODO: remove setIptablesDropRule
 
         Log.i(TAG, "clatd on " + mIface + " stopped");
@@ -357,6 +382,7 @@
         mNat64Prefix = null;
         mXlatLocalAddress4 = null;
         mXlatLocalAddress6 = null;
+        mPid = INVALID_PID;
     }
 
     private static native String native_selectIpv4Address(String v4addr, int prefixlen)
@@ -372,9 +398,9 @@
             int ifindex) throws IOException;
     private static native void native_configurePacketSocket(FileDescriptor sock, String v6,
             int ifindex) throws IOException;
-    private static native int native_maybeStartBpf(FileDescriptor tunfd, FileDescriptor readsock6,
+    private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
             FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
             throws IOException;
-    private static native void native_maybeStopBpf(String iface, String pfx96, String v4,
-            String v6, int pid) throws IOException;
+    private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
+            int pid) throws IOException;
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 0bef0e2..2b698fd 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -38,8 +38,8 @@
         "liblog",
         "liblzma",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
         "libnetworkstatsfactorytestjni",
         "libpackagelistparser",
         "libpcre2",
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
new file mode 100644
index 0000000..bdcc21b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class ClatCoordinatorTest {
+    private static final String BASE_IFACE = "test0";
+    private static final String STACKED_IFACE = "v4-test0";
+    private static final int BASE_IFINDEX = 1000;
+
+    private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+    private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+    private static final int GOOGLE_DNS_4 = 0x08080808;  // 8.8.8.8
+    private static final int NETID = 42;
+
+    // The test fwmark means: PERMISSION_SYSTEM (0x2), protectedFromVpn: true,
+    // explicitlySelected: true, netid: 42. For bit field structure definition, see union Fwmark in
+    // system/netd/include/Fwmark.h
+    private static final int MARK = 0xb002a;
+
+    private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
+    private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+    private static final int CLATD_PID = 10483;
+
+    private static final int TUN_FD = 534;
+    private static final int RAW_SOCK_FD = 535;
+    private static final int PACKET_SOCK_FD = 536;
+    private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+    private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
+            new FileDescriptor());
+
+    @Mock private INetd mNetd;
+    @Spy private TestDependencies mDeps = new TestDependencies();
+
+    /**
+      * The dependency injection class is used to mock the JNI functions and system functions
+      * for clatd coordinator control plane. Note that any testing used JNI functions need to
+      * be overridden to avoid calling native methods.
+      */
+    protected class TestDependencies extends ClatCoordinator.Dependencies {
+        /**
+          * Get netd.
+          */
+        @Override
+        public INetd getNetd() {
+            return mNetd;
+        }
+
+        /**
+         * @see ParcelFileDescriptor#adoptFd(int).
+         */
+        @Override
+        public ParcelFileDescriptor adoptFd(int fd) {
+            switch (fd) {
+                case TUN_FD:
+                    return TUN_PFD;
+                case RAW_SOCK_FD:
+                    return RAW_SOCK_PFD;
+                case PACKET_SOCK_FD:
+                    return PACKET_SOCK_PFD;
+                default:
+                    fail("unsupported arg: " + fd);
+                    return null;
+            }
+        }
+
+        /**
+         * Get interface index for a given interface.
+         */
+        @Override
+        public int getInterfaceIndex(String ifName) {
+            if (BASE_IFACE.equals(ifName)) {
+                return BASE_IFINDEX;
+            }
+            fail("unsupported arg: " + ifName);
+            return -1;
+        }
+
+        /**
+         * Create tun interface for a given interface name.
+         */
+        @Override
+        public int createTunInterface(@NonNull String tuniface) throws IOException {
+            if (STACKED_IFACE.equals(tuniface)) {
+                return TUN_FD;
+            }
+            fail("unsupported arg: " + tuniface);
+            return -1;
+        }
+
+        /**
+         * Pick an IPv4 address for clat.
+         */
+        @Override
+        public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+                throws IOException {
+            if (INIT_V4ADDR_STRING.equals(v4addr) && INIT_V4ADDR_PREFIX_LEN == prefixlen) {
+                return XLAT_LOCAL_IPV4ADDR_STRING;
+            }
+            fail("unsupported args: " + v4addr + ", " + prefixlen);
+            return null;
+        }
+
+        /**
+         * Generate a checksum-neutral IID.
+         */
+        @Override
+        public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+                @NonNull String prefix64) throws IOException {
+            if (BASE_IFACE.equals(iface) && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && NAT64_PREFIX_STRING.equals(prefix64)) {
+                return XLAT_LOCAL_IPV6ADDR_STRING;
+            }
+            fail("unsupported args: " + iface + ", " + v4 + ", " + prefix64);
+            return null;
+        }
+
+        /**
+         * Detect MTU.
+         */
+        @Override
+        public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+                throws IOException {
+            if (NAT64_PREFIX_STRING.equals(platSubnet) && GOOGLE_DNS_4 == platSuffix
+                    && MARK == mark) {
+                return ETHER_MTU;
+            }
+            fail("unsupported args: " + platSubnet + ", " + platSuffix + ", " + mark);
+            return -1;
+        }
+
+        /**
+         * Open IPv6 raw socket and set SO_MARK.
+         */
+        @Override
+        public int openRawSocket6(int mark) throws IOException {
+            if (mark == MARK) {
+                return RAW_SOCK_FD;
+            }
+            fail("unsupported arg: " + mark);
+            return -1;
+        }
+
+        /**
+         * Open packet socket.
+         */
+        @Override
+        public int openPacketSocket() throws IOException {
+            // assume that open socket always successfully because there is no argument to check.
+            return PACKET_SOCK_FD;
+        }
+
+        /**
+         * Add anycast setsockopt.
+         */
+        @Override
+        public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Configure packet socket.
+         */
+        @Override
+        public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+                throws IOException {
+            if (Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), sock)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+                    && BASE_IFINDEX == ifindex) return;
+            fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+        }
+
+        /**
+         * Start clatd.
+         */
+        @Override
+        public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+                @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+                @NonNull String v4, @NonNull String v6) throws IOException {
+            if (Objects.equals(TUN_PFD.getFileDescriptor(), tunfd)
+                    && Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), readsock6)
+                    && Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), writesock6)
+                    && BASE_IFACE.equals(iface)
+                    && NAT64_PREFIX_STRING.equals(pfx96)
+                    && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+                    && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)) {
+                return CLATD_PID;
+            }
+            fail("unsupported args: " + tunfd + ", " + readsock6 + ", " + writesock6 + ", "
+                    + ", " + iface + ", " + v4 + ", " + v6);
+            return -1;
+        }
+
+        /**
+         * Stop clatd.
+         */
+        public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
+                @NonNull String v6, int pid) throws IOException {
+            if (pid == -1) {
+                fail("unsupported arg: " + pid);
+            }
+        }
+    };
+
+    @NonNull
+    private ClatCoordinator makeClatCoordinator() throws Exception {
+        final ClatCoordinator coordinator = new ClatCoordinator(mDeps);
+        return coordinator;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private boolean assertContainsFlag(String[] flags, String match) {
+        for (String flag : flags) {
+            if (flag.equals(match)) return true;
+        }
+        fail("Missing flag: " + match);
+        return false;
+    }
+
+    @Test
+    public void testStartStopClatd() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        final InOrder inOrder = inOrder(mNetd, mDeps);
+        clearInvocations(mNetd, mDeps);
+
+        // [1] Start clatd.
+        final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+
+        // Pick an IPv4 address.
+        inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
+                eq(INIT_V4ADDR_PREFIX_LEN));
+
+        // Generate a checksum-neutral IID.
+        inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING));
+
+        // Open, configure and bring up the tun interface.
+        inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+        inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+        inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+        inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+        inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+                eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                STACKED_IFACE.equals(cfg.ifName)
+                && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+                && (32 == cfg.prefixLength)
+                && "".equals(cfg.hwAddr)
+                && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
+        // Open and configure 464xlat read/write sockets.
+        inOrder.verify(mDeps).openPacketSocket();
+        inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
+        inOrder.verify(mDeps).openRawSocket6(eq(MARK));
+        inOrder.verify(mDeps).adoptFd(eq(RAW_SOCK_FD));
+        inOrder.verify(mDeps).getInterfaceIndex(eq(BASE_IFACE));
+        inOrder.verify(mDeps).addAnycastSetsockopt(
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+        inOrder.verify(mDeps).configurePacketSocket(
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+
+        // Start clatd.
+        inOrder.verify(mDeps).startClatd(
+                argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+                argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+                eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+        inOrder.verifyNoMoreInteractions();
+
+        // [2] Start clatd again failed.
+        assertThrows("java.io.IOException: Clatd has started on test0 (pid 10483)",
+                IOException.class,
+                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+        // [3] Expect clatd to stop successfully.
+        coordinator.clatStop();
+        inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+                eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+        inOrder.verifyNoMoreInteractions();
+
+        // [4] Expect an IO exception while stopping a clatd that doesn't exist.
+        assertThrows("java.io.IOException: Clatd has not started", IOException.class,
+                () -> coordinator.clatStop());
+        inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
+                anyString(), anyInt());
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testGetFwmark() throws Exception {
+        assertEquals(0xb0064, ClatCoordinator.getFwmark(100));
+        assertEquals(0xb03e8, ClatCoordinator.getFwmark(1000));
+        assertEquals(0xb2710, ClatCoordinator.getFwmark(10000));
+        assertEquals(0xbffff, ClatCoordinator.getFwmark(65535));
+    }
+
+    @Test
+    public void testAdjustMtu() throws Exception {
+        // Expected mtu is that IPV6_MIN_MTU(1280) minus MTU_DELTA(28).
+        assertEquals(1252, ClatCoordinator.adjustMtu(-1 /* detect mtu failed */));
+        assertEquals(1252, ClatCoordinator.adjustMtu(500));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1000));
+        assertEquals(1252, ClatCoordinator.adjustMtu(1280));
+
+        // Expected mtu is that the detected mtu minus MTU_DELTA(28).
+        assertEquals(1372, ClatCoordinator.adjustMtu(1400));
+        assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+
+        // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
+        assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+    }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index fe971e7..0f71c13 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -24,7 +24,7 @@
         "libbpf_android",
         "liblog",
         "libnativehelper",
-        "libnetdbpf",
         "libnetdutils",
+        "libnetworkstats",
     ],
 }