Merge "[NETD-BPF#19] Mainline part of bpf code from netd" am: 523eaffcdf
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/1849156
Change-Id: Id7ee21b5e4c51725b5c8fe92e10531b2bce68dd7
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 780ba26..302c0b3 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -19,6 +19,9 @@
]
},
{
+ "name": "netd_updatable_unit_test"
+ },
+ {
"name": "TetheringTests"
},
{
@@ -40,6 +43,10 @@
"name": "bpf_existence_test"
},
{
+ "name": "netd_updatable_unit_test",
+ "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+ },
+ {
"name": "libclat_test"
},
{
@@ -63,6 +70,9 @@
]
},
{
+ "name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+ },
+ {
"name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 12289a9..f761f65 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -55,6 +55,7 @@
"libcom_android_connectivity_com_android_net_module_util_jni",
"libtraffic_controller_jni",
],
+ native_shared_libs: ["libnetd_updatable"],
},
both: {
jni_libs: ["libframework-connectivity-jni"],
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index cce2b71..6718402 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -42,6 +42,7 @@
// TODO: remove it when NetworkStatsService is moved into the mainline module and no more
// calls to JNI in libservices.core.
"//frameworks/base/services/core/jni",
+ "//packages/modules/Connectivity/netd",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
diff --git a/netd/Android.bp b/netd/Android.bp
new file mode 100644
index 0000000..53c1bd4
--- /dev/null
+++ b/netd/Android.bp
@@ -0,0 +1,81 @@
+//
+// 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.
+
+cc_library {
+ name: "libnetd_updatable",
+ version_script: "libnetd_updatable.map.txt",
+ stubs: {
+ versions: [
+ "1",
+ ],
+ symbol_file: "libnetd_updatable.map.txt",
+ },
+ defaults: ["netd_defaults"],
+ header_libs: [
+ "bpf_connectivity_headers",
+ "libcutils_headers",
+ ],
+ srcs: [
+ "BpfHandler.cpp",
+ "NetdUpdatable.cpp",
+ ],
+ static_libs: [
+ "libnetdutils",
+ ],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+ export_include_dirs: ["include"],
+ header_abi_checker: {
+ enabled: true,
+ symbol_file: "libnetd_updatable.map.txt",
+ },
+ sanitize: {
+ cfi: true,
+ },
+ apex_available: ["com.android.tethering"],
+ min_sdk_version: "30",
+}
+
+cc_test {
+ name: "netd_updatable_unit_test",
+ defaults: ["netd_defaults"],
+ test_suites: ["general-tests"],
+ require_root: true, // required by setrlimitForTest()
+ header_libs: [
+ "bpf_connectivity_headers",
+ ],
+ srcs: [
+ "BpfHandlerTest.cpp",
+ ],
+ static_libs: [
+ "libnetd_updatable",
+ ],
+ shared_libs: [
+ "libbase",
+ "libcutils",
+ "liblog",
+ "libnetdutils",
+ ],
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+}
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
new file mode 100644
index 0000000..3cd5e13
--- /dev/null
+++ b/netd/BpfHandler.cpp
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "BpfHandler"
+
+#include "BpfHandler.h"
+
+#include <linux/bpf.h>
+
+#include <android-base/unique_fd.h>
+#include <bpf/WaitForProgsLoaded.h>
+#include <log/log.h>
+#include <netdutils/UidConstants.h>
+#include <private/android_filesystem_config.h>
+
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+namespace net {
+
+using base::unique_fd;
+using bpf::NONEXISTENT_COOKIE;
+using bpf::getSocketCookie;
+using bpf::retrieveProgram;
+using netdutils::Status;
+using netdutils::statusFromErrno;
+
+constexpr int PER_UID_STATS_ENTRIES_LIMIT = 500;
+// At most 90% of the stats map may be used by tagged traffic entries. This ensures
+// that 10% of the map is always available to count untagged traffic, one entry per UID.
+// Otherwise, apps would be able to avoid data usage accounting entirely by filling up the
+// map with tagged traffic entries.
+constexpr int TOTAL_UID_STATS_ENTRIES_LIMIT = STATS_MAP_SIZE * 0.9;
+
+static_assert(STATS_MAP_SIZE - TOTAL_UID_STATS_ENTRIES_LIMIT > 100,
+ "The limit for stats map is to high, stats data may be lost due to overflow");
+
+static Status attachProgramToCgroup(const char* programPath, const unique_fd& cgroupFd,
+ bpf_attach_type type) {
+ unique_fd cgroupProg(retrieveProgram(programPath));
+ if (cgroupProg == -1) {
+ int ret = errno;
+ ALOGE("Failed to get program from %s: %s", programPath, strerror(ret));
+ return statusFromErrno(ret, "cgroup program get failed");
+ }
+ if (android::bpf::attachProgram(type, cgroupProg, cgroupFd)) {
+ int ret = errno;
+ ALOGE("Program from %s attach failed: %s", programPath, strerror(ret));
+ return statusFromErrno(ret, "program attach failed");
+ }
+ return netdutils::status::ok;
+}
+
+static Status initPrograms(const char* cg2_path) {
+ unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+ if (cg_fd == -1) {
+ int ret = errno;
+ ALOGE("Failed to open the cgroup directory: %s", strerror(ret));
+ return statusFromErrno(ret, "Open the cgroup directory failed");
+ }
+ RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
+
+ // For the devices that support cgroup socket filter, the socket filter
+ // should be loaded successfully by bpfloader. So we attach the filter to
+ // cgroup if the program is pinned properly.
+ // TODO: delete the if statement once all devices should support cgroup
+ // socket filter (ie. the minimum kernel version required is 4.14).
+ if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
+ RETURN_IF_NOT_OK(
+ attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
+ }
+ return netdutils::status::ok;
+}
+
+BpfHandler::BpfHandler()
+ : mPerUidStatsEntriesLimit(PER_UID_STATS_ENTRIES_LIMIT),
+ mTotalUidStatsEntriesLimit(TOTAL_UID_STATS_ENTRIES_LIMIT) {}
+
+BpfHandler::BpfHandler(uint32_t perUidLimit, uint32_t totalLimit)
+ : mPerUidStatsEntriesLimit(perUidLimit), mTotalUidStatsEntriesLimit(totalLimit) {}
+
+Status BpfHandler::init(const char* cg2_path) {
+ // Make sure BPF programs are loaded before doing anything
+ android::bpf::waitForProgsLoaded();
+ ALOGI("BPF programs are loaded");
+
+ RETURN_IF_NOT_OK(initPrograms(cg2_path));
+ RETURN_IF_NOT_OK(initMaps());
+
+ return netdutils::status::ok;
+}
+
+Status BpfHandler::initMaps() {
+ std::lock_guard guard(mMutex);
+ RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
+ RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
+ RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
+ RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+ RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
+ BPF_ANY));
+ RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+
+ return netdutils::status::ok;
+}
+
+bool BpfHandler::hasUpdateDeviceStatsPermission(uid_t uid) {
+ // This implementation is the same logic as method ActivityManager#checkComponentPermission.
+ // It implies that the real uid can never be the same as PER_USER_RANGE.
+ uint32_t appId = uid % PER_USER_RANGE;
+ auto permission = mUidPermissionMap.readValue(appId);
+ if (permission.ok() && (permission.value() & BPF_PERMISSION_UPDATE_DEVICE_STATS)) {
+ return true;
+ }
+ return ((appId == AID_ROOT) || (appId == AID_SYSTEM) || (appId == AID_DNS));
+}
+
+int BpfHandler::tagSocket(int sockFd, uint32_t tag, uid_t chargeUid, uid_t realUid) {
+ std::lock_guard guard(mMutex);
+ if (chargeUid != realUid && !hasUpdateDeviceStatsPermission(realUid)) {
+ return -EPERM;
+ }
+
+ uint64_t sock_cookie = getSocketCookie(sockFd);
+ if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
+ UidTagValue newKey = {.uid = (uint32_t)chargeUid, .tag = tag};
+
+ uint32_t totalEntryCount = 0;
+ uint32_t perUidEntryCount = 0;
+ // Now we go through the stats map and count how many entries are associated
+ // with chargeUid. If the uid entry hit the limit for each chargeUid, we block
+ // the request to prevent the map from overflow. It is safe here to iterate
+ // over the map since when mMutex is hold, system server cannot toggle
+ // the live stats map and clean it. So nobody can delete entries from the map.
+ const auto countUidStatsEntries = [chargeUid, &totalEntryCount, &perUidEntryCount](
+ const StatsKey& key,
+ const BpfMap<StatsKey, StatsValue>&) {
+ if (key.uid == chargeUid) {
+ perUidEntryCount++;
+ }
+ totalEntryCount++;
+ return base::Result<void>();
+ };
+ auto configuration = mConfigurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
+ if (!configuration.ok()) {
+ ALOGE("Failed to get current configuration: %s, fd: %d",
+ strerror(configuration.error().code()), mConfigurationMap.getMap().get());
+ return -configuration.error().code();
+ }
+ if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+ ALOGE("unknown configuration value: %d", configuration.value());
+ return -EINVAL;
+ }
+
+ BpfMap<StatsKey, StatsValue>& currentMap =
+ (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
+ base::Result<void> res = currentMap.iterate(countUidStatsEntries);
+ if (!res.ok()) {
+ ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
+ strerror(res.error().code()));
+ return -res.error().code();
+ }
+
+ if (totalEntryCount > mTotalUidStatsEntriesLimit ||
+ perUidEntryCount > mPerUidStatsEntriesLimit) {
+ ALOGE("Too many stats entries in the map, total count: %u, chargeUid(%u) count: %u,"
+ " blocking tag request to prevent map overflow",
+ totalEntryCount, chargeUid, perUidEntryCount);
+ return -EMFILE;
+ }
+ // Update the tag information of a socket to the cookieUidMap. Use BPF_ANY
+ // flag so it will insert a new entry to the map if that value doesn't exist
+ // yet. And update the tag if there is already a tag stored. Since the eBPF
+ // program in kernel only read this map, and is protected by rcu read lock. It
+ // should be fine to cocurrently update the map while eBPF program is running.
+ res = mCookieTagMap.writeValue(sock_cookie, newKey, BPF_ANY);
+ if (!res.ok()) {
+ ALOGE("Failed to tag the socket: %s, fd: %d", strerror(res.error().code()),
+ mCookieTagMap.getMap().get());
+ return -res.error().code();
+ }
+ return 0;
+}
+
+int BpfHandler::untagSocket(int sockFd) {
+ std::lock_guard guard(mMutex);
+ uint64_t sock_cookie = getSocketCookie(sockFd);
+
+ if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
+ base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
+ if (!res.ok()) {
+ ALOGE("Failed to untag socket: %s\n", strerror(res.error().code()));
+ return -res.error().code();
+ }
+ return 0;
+}
+
+} // namespace net
+} // namespace android
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
new file mode 100644
index 0000000..2ede1c1
--- /dev/null
+++ b/netd/BpfHandler.h
@@ -0,0 +1,83 @@
+/**
+ * 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 <mutex>
+
+#include <netdutils/Status.h>
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+
+using android::bpf::BpfMap;
+
+namespace android {
+namespace net {
+
+class BpfHandler {
+ public:
+ BpfHandler();
+ BpfHandler(const BpfHandler&) = delete;
+ BpfHandler& operator=(const BpfHandler&) = delete;
+ netdutils::Status init(const char* cg2_path);
+ /*
+ * Tag the socket with the specified tag and uid. In the qtaguid module, the
+ * first tag request that grab the spinlock of rb_tree can update the tag
+ * information first and other request need to wait until it finish. All the
+ * tag request will be addressed in the order of they obtaining the spinlock.
+ * In the eBPF implementation, the kernel will try to update the eBPF map
+ * entry with the tag request. And the hashmap update process is protected by
+ * the spinlock initialized with the map. So the behavior of two modules
+ * should be the same. No additional lock needed.
+ */
+ int tagSocket(int sockFd, uint32_t tag, uid_t chargeUid, uid_t realUid);
+
+ /*
+ * The untag process is similar to tag socket and both old qtaguid module and
+ * new eBPF module have spinlock inside the kernel for concurrent update. No
+ * external lock is required.
+ */
+ int untagSocket(int sockFd);
+
+ private:
+ // For testing
+ BpfHandler(uint32_t perUidLimit, uint32_t totalLimit);
+
+ netdutils::Status initMaps();
+ bool hasUpdateDeviceStatsPermission(uid_t uid);
+
+ BpfMap<uint64_t, UidTagValue> mCookieTagMap;
+ BpfMap<StatsKey, StatsValue> mStatsMapA;
+ BpfMap<StatsKey, StatsValue> mStatsMapB;
+ BpfMap<uint32_t, uint8_t> mConfigurationMap;
+ BpfMap<uint32_t, uint8_t> mUidPermissionMap;
+
+ std::mutex mMutex;
+
+ // The limit on the number of stats entries a uid can have in the per uid stats map. BpfHandler
+ // will block that specific uid from tagging new sockets after the limit is reached.
+ const uint32_t mPerUidStatsEntriesLimit;
+
+ // The limit on the total number of stats entries in the per uid stats map. BpfHandler will
+ // block all tagging requests after the limit is reached.
+ const uint32_t mTotalUidStatsEntriesLimit;
+
+ // For testing
+ friend class BpfHandlerTest;
+};
+
+} // namespace net
+} // namespace android
\ No newline at end of file
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
new file mode 100644
index 0000000..db59c7c
--- /dev/null
+++ b/netd/BpfHandlerTest.cpp
@@ -0,0 +1,244 @@
+/*
+ * Copyright 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.
+ *
+ * BpfHandlerTest.cpp - unit tests for BpfHandler.cpp
+ */
+
+#include <sys/socket.h>
+
+#include <gtest/gtest.h>
+
+#include "BpfHandler.h"
+
+using namespace android::bpf; // NOLINT(google-build-using-namespace): exempted
+
+namespace android {
+namespace net {
+
+using base::Result;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr int TEST_COOKIE = 1;
+constexpr uid_t TEST_UID = 10086;
+constexpr uid_t TEST_UID2 = 54321;
+constexpr uint32_t TEST_TAG = 42;
+constexpr uint32_t TEST_COUNTERSET = 1;
+constexpr uint32_t TEST_PER_UID_STATS_ENTRIES_LIMIT = 3;
+constexpr uint32_t TEST_TOTAL_UID_STATS_ENTRIES_LIMIT = 7;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class BpfHandlerTest : public ::testing::Test {
+ protected:
+ BpfHandlerTest()
+ : mBh(TEST_PER_UID_STATS_ENTRIES_LIMIT, TEST_TOTAL_UID_STATS_ENTRIES_LIMIT) {}
+ BpfHandler mBh;
+ BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+ BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
+ BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
+
+ void SetUp() {
+ std::lock_guard guard(mBh.mMutex);
+ ASSERT_EQ(0, setrlimitForTest());
+
+ mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeCookieTagMap);
+
+ mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeStatsMapA);
+
+ mFakeConfigurationMap.reset(
+ createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+ ASSERT_VALID(mFakeConfigurationMap);
+
+ mFakeUidPermissionMap.reset(
+ createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeUidPermissionMap);
+
+ mBh.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+ ASSERT_VALID(mBh.mCookieTagMap);
+ mBh.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+ ASSERT_VALID(mBh.mStatsMapA);
+ mBh.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+ ASSERT_VALID(mBh.mConfigurationMap);
+ // Always write to stats map A by default.
+ ASSERT_RESULT_OK(mBh.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
+ SELECT_MAP_A, BPF_ANY));
+ mBh.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+ ASSERT_VALID(mBh.mUidPermissionMap);
+ }
+
+ int dupFd(const android::base::unique_fd& mapFd) {
+ return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+ }
+
+ int setUpSocketAndTag(int protocol, uint64_t* cookie, uint32_t tag, uid_t uid,
+ uid_t realUid) {
+ int sock = socket(protocol, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ EXPECT_LE(0, sock);
+ *cookie = getSocketCookie(sock);
+ EXPECT_NE(NONEXISTENT_COOKIE, *cookie);
+ EXPECT_EQ(0, mBh.tagSocket(sock, tag, uid, realUid));
+ return sock;
+ }
+
+ void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
+ Result<UidTagValue> tagResult = mFakeCookieTagMap.readValue(cookie);
+ ASSERT_RESULT_OK(tagResult);
+ EXPECT_EQ(uid, tagResult.value().uid);
+ EXPECT_EQ(tag, tagResult.value().tag);
+ }
+
+ void expectNoTag(uint64_t cookie) { EXPECT_FALSE(mFakeCookieTagMap.readValue(cookie).ok()); }
+
+ void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
+ UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
+ EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
+ *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
+ StatsValue statsMapValue = {.rxPackets = 1, .rxBytes = 100};
+ EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+ key->tag = 0;
+ EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+ // put tag information back to statsKey
+ key->tag = tag;
+ }
+
+ template <class Key, class Value>
+ void expectMapEmpty(BpfMap<Key, Value>& map) {
+ auto isEmpty = map.isEmpty();
+ EXPECT_RESULT_OK(isEmpty);
+ EXPECT_TRUE(isEmpty.value());
+ }
+
+ void expectTagSocketReachLimit(uint32_t tag, uint32_t uid) {
+ int sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ EXPECT_LE(0, sock);
+ if (sock < 0) return;
+ uint64_t sockCookie = getSocketCookie(sock);
+ EXPECT_NE(NONEXISTENT_COOKIE, sockCookie);
+ EXPECT_EQ(-EMFILE, mBh.tagSocket(sock, tag, uid, uid));
+ expectNoTag(sockCookie);
+
+ // Delete stats entries then tag socket success
+ StatsKey key = {.uid = uid, .tag = 0, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
+ ASSERT_RESULT_OK(mFakeStatsMapA.deleteValue(key));
+ EXPECT_EQ(0, mBh.tagSocket(sock, tag, uid, uid));
+ expectUidTag(sockCookie, uid, tag);
+ }
+};
+
+TEST_F(BpfHandlerTest, TestTagSocketV4) {
+ uint64_t sockCookie;
+ int v4socket = setUpSocketAndTag(AF_INET, &sockCookie, TEST_TAG, TEST_UID, TEST_UID);
+ expectUidTag(sockCookie, TEST_UID, TEST_TAG);
+ ASSERT_EQ(0, mBh.untagSocket(v4socket));
+ expectNoTag(sockCookie);
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestReTagSocket) {
+ uint64_t sockCookie;
+ int v4socket = setUpSocketAndTag(AF_INET, &sockCookie, TEST_TAG, TEST_UID, TEST_UID);
+ expectUidTag(sockCookie, TEST_UID, TEST_TAG);
+ ASSERT_EQ(0, mBh.tagSocket(v4socket, TEST_TAG + 1, TEST_UID + 1, TEST_UID + 1));
+ expectUidTag(sockCookie, TEST_UID + 1, TEST_TAG + 1);
+}
+
+TEST_F(BpfHandlerTest, TestTagTwoSockets) {
+ uint64_t sockCookie1;
+ uint64_t sockCookie2;
+ int v4socket1 = setUpSocketAndTag(AF_INET, &sockCookie1, TEST_TAG, TEST_UID, TEST_UID);
+ setUpSocketAndTag(AF_INET, &sockCookie2, TEST_TAG, TEST_UID, TEST_UID);
+ expectUidTag(sockCookie1, TEST_UID, TEST_TAG);
+ expectUidTag(sockCookie2, TEST_UID, TEST_TAG);
+ ASSERT_EQ(0, mBh.untagSocket(v4socket1));
+ expectNoTag(sockCookie1);
+ expectUidTag(sockCookie2, TEST_UID, TEST_TAG);
+ ASSERT_FALSE(mFakeCookieTagMap.getNextKey(sockCookie2).ok());
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketV6) {
+ uint64_t sockCookie;
+ int v6socket = setUpSocketAndTag(AF_INET6, &sockCookie, TEST_TAG, TEST_UID, TEST_UID);
+ expectUidTag(sockCookie, TEST_UID, TEST_TAG);
+ ASSERT_EQ(0, mBh.untagSocket(v6socket));
+ expectNoTag(sockCookie);
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestTagInvalidSocket) {
+ int invalidSocket = -1;
+ ASSERT_GT(0, mBh.tagSocket(invalidSocket, TEST_TAG, TEST_UID, TEST_UID));
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketWithoutPermission) {
+ int sock = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ ASSERT_NE(-1, sock);
+ ASSERT_EQ(-EPERM, mBh.tagSocket(sock, TEST_TAG, TEST_UID, TEST_UID2));
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketWithPermission) {
+ // Grant permission to real uid. In practice, the uid permission map will be updated by
+ // TrafficController::setPermissionForUids().
+ uid_t realUid = TEST_UID2;
+ ASSERT_RESULT_OK(mFakeUidPermissionMap.writeValue(realUid,
+ BPF_PERMISSION_UPDATE_DEVICE_STATS, BPF_ANY));
+
+ // Tag a socket to a different uid other then realUid.
+ uint64_t sockCookie;
+ int v6socket = setUpSocketAndTag(AF_INET6, &sockCookie, TEST_TAG, TEST_UID, realUid);
+ expectUidTag(sockCookie, TEST_UID, TEST_TAG);
+ EXPECT_EQ(0, mBh.untagSocket(v6socket));
+ expectNoTag(sockCookie);
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestUntagInvalidSocket) {
+ int invalidSocket = -1;
+ ASSERT_GT(0, mBh.untagSocket(invalidSocket));
+ int v4socket = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ ASSERT_GT(0, mBh.untagSocket(v4socket));
+ expectMapEmpty(mFakeCookieTagMap);
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketReachLimitFail) {
+ uid_t uid = TEST_UID;
+ StatsKey tagStatsMapKey[3];
+ for (int i = 0; i < 3; i++) {
+ uint64_t cookie = TEST_COOKIE + i;
+ uint32_t tag = TEST_TAG + i;
+ populateFakeStats(cookie, uid, tag, &tagStatsMapKey[i]);
+ }
+ expectTagSocketReachLimit(TEST_TAG, TEST_UID);
+}
+
+TEST_F(BpfHandlerTest, TestTagSocketReachTotalLimitFail) {
+ StatsKey tagStatsMapKey[4];
+ for (int i = 0; i < 4; i++) {
+ uint64_t cookie = TEST_COOKIE + i;
+ uint32_t tag = TEST_TAG + i;
+ uid_t uid = TEST_UID + i;
+ populateFakeStats(cookie, uid, tag, &tagStatsMapKey[i]);
+ }
+ expectTagSocketReachLimit(TEST_TAG, TEST_UID);
+}
+
+} // namespace net
+} // namespace android
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
new file mode 100644
index 0000000..f0997fc
--- /dev/null
+++ b/netd/NetdUpdatable.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NetdUpdatable"
+
+#include "NetdUpdatable.h"
+
+#include <android-base/logging.h>
+#include <netdutils/Status.h>
+
+#include "NetdUpdatablePublic.h"
+
+int libnetd_updatable_init(const char* cg2_path) {
+ android::base::InitLogging(/*argv=*/nullptr);
+ LOG(INFO) << __func__ << ": Initializing";
+
+ android::net::gNetdUpdatable = android::net::NetdUpdatable::getInstance();
+ android::netdutils::Status ret = android::net::gNetdUpdatable->mBpfHandler.init(cg2_path);
+ if (!android::netdutils::isOk(ret)) {
+ LOG(ERROR) << __func__ << ": BPF handler init failed";
+ return -ret.code();
+ }
+ return 0;
+}
+
+int libnetd_updatable_tagSocket(int sockFd, uint32_t tag, uid_t chargeUid, uid_t realUid) {
+ if (android::net::gNetdUpdatable == nullptr) return -EPERM;
+ return android::net::gNetdUpdatable->mBpfHandler.tagSocket(sockFd, tag, chargeUid, realUid);
+}
+
+int libnetd_updatable_untagSocket(int sockFd) {
+ if (android::net::gNetdUpdatable == nullptr) return -EPERM;
+ return android::net::gNetdUpdatable->mBpfHandler.untagSocket(sockFd);
+}
+
+namespace android {
+namespace net {
+
+NetdUpdatable* gNetdUpdatable = nullptr;
+
+NetdUpdatable* NetdUpdatable::getInstance() {
+ // Instantiated on first use.
+ static NetdUpdatable instance;
+ return &instance;
+}
+
+} // namespace net
+} // namespace android
diff --git a/netd/NetdUpdatable.h b/netd/NetdUpdatable.h
new file mode 100644
index 0000000..333037f
--- /dev/null
+++ b/netd/NetdUpdatable.h
@@ -0,0 +1,37 @@
+/**
+ * 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 "BpfHandler.h"
+
+namespace android {
+namespace net {
+
+class NetdUpdatable {
+ public:
+ NetdUpdatable() = default;
+ NetdUpdatable(const NetdUpdatable&) = delete;
+ NetdUpdatable& operator=(const NetdUpdatable&) = delete;
+ static NetdUpdatable* getInstance();
+
+ BpfHandler mBpfHandler;
+};
+
+extern NetdUpdatable* gNetdUpdatable;
+
+} // namespace net
+} // namespace android
\ No newline at end of file
diff --git a/netd/include/NetdUpdatablePublic.h b/netd/include/NetdUpdatablePublic.h
new file mode 100644
index 0000000..1ca5ea2
--- /dev/null
+++ b/netd/include/NetdUpdatablePublic.h
@@ -0,0 +1,61 @@
+/*
+ * 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 <stdint.h>
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+__BEGIN_DECLS
+
+/*
+ * Initial function for libnetd_updatable library.
+ *
+ * The function uses |cg2_path| as cgroup v2 mount location to attach BPF programs so that the
+ * kernel can record packet number, size, etc. in BPF maps when packets pass through, and let user
+ * space retrieve statistics.
+ *
+ * Returns 0 on success, or a negative POSIX error code (see errno.h) on
+ * failure.
+ */
+int libnetd_updatable_init(const char* cg2_path);
+
+/*
+ * Set the socket tag and owning UID for traffic statistics on the specified socket. Permission
+ * check is performed based on the |realUid| before socket tagging.
+ *
+ * The |sockFd| is a file descriptor of the socket that needs to tag. The |tag| is the mark to tag.
+ * It can be an arbitrary value in uint32_t range. The |chargeUid| is owning uid which will be
+ * tagged along with the |tag|. The |realUid| is an effective uid of the calling process, which is
+ * used for permission check before socket tagging.
+ *
+ * Returns 0 on success, or a negative POSIX error code (see errno.h) on failure.
+ */
+int libnetd_updatable_tagSocket(int sockFd, uint32_t tag, uid_t chargeUid,
+ uid_t realUid);
+
+/*
+ * Untag a network socket. Future traffic on this socket will no longer be associated with any
+ * previously configured tag and uid.
+ *
+ * The |sockFd| is a file descriptor of the socket that wants to untag.
+ *
+ * Returns 0 on success, or a negative POSIX error code (see errno.h) on failure.
+ */
+int libnetd_updatable_untagSocket(int sockFd);
+
+__END_DECLS
\ No newline at end of file
diff --git a/netd/libnetd_updatable.map.txt b/netd/libnetd_updatable.map.txt
new file mode 100644
index 0000000..dcb11a1
--- /dev/null
+++ b/netd/libnetd_updatable.map.txt
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+# This lists the entry points visible to applications that use the libnetd_updatable
+# library. Other entry points present in the library won't be usable.
+
+LIBNETD_UPDATABLE {
+ global:
+ libnetd_updatable_init; # apex
+ libnetd_updatable_tagSocket; # apex
+ libnetd_updatable_untagSocket; # apex
+ local:
+ *;
+};