From 1647f60d66cf7f85586607d49e94fd0747734c76 Mon Sep 17 00:00:00 2001 From: Ken Chen Date: Tue, 5 Oct 2021 21:55:22 +0800 Subject: [PATCH] [NETD-BPF#19] Mainline part of bpf code from netd 1. Add libnetd_updatable.so in com.android.tethering. The library is loaded by netd. Currently, it mainly targets on a few functions which access BPF maps. The functionality may extend in the future. 2. Attach gcroup progs from libnetd_updatable.so. 3. Move (privileged)TagSocket and untagSocket implementation to mainline module. Combine privilegedTagSocket and untagSocket into a single function. 4. Split related unit tests from netd_unit_test to libnetd_updatable_unit_test as well. Bug: 202086915 Test: cd system/netd; atest Test: atest TrafficStatsTest NetworkUsageStatsTest Change-Id: Ib556458103a4cbb643c1342d9b689ac692160de0 --- TEST_MAPPING | 10 ++ Tethering/apex/Android.bp | 1 + bpf_progs/Android.bp | 1 + netd/Android.bp | 81 ++++++++++ netd/BpfHandler.cpp | 212 +++++++++++++++++++++++++ netd/BpfHandler.h | 83 ++++++++++ netd/BpfHandlerTest.cpp | 244 +++++++++++++++++++++++++++++ netd/NetdUpdatable.cpp | 61 ++++++++ netd/NetdUpdatable.h | 37 +++++ netd/include/NetdUpdatablePublic.h | 61 ++++++++ netd/libnetd_updatable.map.txt | 27 ++++ 11 files changed, 818 insertions(+) create mode 100644 netd/Android.bp create mode 100644 netd/BpfHandler.cpp create mode 100644 netd/BpfHandler.h create mode 100644 netd/BpfHandlerTest.cpp create mode 100644 netd/NetdUpdatable.cpp create mode 100644 netd/NetdUpdatable.h create mode 100644 netd/include/NetdUpdatablePublic.h create mode 100644 netd/libnetd_updatable.map.txt diff --git a/TEST_MAPPING b/TEST_MAPPING index 780ba267a3..302c0b363d 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -18,6 +18,9 @@ } ] }, + { + "name": "netd_updatable_unit_test" + }, { "name": "TetheringTests" }, @@ -39,6 +42,10 @@ { "name": "bpf_existence_test" }, + { + "name": "netd_updatable_unit_test", + "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"] + }, { "name": "libclat_test" }, @@ -62,6 +69,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 53a14ad9db..f9606f8597 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp @@ -55,6 +55,7 @@ apex { "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 cce2b71d70..6718402652 100644 --- a/bpf_progs/Android.bp +++ b/bpf_progs/Android.bp @@ -42,6 +42,7 @@ cc_library_headers { // 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 0000000000..53c1bd4cfa --- /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 0000000000..3cd5e133c1 --- /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 + +#include +#include +#include +#include +#include + +#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&) { + if (key.uid == chargeUid) { + perUidEntryCount++; + } + totalEntryCount++; + return base::Result(); + }; + 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& currentMap = + (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB; + base::Result 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 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 0000000000..2ede1c1bc7 --- /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 + +#include +#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 mCookieTagMap; + BpfMap mStatsMapA; + BpfMap mStatsMapB; + BpfMap mConfigurationMap; + BpfMap 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 0000000000..db59c7cf32 --- /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 + +#include + +#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 mFakeCookieTagMap; + BpfMap mFakeStatsMapA; + BpfMap mFakeConfigurationMap; + BpfMap 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 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 + void expectMapEmpty(BpfMap& 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 0000000000..f0997fc151 --- /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 +#include + +#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 0000000000..333037fb49 --- /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 0000000000..1ca5ea2aeb --- /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 +#include +#include + +__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 0000000000..dcb11a1019 --- /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: + *; +};