Snap for 8164116 from 1fdf2c966498ac8b89b0815a270baf0b1841754e to mainline-resolv-release Change-Id: I147db7cd14b9dcd2634c3303064dc2531f56ea9e
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts new file mode 100644 index 0000000..a6627fe --- /dev/null +++ b/OWNERS_core_networking_xts
@@ -0,0 +1,2 @@ [email protected] [email protected]
diff --git a/TEST_MAPPING b/TEST_MAPPING index 6996ad9..302c0b3 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING
@@ -19,10 +19,19 @@ ] }, { + "name": "netd_updatable_unit_test" + }, + { "name": "TetheringTests" }, { "name": "TetheringIntegrationTests" + }, + { + "name": "traffic_controller_unit_test" + }, + { + "name": "libnetworkstats_test" } ], "postsubmit": [ @@ -32,6 +41,20 @@ // TODO: move to presubmit when known green. { "name": "bpf_existence_test" + }, + { + "name": "netd_updatable_unit_test", + "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"] + }, + { + "name": "libclat_test" + }, + { + "name": "traffic_controller_unit_test", + "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"] + }, + { + "name": "libnetworkstats_test" } ], "mainline-presubmit": [ @@ -47,7 +70,16 @@ ] }, { + "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]" + }, + { + "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/Tethering/Android.bp b/Tethering/Android.bp index 3e1eec7..ae96e8c 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp
@@ -53,6 +53,7 @@ "framework-statsd.stubs.module_lib", "framework-tethering.impl", "framework-wifi", + "framework-bluetooth", "unsupportedappusage", ], plugins: ["java_api_finder"], @@ -64,6 +65,7 @@ android_library { name: "TetheringApiCurrentLib", defaults: [ + "ConnectivityNextEnableDefaults", "TetheringAndroidLibraryDefaults", "TetheringApiLevel" ], @@ -97,7 +99,6 @@ ], min_sdk_version: "30", header_libs: [ - "bpf_syscall_wrappers", "bpf_connectivity_headers", ], srcs: [ @@ -157,7 +158,7 @@ // Non-updatable tethering running in the system server process for devices not using the module android_app { name: "InProcessTethering", - defaults: ["TetheringAppDefaults", "TetheringApiLevel"], + defaults: ["TetheringAppDefaults", "TetheringApiLevel", "ConnectivityNextEnableDefaults"], static_libs: ["TetheringApiCurrentLib"], certificate: "platform", manifest: "AndroidManifest_InProcess.xml", @@ -207,4 +208,5 @@ sdk { name: "tethering-module-sdk", bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"], + systemserverclasspath_fragments: ["com.android.tethering-systemserverclasspath-fragment"], }
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp index 9c0722a..416a7f7 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp
@@ -31,6 +31,7 @@ // package names and keys, so that apex will be unused anyway. apps: ["Tethering"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false. } +enable_tethering_next_apex = false // This is a placeholder comment to avoid merge conflicts // as the above target may have different "enabled" values // depending on the branch @@ -44,21 +45,29 @@ bootclasspath_fragments: [ "com.android.tethering-bootclasspath-fragment", ], - java_libs: [ - "service-connectivity", + systemserverclasspath_fragments: [ + "com.android.tethering-systemserverclasspath-fragment", ], multilib: { first: { jni_libs: [ "libservice-connectivity", - "libcom_android_connectivity_com_android_net_module_util_jni" + "libandroid_net_connectivity_com_android_net_module_util_jni", ], + native_shared_libs: ["libnetd_updatable"], }, both: { jni_libs: ["libframework-connectivity-jni"], }, }, + binaries: [ + "clatd", + ], + canned_fs_config: "canned_fs_config", bpfs: [ + "clatd.o_mainline", + "netd.o_mainline", + "dscp_policy.o", "offload.o", "test.o", ], @@ -91,6 +100,7 @@ name: "com.android.tethering-bootclasspath-fragment", contents: [ "framework-connectivity", + // Changed in sc-mainline-prod only: no framework-connectivity-tiramisu "framework-tethering", ], apex_available: ["com.android.tethering"], @@ -112,15 +122,30 @@ // Additional hidden API flag files to override the defaults. This must only be // modified by the Soong or platform compat team. hidden_api: { - max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"], + max_target_r_low_priority: [ + // Changed in sc-mainline-prod only: no list for + // framework-connectivity-tiramisu APIs as it is not in the APEX + ], + max_target_o_low_priority: [ + "hiddenapi/hiddenapi-max-target-o-low-priority.txt", + // Changed in sc-mainline-prod only: no list for + // framework-connectivity-tiramisu APIs as it is not in the APEX + ], unsupported: ["hiddenapi/hiddenapi-unsupported.txt"], }, } +systemserverclasspath_fragment { + name: "com.android.tethering-systemserverclasspath-fragment", + standalone_contents: ["service-connectivity"], + apex_available: ["com.android.tethering"], +} + override_apex { name: "com.android.tethering.inprocess", base: "com.android.tethering", package_name: "com.android.tethering.inprocess", + enabled: enable_tethering_next_apex, apps: [ "ServiceConnectivityResources", "InProcessTethering",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config new file mode 100644 index 0000000..5a03347 --- /dev/null +++ b/Tethering/apex/canned_fs_config
@@ -0,0 +1,2 @@ +/bin/for-system 0 1000 0750 +/bin/for-system/clatd 1029 1029 06755
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt new file mode 100644 index 0000000..88c77f2 --- /dev/null +++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
@@ -0,0 +1,87 @@ +Landroid/net/nsd/DnsSdTxtRecord;-><init>()V +Landroid/net/nsd/DnsSdTxtRecord;-><init>(Landroid/net/nsd/DnsSdTxtRecord;)V +Landroid/net/nsd/DnsSdTxtRecord;-><init>([B)V +Landroid/net/nsd/DnsSdTxtRecord;->contains(Ljava/lang/String;)Z +Landroid/net/nsd/DnsSdTxtRecord;->CREATOR:Landroid/os/Parcelable$Creator; +Landroid/net/nsd/DnsSdTxtRecord;->get(Ljava/lang/String;)Ljava/lang/String; +Landroid/net/nsd/DnsSdTxtRecord;->getKey(I)Ljava/lang/String; +Landroid/net/nsd/DnsSdTxtRecord;->getRawData()[B +Landroid/net/nsd/DnsSdTxtRecord;->getValue(I)[B +Landroid/net/nsd/DnsSdTxtRecord;->getValue(Ljava/lang/String;)[B +Landroid/net/nsd/DnsSdTxtRecord;->getValueAsString(I)Ljava/lang/String; +Landroid/net/nsd/DnsSdTxtRecord;->insert([B[BI)V +Landroid/net/nsd/DnsSdTxtRecord;->keyCount()I +Landroid/net/nsd/DnsSdTxtRecord;->mData:[B +Landroid/net/nsd/DnsSdTxtRecord;->mSeperator:B +Landroid/net/nsd/DnsSdTxtRecord;->remove(Ljava/lang/String;)I +Landroid/net/nsd/DnsSdTxtRecord;->set(Ljava/lang/String;Ljava/lang/String;)V +Landroid/net/nsd/DnsSdTxtRecord;->size()I +Landroid/net/nsd/INsdManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V +Landroid/net/nsd/INsdManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; +Landroid/net/nsd/INsdManager$Stub$Proxy;->getMessenger()Landroid/os/Messenger; +Landroid/net/nsd/INsdManager$Stub$Proxy;->mRemote:Landroid/os/IBinder; +Landroid/net/nsd/INsdManager$Stub$Proxy;->setEnabled(Z)V +Landroid/net/nsd/INsdManager$Stub;-><init>()V +Landroid/net/nsd/INsdManager$Stub;->DESCRIPTOR:Ljava/lang/String; +Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_getMessenger:I +Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_setEnabled:I +Landroid/net/nsd/INsdManager;->setEnabled(Z)V +Landroid/net/nsd/NsdManager;-><init>(Landroid/content/Context;Landroid/net/nsd/INsdManager;)V +Landroid/net/nsd/NsdManager;->BASE:I +Landroid/net/nsd/NsdManager;->checkListener(Ljava/lang/Object;)V +Landroid/net/nsd/NsdManager;->checkProtocol(I)V +Landroid/net/nsd/NsdManager;->checkServiceInfo(Landroid/net/nsd/NsdServiceInfo;)V +Landroid/net/nsd/NsdManager;->DBG:Z +Landroid/net/nsd/NsdManager;->DISABLE:I +Landroid/net/nsd/NsdManager;->disconnect()V +Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES:I +Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_FAILED:I +Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_STARTED:I +Landroid/net/nsd/NsdManager;->ENABLE:I +Landroid/net/nsd/NsdManager;->EVENT_NAMES:Landroid/util/SparseArray; +Landroid/net/nsd/NsdManager;->fatal(Ljava/lang/String;)V +Landroid/net/nsd/NsdManager;->FIRST_LISTENER_KEY:I +Landroid/net/nsd/NsdManager;->getListenerKey(Ljava/lang/Object;)I +Landroid/net/nsd/NsdManager;->getMessenger()Landroid/os/Messenger; +Landroid/net/nsd/NsdManager;->getNsdServiceInfoType(Landroid/net/nsd/NsdServiceInfo;)Ljava/lang/String; +Landroid/net/nsd/NsdManager;->init()V +Landroid/net/nsd/NsdManager;->mAsyncChannel:Lcom/android/internal/util/AsyncChannel; +Landroid/net/nsd/NsdManager;->mConnected:Ljava/util/concurrent/CountDownLatch; +Landroid/net/nsd/NsdManager;->mContext:Landroid/content/Context; +Landroid/net/nsd/NsdManager;->mHandler:Landroid/net/nsd/NsdManager$ServiceHandler; +Landroid/net/nsd/NsdManager;->mListenerKey:I +Landroid/net/nsd/NsdManager;->mListenerMap:Landroid/util/SparseArray; +Landroid/net/nsd/NsdManager;->mMapLock:Ljava/lang/Object; +Landroid/net/nsd/NsdManager;->mService:Landroid/net/nsd/INsdManager; +Landroid/net/nsd/NsdManager;->mServiceMap:Landroid/util/SparseArray; +Landroid/net/nsd/NsdManager;->nameOf(I)Ljava/lang/String; +Landroid/net/nsd/NsdManager;->NATIVE_DAEMON_EVENT:I +Landroid/net/nsd/NsdManager;->nextListenerKey()I +Landroid/net/nsd/NsdManager;->putListener(Ljava/lang/Object;Landroid/net/nsd/NsdServiceInfo;)I +Landroid/net/nsd/NsdManager;->REGISTER_SERVICE:I +Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_FAILED:I +Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_SUCCEEDED:I +Landroid/net/nsd/NsdManager;->removeListener(I)V +Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE:I +Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_FAILED:I +Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_SUCCEEDED:I +Landroid/net/nsd/NsdManager;->SERVICE_FOUND:I +Landroid/net/nsd/NsdManager;->SERVICE_LOST:I +Landroid/net/nsd/NsdManager;->setEnabled(Z)V +Landroid/net/nsd/NsdManager;->STOP_DISCOVERY:I +Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_FAILED:I +Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_SUCCEEDED:I +Landroid/net/nsd/NsdManager;->TAG:Ljava/lang/String; +Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE:I +Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_FAILED:I +Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_SUCCEEDED:I +Landroid/net/nsd/NsdServiceInfo;-><init>(Ljava/lang/String;Ljava/lang/String;)V +Landroid/net/nsd/NsdServiceInfo;->getTxtRecord()[B +Landroid/net/nsd/NsdServiceInfo;->getTxtRecordSize()I +Landroid/net/nsd/NsdServiceInfo;->mHost:Ljava/net/InetAddress; +Landroid/net/nsd/NsdServiceInfo;->mPort:I +Landroid/net/nsd/NsdServiceInfo;->mServiceName:Ljava/lang/String; +Landroid/net/nsd/NsdServiceInfo;->mServiceType:Ljava/lang/String; +Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap; +Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V +Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-r-loprio.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-r-loprio.txt new file mode 100644 index 0000000..211b847 --- /dev/null +++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-r-loprio.txt
@@ -0,0 +1 @@ +Landroid/net/nsd/INsdManager$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/nsd/INsdManager;
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp deleted file mode 100644 index f9e4824..0000000 --- a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp +++ /dev/null
@@ -1,397 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <arpa/inet.h> -#include <jni.h> -#include <linux/if_arp.h> -#include <linux/if_ether.h> -#include <linux/netlink.h> -#include <linux/pkt_cls.h> -#include <linux/pkt_sched.h> -#include <linux/rtnetlink.h> -#include <nativehelper/JNIHelp.h> -#include <net/if.h> -#include <stdio.h> -#include <sys/socket.h> -#include <sys/utsname.h> - -// TODO: use unique_fd. -#define BPF_FD_JUST_USE_INT -#include "BpfSyscallWrappers.h" -#include "bpf_tethering.h" -#include "nativehelper/scoped_utf_chars.h" - -// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c. -#define CLS_BPF_NAME_LEN 256 - -// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c. -#define CLS_BPF_KIND_NAME "bpf" - -namespace android { -// Sync from system/netd/server/NetlinkCommands.h -const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; -const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; - -static void throwIOException(JNIEnv *env, const char* msg, int error) { - jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error)); -} - -// TODO: move to frameworks/libs/net/common/native for sharing with -// system/netd/server/OffloadUtils.{c, h}. -static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) { - int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd - if (fd == -1) { - throwIOException(env, "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)", errno); - return; - } - - static constexpr int on = 1; - if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) { - throwIOException(env, "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1)", errno); - close(fd); - return; - } - - // this is needed to get valid strace netlink parsing, it allocates the pid - if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { - throwIOException(env, "bind(fd, {AF_NETLINK, 0, 0})", errno); - close(fd); - return; - } - - // we do not want to receive messages from anyone besides the kernel - if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { - throwIOException(env, "connect(fd, {AF_NETLINK, 0, 0})", errno); - close(fd); - return; - } - - int rv = send(fd, req, len, 0); - - if (rv == -1) { - throwIOException(env, "send(fd, req, len, 0)", errno); - close(fd); - return; - } - - if (rv != len) { - throwIOException(env, "send(fd, req, len, 0)", EMSGSIZE); - close(fd); - return; - } - - struct { - nlmsghdr h; - nlmsgerr e; - char buf[256]; - } resp = {}; - - rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC); - - if (rv == -1) { - throwIOException(env, "recv() failed", errno); - close(fd); - return; - } - - if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { - jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv); - close(fd); - return; - } - - if (resp.h.nlmsg_len != (unsigned)rv) { - jniThrowExceptionFmt(env, "java/io/IOException", - "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, - rv); - close(fd); - return; - } - - if (resp.h.nlmsg_type != NLMSG_ERROR) { - jniThrowExceptionFmt(env, "java/io/IOException", - "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); - close(fd); - return; - } - - if (resp.e.error) { // returns 0 on success - throwIOException(env, "NLMSG_ERROR message return error", -resp.e.error); - } - close(fd); - return; -} - -static int hardwareAddressType(const char* interface) { - int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); - if (fd < 0) return -errno; - - struct ifreq ifr = {}; - // We use strncpy() instead of strlcpy() since kernel has to be able - // to handle non-zero terminated junk passed in by userspace anyway, - // and this way too long interface names (more than IFNAMSIZ-1 = 15 - // characters plus terminating NULL) will not get truncated to 15 - // characters and zero-terminated and thus potentially erroneously - // match a truncated interface if one were to exist. - strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name)); - - int rv; - if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) { - rv = -errno; - } else { - rv = ifr.ifr_hwaddr.sa_family; - } - - close(fd); - return rv; -} - -// ----------------------------------------------------------------------------- -// TODO - just use BpfUtils.h once that is available in sc-mainline-prod and has kernelVersion() -// -// In the mean time copying verbatim from: -// system/bpf/libbpf_android/include/bpf/BpfUtils.h -// and -// system/bpf/libbpf_android/BpfUtils.cpp - -#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c)) - -static unsigned kernelVersion() { - struct utsname buf; - int ret = uname(&buf); - if (ret) return 0; - - unsigned kver_major; - unsigned kver_minor; - unsigned kver_sub; - char discard; - ret = sscanf(buf.release, "%u.%u.%u%c", &kver_major, &kver_minor, &kver_sub, &discard); - // Check the device kernel version - if (ret < 3) return 0; - - return KVER(kver_major, kver_minor, kver_sub); -} - -static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) { - return kernelVersion() >= KVER(major, minor, sub); -} -// ----------------------------------------------------------------------------- - -static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz, - jstring iface) { - ScopedUtfChars interface(env, iface); - - int rv = hardwareAddressType(interface.c_str()); - if (rv < 0) { - jniThrowExceptionFmt(env, "java/io/IOException", - "Get hardware address type of interface %s failed: %s", - interface.c_str(), strerror(-rv)); - return false; - } - - // Backwards compatibility with pre-GKI kernels that use various custom - // ARPHRD_* for their cellular interface - switch (rv) { - // ARPHRD_PUREIP on at least some Mediatek Android kernels - // example: wembley with 4.19 kernel - case 520: - // in Linux 4.14+ rmnet support was upstreamed and ARHRD_RAWIP became 519, - // but it is 530 on at least some Qualcomm Android 4.9 kernels with rmnet - // example: Pixel 3 family - case 530: - // >5.4 kernels are GKI2.0 and thus upstream compatible, however 5.10 - // shipped with Android S, so (for safety) let's limit ourselves to - // >5.10, ie. 5.11+ as a guarantee we're on Android T+ and thus no - // longer need this non-upstream compatibility logic - static bool is_pre_5_11_kernel = !isAtLeastKernelVersion(5, 11, 0); - if (is_pre_5_11_kernel) return false; - } - - switch (rv) { - case ARPHRD_ETHER: - return true; - case ARPHRD_NONE: - case ARPHRD_PPP: - case ARPHRD_RAWIP: - return false; - default: - jniThrowExceptionFmt(env, "java/io/IOException", - "Unknown hardware address type %d on interface %s", rv, - interface.c_str()); - return false; - } -} - -// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... -// direct-action -static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf( - JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto, - jstring bpfProgPath) { - ScopedUtfChars pathname(env, bpfProgPath); - - const int bpfFd = bpf::retrieveProgram(pathname.c_str()); - if (bpfFd == -1) { - throwIOException(env, "retrieveProgram failed", errno); - return; - } - - struct { - nlmsghdr n; - tcmsg t; - struct { - nlattr attr; - // The maximum classifier name length is defined in - // tcf_proto_ops in include/net/sch_generic.h. - char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))]; - } kind; - struct { - nlattr attr; - struct { - nlattr attr; - __u32 u32; - } fd; - struct { - nlattr attr; - char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)]; - } name; - struct { - nlattr attr; - __u32 u32; - } flags; - } options; - } req = { - .n = - { - .nlmsg_len = sizeof(req), - .nlmsg_type = RTM_NEWTFILTER, - .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE, - }, - .t = - { - .tcm_family = AF_UNSPEC, - .tcm_ifindex = ifIndex, - .tcm_handle = TC_H_UNSPEC, - .tcm_parent = TC_H_MAKE(TC_H_CLSACT, - ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), - .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) | - htons(static_cast<uint16_t>(proto))), - }, - .kind = - { - .attr = - { - .nla_len = sizeof(req.kind), - .nla_type = TCA_KIND, - }, - .str = CLS_BPF_KIND_NAME, - }, - .options = - { - .attr = - { - .nla_len = sizeof(req.options), - .nla_type = NLA_F_NESTED | TCA_OPTIONS, - }, - .fd = - { - .attr = - { - .nla_len = sizeof(req.options.fd), - .nla_type = TCA_BPF_FD, - }, - .u32 = static_cast<__u32>(bpfFd), - }, - .name = - { - .attr = - { - .nla_len = sizeof(req.options.name), - .nla_type = TCA_BPF_NAME, - }, - // Visible via 'tc filter show', but - // is overwritten by strncpy below - .str = "placeholder", - }, - .flags = - { - .attr = - { - .nla_len = sizeof(req.options.flags), - .nla_type = TCA_BPF_FLAGS, - }, - .u32 = TCA_BPF_FLAG_ACT_DIRECT, - }, - }, - }; - - snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]", - basename(pathname.c_str())); - - // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of - // BPF program before returning the function in any case. - sendAndProcessNetlinkResponse(env, &req, sizeof(req)); - close(bpfFd); -} - -// tc filter del dev .. in/egress prio .. protocol .. -static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz, - jint ifIndex, - jboolean ingress, - jshort prio, jshort proto) { - const struct { - nlmsghdr n; - tcmsg t; - } req = { - .n = - { - .nlmsg_len = sizeof(req), - .nlmsg_type = RTM_DELTFILTER, - .nlmsg_flags = NETLINK_REQUEST_FLAGS, - }, - .t = - { - .tcm_family = AF_UNSPEC, - .tcm_ifindex = ifIndex, - .tcm_handle = TC_H_UNSPEC, - .tcm_parent = TC_H_MAKE(TC_H_CLSACT, - ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), - .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) | - htons(static_cast<uint16_t>(proto))), - }, - }; - - sendAndProcessNetlinkResponse(env, &req, sizeof(req)); -} - -/* - * JNI registration. - */ -static const JNINativeMethod gMethods[] = { - /* name, signature, funcPtr */ - {"isEthernet", "(Ljava/lang/String;)Z", - (void*)com_android_networkstack_tethering_BpfUtils_isEthernet}, - {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V", - (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf}, - {"tcFilterDelDev", "(IZSS)V", - (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev}, -}; - -int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) { - return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods, - NELEM(gMethods)); -} - -}; // namespace android
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp index 72895f1..ed80128 100644 --- a/Tethering/jni/onload.cpp +++ b/Tethering/jni/onload.cpp
@@ -23,6 +23,7 @@ namespace android { int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); +int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name); int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env); int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env); int register_com_android_networkstack_tethering_util_TetheringUtils(JNIEnv* env); @@ -39,9 +40,10 @@ if (register_com_android_net_module_util_BpfMap(env, "com/android/networkstack/tethering/util/BpfMap") < 0) return JNI_ERR; - if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR; + if (register_com_android_net_module_util_TcUtils(env, + "com/android/networkstack/tethering/util/TcUtils") < 0) return JNI_ERR; - if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR; + if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR; return JNI_VERSION_1_6; }
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags index f62df7f..6735317 100644 --- a/Tethering/proguard.flags +++ b/Tethering/proguard.flags
@@ -8,6 +8,10 @@ native <methods>; } +-keep class com.android.networkstack.tethering.util.TcUtils { + native <methods>; +} + -keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct { *; }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index b4228da..2bb19db 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java
@@ -614,10 +614,8 @@ return false; } - if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { - // BT configures the interface elsewhere: only start DHCP. - // TODO: make all tethering types behave the same way, and delete the bluetooth - // code that calls into NetworkManagementService directly. + if (shouldNotConfigureBluetoothInterface()) { + // Interface was already configured elsewhere, only start DHCP. return configureDhcp(enabled, mIpv4Address, null /* clientAddress */); } @@ -651,12 +649,15 @@ return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr); } + private boolean shouldNotConfigureBluetoothInterface() { + // Before T, bluetooth tethering configures the interface elsewhere. + return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT(); + } + private LinkAddress requestIpv4Address(final boolean useLastAddress) { if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr; - if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { - return new LinkAddress(BLUETOOTH_IFACE_ADDR); - } + if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR); return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress); }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java index 4f095cf..77efb51 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -24,6 +24,8 @@ import androidx.annotation.NonNull; +import com.android.net.module.util.TcUtils; + import java.io.IOException; /** @@ -53,11 +55,11 @@ static final boolean DOWNSTREAM = true; static final boolean UPSTREAM = false; - // The priority of clat/tether hooks - smaller is higher priority. + // The priority of tether hooks - smaller is higher priority. // TC tether is higher priority then TC clat to match XDP winning over TC. - // Sync from system/netd/server/OffloadUtils.h. - static final short PRIO_TETHER6 = 1; - static final short PRIO_TETHER4 = 2; + // Sync from system/netd/server/TcUtils.h. + static final short PRIO_TETHER6 = 2; + static final short PRIO_TETHER4 = 3; // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) { @@ -82,7 +84,7 @@ boolean ether; try { - ether = isEthernet(iface); + ether = TcUtils.isEthernet(iface); } catch (IOException e) { throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e); } @@ -90,7 +92,7 @@ try { // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/... // direct-action - tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6, + TcUtils.tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6, makeProgPath(downstream, 6, ether)); } catch (IOException e) { throw new IOException("tc filter add dev (" + params.index + "[" + iface @@ -100,7 +102,7 @@ try { // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/... // direct-action - tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP, + TcUtils.tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP, makeProgPath(downstream, 4, ether)); } catch (IOException e) { throw new IOException("tc filter add dev (" + params.index + "[" + iface @@ -121,7 +123,7 @@ try { // tc filter del dev .. ingress prio 1 protocol ipv6 - tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6); + TcUtils.tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6); } catch (IOException e) { throw new IOException("tc filter del dev (" + params.index + "[" + iface + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); @@ -129,18 +131,10 @@ try { // tc filter del dev .. ingress prio 2 protocol ip - tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP); + TcUtils.tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP); } catch (IOException e) { throw new IOException("tc filter del dev (" + params.index + "[" + iface + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); } } - - private static native boolean isEthernet(String iface) throws IOException; - - private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, - short proto, String bpfProgPath) throws IOException; - - private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio, - short proto) throws IOException; }
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index 55c24d3..db9a64f 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -134,7 +134,12 @@ import com.android.internal.util.MessageUtils; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.networkstack.apishim.common.BluetoothPanShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.tethering.util.InterfaceSet; import com.android.networkstack.tethering.util.PrefixUtils; import com.android.networkstack.tethering.util.TetheringUtils; @@ -265,8 +270,11 @@ private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED; private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest; + private TetheredInterfaceRequestShim mBluetoothIfaceRequest; private String mConfiguredEthernetIface; + private String mConfiguredBluetoothIface; private EthernetCallback mEthernetCallback; + private TetheredInterfaceCallbackShim mBluetoothCallback; private SettingsObserver mSettingsObserver; private BluetoothPan mBluetoothPan; private PanServiceListener mBluetoothPanListener; @@ -533,14 +541,16 @@ } } - // This method needs to exist because TETHERING_BLUETOOTH and TETHERING_WIGIG can't use - // enableIpServing. + // This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG + // can't use enableIpServing. private void processInterfaceStateChange(final String iface, boolean enabled) { // Do not listen to USB interface state changes or USB interface add/removes. USB tethering // is driven only by USB_ACTION broadcasts. final int type = ifaceNameToType(iface); if (type == TETHERING_USB || type == TETHERING_NCM) return; + if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return; + if (enabled) { ensureIpServerStarted(iface); } else { @@ -769,6 +779,9 @@ TETHERING_BLUETOOTH); } mPendingPanRequests.clear(); + mBluetoothIfaceRequest = null; + mBluetoothCallback = null; + maybeDisableBluetoothIpServing(); }); } @@ -779,7 +792,11 @@ private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan, final boolean enable, final IIntResultListener listener) { - bluetoothPan.setBluetoothTethering(enable); + if (SdkLevel.isAtLeastT()) { + changeBluetoothTetheringSettings(bluetoothPan, enable); + } else { + changeBluetoothTetheringSettingsPreT(bluetoothPan, enable); + } // Enabling bluetooth tethering settings can silently fail. Send internal error if the // result is not expected. @@ -788,6 +805,68 @@ sendTetherResult(listener, result, TETHERING_BLUETOOTH); } + private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan, + final boolean enable) { + bluetoothPan.setBluetoothTethering(enable); + } + + private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan, + final boolean enable) { + final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan); + if (enable) { + if (mBluetoothIfaceRequest != null) { + Log.d(TAG, "Bluetooth tethering settings already enabled"); + return; + } + + mBluetoothCallback = new BluetoothCallback(); + try { + mBluetoothIfaceRequest = panShim.requestTetheredInterface(mExecutor, + mBluetoothCallback); + } catch (UnsupportedApiLevelException e) { + Log.wtf(TAG, "Use unsupported API, " + e); + } + } else { + if (mBluetoothIfaceRequest == null) { + Log.d(TAG, "Bluetooth tethering settings already disabled"); + return; + } + + mBluetoothIfaceRequest.release(); + mBluetoothIfaceRequest = null; + mBluetoothCallback = null; + // If bluetooth request is released, tethering won't able to receive + // onUnavailable callback, explicitly disable bluetooth IpServer manually. + maybeDisableBluetoothIpServing(); + } + } + + // BluetoothCallback is only called after T. Before T, PanService would call tether/untether to + // notify bluetooth interface status. + private class BluetoothCallback implements TetheredInterfaceCallbackShim { + @Override + public void onAvailable(String iface) { + if (this != mBluetoothCallback) return; + + enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH)); + mConfiguredBluetoothIface = iface; + } + + @Override + public void onUnavailable() { + if (this != mBluetoothCallback) return; + + maybeDisableBluetoothIpServing(); + } + } + + private void maybeDisableBluetoothIpServing() { + if (mConfiguredBluetoothIface == null) return; + + ensureIpServerStopped(mConfiguredBluetoothIface); + mConfiguredBluetoothIface = null; + } + private int setEthernetTethering(final boolean enable) { final EthernetManager em = (EthernetManager) mContext.getSystemService( Context.ETHERNET_SERVICE);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java index 7df9475..c1a747e 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -18,6 +18,7 @@ import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; import android.content.Context; import android.net.INetd; import android.net.ip.IpServer; @@ -31,6 +32,8 @@ import androidx.annotation.NonNull; import com.android.internal.util.StateMachine; +import com.android.networkstack.apishim.BluetoothPanShimImpl; +import com.android.networkstack.apishim.common.BluetoothPanShim; import java.util.ArrayList; @@ -158,4 +161,13 @@ TetheringConfiguration cfg) { return new PrivateAddressCoordinator(ctx, cfg); } + + /** + * Get BluetoothPanShim object to enable/disable bluetooth tethering. + * + * TODO: use BluetoothPan directly when mainline module is built with API 32. + */ + public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) { + return BluetoothPanShimImpl.newInstance(pan); + } }
diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt index 23d3f56..a7c7488 100644 --- a/Tethering/tests/jarjar-rules.txt +++ b/Tethering/tests/jarjar-rules.txt
@@ -13,6 +13,9 @@ # Classes from net-utils-framework-common rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1 +# Classes from net-tests-utils +rule com.android.testutils.TestBpfMap* com.android.networkstack.tethering.testutils.TestBpfMap@1 + # TODO: either stop using frameworks-base-testutils or remove the unit test classes it contains. # TestableLooper from "testables" can be used instead of TestLooper from frameworks-base-testutils. zap android.os.test.TestLooperTest*
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index 2f2cde0..267c376 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -16,6 +16,7 @@ package android.net.ip; +import static android.net.INetd.IF_STATE_DOWN; import static android.net.INetd.IF_STATE_UP; import static android.net.RouteInfo.RTN_UNICAST; import static android.net.TetheringManager.TETHERING_BLUETOOTH; @@ -33,6 +34,7 @@ import static android.net.ip.IpServer.STATE_UNAVAILABLE; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH; @@ -400,11 +402,16 @@ } @Test - public void canBeTethered() throws Exception { + public void canBeTetheredAsBluetooth() throws Exception { initStateMachine(TETHERING_BLUETOOTH); dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); - InOrder inOrder = inOrder(mCallback, mNetd); + InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); + if (isAtLeastT()) { + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> + IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); + } inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); // One for ipv4 route, one for ipv6 link local route. @@ -426,7 +433,13 @@ inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only + // happen after T. Before T, the interface configuration control in bluetooth side. + if (isAtLeastT()) { + inOrder.verify(mNetd).interfaceSetCfg( + argThat(cfg -> assertContainsFlag(cfg.flags, IF_STATE_DOWN))); + } + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> cfg.flags.length == 0)); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); @@ -443,7 +456,7 @@ InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> - IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); + IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), @@ -587,7 +600,8 @@ inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer); inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer); @@ -683,7 +697,11 @@ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE); - assertDhcpStarted(mBluetoothPrefix); + if (isAtLeastT()) { + assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress)); + } else { + assertDhcpStarted(mBluetoothPrefix); + } } @Test @@ -1371,7 +1389,6 @@ for (String flag : flags) { if (flag.equals(match)) return true; } - fail("Missing flag: " + match); return false; }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index c5969d2..d51f6fd 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -89,7 +89,6 @@ import android.os.Build; import android.os.Handler; import android.os.test.TestLooper; -import android.system.ErrnoException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -100,7 +99,6 @@ import com.android.net.module.util.BpfMap; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.NetworkStackConstants; -import com.android.net.module.util.Struct; import com.android.net.module.util.netlink.ConntrackMessage; import com.android.net.module.util.netlink.NetlinkConstants; import com.android.net.module.util.netlink.NetlinkSocket; @@ -110,6 +108,7 @@ import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.TestBpfMap; import com.android.testutils.TestableNetworkStatsProviderCbBinder; import org.junit.Before; @@ -128,10 +127,7 @@ import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.BiConsumer; @RunWith(AndroidJUnit4.class) @SmallTest @@ -157,60 +153,6 @@ UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */, NetworkStackConstants.ETHER_MTU); - // The test fake BPF map class is needed because the test has no privilege to access the BPF - // map. All member functions which eventually call JNI to access the real native BPF map need - // to be overridden. - // TODO: consider moving to an individual file. - private class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> { - private final HashMap<K, V> mMap = new HashMap<K, V>(); - - TestBpfMap(final Class<K> key, final Class<V> value) { - super(key, value); - } - - @Override - public void forEach(BiConsumer<K, V> action) throws ErrnoException { - // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to - // implement the entry deletion in the iteration if required. - for (Map.Entry<K, V> entry : mMap.entrySet()) { - action.accept(entry.getKey(), entry.getValue()); - } - } - - @Override - public void updateEntry(K key, V value) throws ErrnoException { - mMap.put(key, value); - } - - @Override - public void insertEntry(K key, V value) throws ErrnoException, - IllegalArgumentException { - // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry. - if (mMap.get(key) != null) { - throw new IllegalArgumentException(key + " already exist"); - } - mMap.put(key, value); - } - - @Override - public boolean deleteEntry(Struct key) throws ErrnoException { - return mMap.remove(key) != null; - } - - @Override - public V getValue(@NonNull K key) throws ErrnoException { - // Return value for a given key. Otherwise, return null without an error ENOENT. - // BpfMap#getValue treats that the entry is not found as no error. - return mMap.get(key); - } - - @Override - public void clear() throws ErrnoException { - // TODO: consider using mocked #getFirstKey and #deleteEntry to implement. - mMap.clear(); - } - }; - @Mock private NetworkStatsManager mStatsManager; @Mock private INetd mNetd; @Mock private IpServer mIpServer;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 40d133a..e4dbc7d 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -62,6 +62,7 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.modules.utils.build.SdkLevel.isAtLeastS; +import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; @@ -81,6 +82,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Matchers.anyInt; @@ -182,6 +185,10 @@ import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.FakeSettingsProvider; import com.android.net.module.util.CollectionUtils; +import com.android.networkstack.apishim.common.BluetoothPanShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; import com.android.testutils.MiscAsserts; @@ -261,6 +268,8 @@ @Mock private PackageManager mPackageManager; @Mock private BluetoothAdapter mBluetoothAdapter; @Mock private BluetoothPan mBluetoothPan; + @Mock private BluetoothPanShim mBluetoothPanShim; + @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim; private final MockIpServerDependencies mIpServerDependencies = spy(new MockIpServerDependencies()); @@ -285,6 +294,7 @@ private PrivateAddressCoordinator mPrivateAddressCoordinator; private SoftApCallback mSoftApCallback; private UpstreamNetworkMonitor mUpstreamNetworkMonitor; + private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim; private TestConnectivityManager mCm; @@ -483,13 +493,23 @@ return false; } - @Override public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx, TetheringConfiguration cfg) { mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg); return mPrivateAddressCoordinator; } + + @Override + public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) { + try { + when(mBluetoothPanShim.requestTetheredInterface( + any(), any())).thenReturn(mTetheredInterfaceRequestShim); + } catch (UnsupportedApiLevelException e) { + fail("BluetoothPan#requestTetheredInterface is not supported"); + } + return mBluetoothPanShim; + } } private static LinkProperties buildUpstreamLinkProperties(String interfaceName, @@ -2557,6 +2577,44 @@ @Test public void testBluetoothTethering() throws Exception { + // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP. + assumeTrue(isAtLeastT()); + + final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR); + mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result); + mLooper.dispatchAll(); + verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */); + result.assertHasResult(); + + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + verifyNetdCommandForBtSetup(); + + // If PAN disconnect, tethering should also be stopped. + mTetheredInterfaceCallbackShim.onUnavailable(); + mLooper.dispatchAll(); + verifyNetdCommandForBtTearDown(); + + // Tethering could restart if PAN reconnect. + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + verifyNetdCommandForBtSetup(); + + // Pretend that bluetooth tethering was disabled. + mockBluetoothSettings(true /* bluetoothOn */, false /* tetheringOn */); + mTethering.stopTethering(TETHERING_BLUETOOTH); + mLooper.dispatchAll(); + verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */); + + verifyNetdCommandForBtTearDown(); + } + + @Test + public void testBluetoothTetheringBeforeT() throws Exception { + // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP. + assumeFalse(isAtLeastT()); + final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR); mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */); mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result); @@ -2610,12 +2668,17 @@ mTethering.interfaceAdded(TEST_BT_IFNAME); mLooper.dispatchAll(); - mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false); - mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true); - final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); - mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult); - mLooper.dispatchAll(); - tetherResult.assertHasResult(); + if (isAtLeastT()) { + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + } else { + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false); + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true); + final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); + mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult); + mLooper.dispatchAll(); + tetherResult.assertHasResult(); + } verifyNetdCommandForBtSetup(); @@ -2632,6 +2695,10 @@ } private void verifyNetdCommandForBtSetup() throws Exception { + if (isAtLeastT()) { + verify(mNetd).interfaceSetCfg(argThat(cfg -> TEST_BT_IFNAME.equals(cfg.ifName) + && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP))); + } verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME); verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), @@ -2644,19 +2711,30 @@ reset(mNetd); } + private boolean assertContainsFlag(String[] flags, String match) { + for (String flag : flags) { + if (flag.equals(match)) return true; + } + return false; + } + private void verifyNetdCommandForBtTearDown() throws Exception { verify(mNetd).tetherApplyDnsInterfaces(); verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME); verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); - verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only + // happen after T. Before T, the interface configuration control in bluetooth side. + verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg( + any(InterfaceConfigurationParcel.class)); verify(mNetd).tetherStop(); verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME); + reset(mNetd); } // If bindToPanService is true, this function would return ServiceListener which could notify // PanService is connected or disconnected. private ServiceListener verifySetBluetoothTethering(final boolean enable, - final boolean bindToPanService) { + final boolean bindToPanService) throws Exception { ServiceListener listener = null; verify(mBluetoothAdapter).isEnabled(); if (bindToPanService) { @@ -2671,7 +2749,19 @@ verify(mBluetoothAdapter, never()).getProfileProxy(eq(mServiceContext), any(), anyInt()); } - verify(mBluetoothPan).setBluetoothTethering(enable); + + if (isAtLeastT()) { + if (enable) { + final ArgumentCaptor<TetheredInterfaceCallbackShim> callbackCaptor = + ArgumentCaptor.forClass(TetheredInterfaceCallbackShim.class); + verify(mBluetoothPanShim).requestTetheredInterface(any(), callbackCaptor.capture()); + mTetheredInterfaceCallbackShim = callbackCaptor.getValue(); + } else { + verify(mTetheredInterfaceRequestShim).release(); + } + } else { + verify(mBluetoothPan).setBluetoothTethering(enable); + } verify(mBluetoothPan).isTetheringOn(); verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan); reset(mBluetoothAdapter, mBluetoothPan);
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp index d015ef6..6718402 100644 --- a/bpf_progs/Android.bp +++ b/bpf_progs/Android.bp
@@ -42,10 +42,13 @@ // 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", + "//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", ], @@ -55,6 +58,16 @@ // bpf kernel programs // bpf { + name: "dscp_policy.o", + srcs: ["dscp_policy.c"], + cflags: [ + "-Wall", + "-Werror", + ], + sub_dir: "net_shared", +} + +bpf { name: "offload.o", srcs: ["offload.c"], cflags: [ @@ -71,3 +84,29 @@ "-Werror", ], } + +bpf { + name: "clatd.o_mainline", + srcs: ["clatd.c"], + cflags: [ + "-Wall", + "-Werror", + ], + include_dirs: [ + "frameworks/libs/net/common/netd/libnetdutils/include", + ], + sub_dir: "net_shared", +} + +bpf { + name: "netd.o_mainline", + srcs: ["netd.c"], + cflags: [ + "-Wall", + "-Werror", + ], + include_dirs: [ + "frameworks/libs/net/common/netd/libnetdutils/include", + ], + sub_dir: "net_shared", +}
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h index 8577d9d..2ddc7b8 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. @@ -131,7 +130,8 @@ STANDBY_MATCH = (1 << 3), POWERSAVE_MATCH = (1 << 4), RESTRICTED_MATCH = (1 << 5), - IIF_MATCH = (1 << 6), + LOW_POWER_STANDBY_MATCH = (1 << 6), + IIF_MATCH = (1 << 7), }; enum BpfPermissionMatch {
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c new file mode 100644 index 0000000..dc646c3 --- /dev/null +++ b/bpf_progs/clatd.c
@@ -0,0 +1,322 @@ +/* + * Copyright (C) 2019 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 <linux/bpf.h> +#include <linux/if.h> +#include <linux/if_ether.h> +#include <linux/in.h> +#include <linux/in6.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/pkt_cls.h> +#include <linux/swab.h> +#include <stdbool.h> +#include <stdint.h> + +// bionic kernel uapi linux/udp.h header is munged... +#define __kernel_udphdr udphdr +#include <linux/udp.h> + +#include "bpf_helpers.h" +#include "bpf_net_helpers.h" +#include "bpf_shared.h" + +// From kernel:include/net/ip.h +#define IP_DF 0x4000 // Flag: "Don't Fragment" + +DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM) + +static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) { + const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0; + void* data = (void*)(long)skb->data; + const void* data_end = (void*)(long)skb->data_end; + const struct ethhdr* const eth = is_ethernet ? data : NULL; // used iff is_ethernet + const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data; + + // Require ethernet dst mac address to be our unicast address. + if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE; + + // Must be meta-ethernet IPv6 frame + if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE; + + // Must have (ethernet and) ipv6 header + if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_PIPE; + + // Ethertype - if present - must be IPv6 + if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_PIPE; + + // IP version must be 6 + if (ip6->version != 6) return TC_ACT_PIPE; + + // Maximum IPv6 payload length that can be translated to IPv4 + if (ntohs(ip6->payload_len) > 0xFFFF - sizeof(struct iphdr)) return TC_ACT_PIPE; + + switch (ip6->nexthdr) { + case IPPROTO_TCP: // For TCP & UDP the checksum neutrality of the chosen IPv6 + case IPPROTO_UDP: // address means there is no need to update their checksums. + case IPPROTO_GRE: // We do not need to bother looking at GRE/ESP headers, + case IPPROTO_ESP: // since there is never a checksum to update. + break; + + default: // do not know how to handle anything else + return TC_ACT_PIPE; + } + + ClatIngress6Key k = { + .iif = skb->ifindex, + .pfx96.in6_u.u6_addr32 = + { + ip6->saddr.in6_u.u6_addr32[0], + ip6->saddr.in6_u.u6_addr32[1], + ip6->saddr.in6_u.u6_addr32[2], + }, + .local6 = ip6->daddr, + }; + + ClatIngress6Value* v = bpf_clat_ingress6_map_lookup_elem(&k); + + if (!v) return TC_ACT_PIPE; + + struct ethhdr eth2; // used iff is_ethernet + if (is_ethernet) { + eth2 = *eth; // Copy over the ethernet header (src/dst mac) + eth2.h_proto = htons(ETH_P_IP); // But replace the ethertype + } + + struct iphdr ip = { + .version = 4, // u4 + .ihl = sizeof(struct iphdr) / sizeof(__u32), // u4 + .tos = (ip6->priority << 4) + (ip6->flow_lbl[0] >> 4), // u8 + .tot_len = htons(ntohs(ip6->payload_len) + sizeof(struct iphdr)), // u16 + .id = 0, // u16 + .frag_off = htons(IP_DF), // u16 + .ttl = ip6->hop_limit, // u8 + .protocol = ip6->nexthdr, // u8 + .check = 0, // u16 + .saddr = ip6->saddr.in6_u.u6_addr32[3], // u32 + .daddr = v->local4.s_addr, // u32 + }; + + // Calculate the IPv4 one's complement checksum of the IPv4 header. + __wsum sum4 = 0; + for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) { + sum4 += ((__u16*)&ip)[i]; + } + // Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4 + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16 + ip.check = (__u16)~sum4; // sum4 cannot be zero, so this is never 0xFFFF + + // Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header. + __wsum sum6 = 0; + // We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits) + for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) { + sum6 += ~((__u16*)ip6)[i]; // note the bitwise negation + } + + // Note that there is no L4 checksum update: we are relying on the checksum neutrality + // of the ipv6 address chosen by netd's ClatdController. + + // Packet mutations begin - point of no return, but if this first modification fails + // the packet is probably still pristine, so let clatd handle it. + if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) return TC_ACT_PIPE; + + // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet. + // + // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload, + // thus we need to subtract out the ipv6 header's sum, and add in the ipv4 header's sum. + // However, by construction of ip.check above the checksum of an ipv4 header is zero. + // Thus we only need to subtract the ipv6 header's sum, which is the same as adding + // in the sum of the bitwise negation of the ipv6 header. + // + // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error + // (-ENOTSUPP) if it isn't. So we just ignore the return code. + // + // if (skb->ip_summed == CHECKSUM_COMPLETE) + // return (skb->csum = csum_add(skb->csum, csum)); + // else + // return -ENOTSUPP; + bpf_csum_update(skb, sum6); + + // bpf_skb_change_proto() invalidates all pointers - reload them. + data = (void*)(long)skb->data; + data_end = (void*)(long)skb->data_end; + + // I cannot think of any valid way for this error condition to trigger, however I do + // believe the explicit check is required to keep the in kernel ebpf verifier happy. + if (data + l2_header_size + sizeof(struct iphdr) > data_end) return TC_ACT_SHOT; + + if (is_ethernet) { + struct ethhdr* new_eth = data; + + // Copy over the updated ethernet header + *new_eth = eth2; + + // Copy over the new ipv4 header. + *(struct iphdr*)(new_eth + 1) = ip; + } else { + // Copy over the new ipv4 header without an ethernet header. + *(struct iphdr*)data = ip; + } + + // Redirect, possibly back to same interface, so tcpdump sees packet twice. + if (v->oif) return bpf_redirect(v->oif, BPF_F_INGRESS); + + // Just let it through, tcpdump will not see IPv4 packet. + return TC_ACT_PIPE; +} + +DEFINE_BPF_PROG("schedcls/ingress6/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_ether) +(struct __sk_buff* skb) { + return nat64(skb, true); +} + +DEFINE_BPF_PROG("schedcls/ingress6/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_rawip) +(struct __sk_buff* skb) { + return nat64(skb, false); +} + +DEFINE_BPF_MAP_GRW(clat_egress4_map, HASH, ClatEgress4Key, ClatEgress4Value, 16, AID_SYSTEM) + +DEFINE_BPF_PROG("schedcls/egress4/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_ether) +(struct __sk_buff* skb) { + return TC_ACT_PIPE; +} + +DEFINE_BPF_PROG("schedcls/egress4/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_rawip) +(struct __sk_buff* skb) { + void* data = (void*)(long)skb->data; + const void* data_end = (void*)(long)skb->data_end; + const struct iphdr* const ip4 = data; + + // Must be meta-ethernet IPv4 frame + if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE; + + // Must have ipv4 header + if (data + sizeof(*ip4) > data_end) return TC_ACT_PIPE; + + // IP version must be 4 + if (ip4->version != 4) return TC_ACT_PIPE; + + // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header + if (ip4->ihl != 5) return TC_ACT_PIPE; + + // Calculate the IPv4 one's complement checksum of the IPv4 header. + __wsum sum4 = 0; + for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) { + sum4 += ((__u16*)ip4)[i]; + } + // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4 + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16 + // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF + if (sum4 != 0xFFFF) return TC_ACT_PIPE; + + // Minimum IPv4 total length is the size of the header + if (ntohs(ip4->tot_len) < sizeof(*ip4)) return TC_ACT_PIPE; + + // We are incapable of dealing with IPv4 fragments + if (ip4->frag_off & ~htons(IP_DF)) return TC_ACT_PIPE; + + switch (ip4->protocol) { + case IPPROTO_TCP: // For TCP & UDP the checksum neutrality of the chosen IPv6 + case IPPROTO_GRE: // address means there is no need to update their checksums. + case IPPROTO_ESP: // We do not need to bother looking at GRE/ESP headers, + break; // since there is never a checksum to update. + + case IPPROTO_UDP: // See above comment, but must also have UDP header... + if (data + sizeof(*ip4) + sizeof(struct udphdr) > data_end) return TC_ACT_PIPE; + const struct udphdr* uh = (const struct udphdr*)(ip4 + 1); + // If IPv4/UDP checksum is 0 then fallback to clatd so it can calculate the + // checksum. Otherwise the network or more likely the NAT64 gateway might + // drop the packet because in most cases IPv6/UDP packets with a zero checksum + // are invalid. See RFC 6935. TODO: calculate checksum via bpf_csum_diff() + if (!uh->check) return TC_ACT_PIPE; + break; + + default: // do not know how to handle anything else + return TC_ACT_PIPE; + } + + ClatEgress4Key k = { + .iif = skb->ifindex, + .local4.s_addr = ip4->saddr, + }; + + ClatEgress4Value* v = bpf_clat_egress4_map_lookup_elem(&k); + + if (!v) return TC_ACT_PIPE; + + // Translating without redirecting doesn't make sense. + if (!v->oif) return TC_ACT_PIPE; + + // This implementation is currently limited to rawip. + if (v->oifIsEthernet) return TC_ACT_PIPE; + + struct ipv6hdr ip6 = { + .version = 6, // __u8:4 + .priority = ip4->tos >> 4, // __u8:4 + .flow_lbl = {(ip4->tos & 0xF) << 4, 0, 0}, // __u8[3] + .payload_len = htons(ntohs(ip4->tot_len) - 20), // __be16 + .nexthdr = ip4->protocol, // __u8 + .hop_limit = ip4->ttl, // __u8 + .saddr = v->local6, // struct in6_addr + .daddr = v->pfx96, // struct in6_addr + }; + ip6.daddr.in6_u.u6_addr32[3] = ip4->daddr; + + // Calculate the IPv6 16-bit one's complement checksum of the IPv6 header. + __wsum sum6 = 0; + // We'll end up with a non-zero sum due to ip6.version == 6 + for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) { + sum6 += ((__u16*)&ip6)[i]; + } + + // Note that there is no L4 checksum update: we are relying on the checksum neutrality + // of the ipv6 address chosen by netd's ClatdController. + + // Packet mutations begin - point of no return, but if this first modification fails + // the packet is probably still pristine, so let clatd handle it. + if (bpf_skb_change_proto(skb, htons(ETH_P_IPV6), 0)) return TC_ACT_PIPE; + + // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet. + // + // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload, + // thus we need to subtract out the ipv4 header's sum, and add in the ipv6 header's sum. + // However, we've already verified the ipv4 checksum is correct and thus 0. + // Thus we only need to add the ipv6 header's sum. + // + // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error + // (-ENOTSUPP) if it isn't. So we just ignore the return code (see above for more details). + bpf_csum_update(skb, sum6); + + // bpf_skb_change_proto() invalidates all pointers - reload them. + data = (void*)(long)skb->data; + data_end = (void*)(long)skb->data_end; + + // I cannot think of any valid way for this error condition to trigger, however I do + // believe the explicit check is required to keep the in kernel ebpf verifier happy. + if (data + sizeof(ip6) > data_end) return TC_ACT_SHOT; + + // Copy over the new ipv6 header without an ethernet header. + *(struct ipv6hdr*)data = ip6; + + // Redirect to non v4-* interface. Tcpdump only sees packet after this redirect. + return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */); +} + +LICENSE("Apache 2.0"); +CRITICAL("netd");
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c new file mode 100644 index 0000000..9989e6b --- /dev/null +++ b/bpf_progs/dscp_policy.c
@@ -0,0 +1,280 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <linux/types.h> +#include <linux/bpf.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/if_ether.h> +#include <linux/pkt_cls.h> +#include <linux/tcp.h> +#include <stdint.h> +#include <netinet/in.h> +#include <netinet/udp.h> +#include <string.h> + +#include "bpf_helpers.h" + +#define MAX_POLICIES 16 +#define MAP_A 1 +#define MAP_B 2 + +#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.") + +// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h +// should they be moved to common location? +static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = + (void*)BPF_FUNC_get_socket_cookie; +static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len, + __u64 flags) = (void*)BPF_FUNC_skb_store_bytes; +static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to, + __u64 flags) = (void*)BPF_FUNC_l3_csum_replace; + +typedef struct { + // Add family here to match __sk_buff ? + struct in_addr srcIp; + struct in_addr dstIp; + __be16 srcPort; + __be16 dstPort; + uint8_t proto; + uint8_t dscpVal; + uint8_t pad[2]; +} Ipv4RuleEntry; +STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2); // 16, 4 for in_addr + +#define SRC_IP_MASK 1 +#define DST_IP_MASK 2 +#define SRC_PORT_MASK 4 +#define DST_PORT_MASK 8 +#define PROTO_MASK 16 + +typedef struct { + struct in6_addr srcIp; + struct in6_addr dstIp; + __be16 srcPort; + __be16 dstPortStart; + __be16 dstPortEnd; + uint8_t proto; + uint8_t dscpVal; + uint8_t mask; + uint8_t pad[3]; +} Ipv4Policy; +STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44 + +typedef struct { + struct in6_addr srcIp; + struct in6_addr dstIp; + __be16 srcPort; + __be16 dstPortStart; + __be16 dstPortEnd; + uint8_t proto; + uint8_t dscpVal; + uint8_t mask; + // should we override this struct to include the param bitmask for linear search? + // For mapping socket to policies, all the params should match exactly since we can + // pull any missing from the sock itself. +} Ipv6RuleEntry; +STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44 + +// TODO: move to using 1 map. Map v4 address to 0xffff::v4 +DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES, + AID_SYSTEM) +DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES, + AID_SYSTEM) +DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES, + AID_SYSTEM) +DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES, + AID_SYSTEM) +DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM) + +DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES, + AID_SYSTEM) +DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES, + AID_SYSTEM) + +DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM, + schedcls_set_dscp, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + int one = 0; + uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one); + + // use this with HASH map so map lookup only happens once policies have been added? + if (!selectedMap) { + return TC_ACT_PIPE; + } + + // used for map lookup + uint64_t cookie = bpf_get_socket_cookie(skb); + + // Do we need separate maps for ipv4/ipv6 + if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons() + Ipv4RuleEntry* v4Policy; + if (*selectedMap == MAP_A) { + v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie); + } else { + v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie); + } + + // How to use bitmask here to compare params efficiently? + // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader? + + void* data = (void*)(long)skb->data; + const void* data_end = (void*)(long)skb->data_end; + const struct iphdr* const iph = data; + + // Must have ipv4 header + if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE; + + // IP version must be 4 + if (iph->version != 4) return TC_ACT_PIPE; + + // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header + if (iph->ihl != 5) return TC_ACT_PIPE; + + if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE; + + struct udphdr *udp; + udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr) + + if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE; + + // Source/destination port in udphdr are stored in be16, need to convert to le16. + // This can be done via ntohs or htons. Is there a more preferred way? + // Cached policy was found. + if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr && + iph->daddr == v4Policy->dstIp.s_addr && + ntohs(udp->source) == v4Policy->srcPort && + ntohs(udp->dest) == v4Policy->dstPort && + iph->protocol == v4Policy->proto) { + // set dscpVal in packet. Least sig 2 bits of TOS + // reference ipv4_change_dsfield() + + // TODO: fix checksum... + int ecn = iph->tos & 3; + uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn; + int oldDscpVal = iph->tos >> 2; + bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t)); + bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0); + return TC_ACT_PIPE; + } + + // linear scan ipv4_dscp_policies_map, stored socket params do not match actual + int bestScore = -1; + uint32_t bestMatch = 0; + + for (register uint64_t i = 0; i < MAX_POLICIES; i++) { + int score = 0; + uint8_t tempMask = 0; + // Using a uint62 in for loop prevents infinite loop during BPF load, + // but the key is uint32, so convert back. + uint32_t key = i; + Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key); + + // if mask is 0 continue, key does not have corresponding policy value + if (policy && policy->mask != 0) { + if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK && + iph->saddr == policy->srcIp.s6_addr32[3]) { + score++; + tempMask |= SRC_IP_MASK; + } + if ((policy->mask & DST_IP_MASK) == DST_IP_MASK && + iph->daddr == policy->dstIp.s6_addr32[3]) { + score++; + tempMask |= DST_IP_MASK; + } + if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK && + ntohs(udp->source) == htons(policy->srcPort)) { + score++; + tempMask |= SRC_PORT_MASK; + } + if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK && + ntohs(udp->dest) >= htons(policy->dstPortStart) && + ntohs(udp->dest) <= htons(policy->dstPortEnd)) { + score++; + tempMask |= DST_PORT_MASK; + } + if ((policy->mask & PROTO_MASK) == PROTO_MASK && + iph->protocol == policy->proto) { + score++; + tempMask |= PROTO_MASK; + } + + if (score > bestScore && tempMask == policy->mask) { + bestMatch = i; + bestScore = score; + } + } + } + + uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value? + uint8_t curDscp = iph->tos & 252; + if (bestScore > 0) { + Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch); + if (policy) { + // TODO: if DSCP value is already set ignore? + // TODO: update checksum, for testing increment counter... + int ecn = iph->tos & 3; + newDscpVal = (policy->dscpVal << 2) + ecn; + } + } + + Ipv4RuleEntry value = { + .srcIp.s_addr = iph->saddr, + .dstIp.s_addr = iph->daddr, + .srcPort = udp->source, + .dstPort = udp->dest, + .proto = iph->protocol, + .dscpVal = newDscpVal, + }; + + if (!cookie) + return TC_ACT_PIPE; + + // Update map + if (*selectedMap == MAP_A) { + bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY); + } else { + bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY); + } + + // Need to store bytes after updating map or program will not load. + if (newDscpVal != curDscp) { + // 1 is the offset (Version/Header length) + int oldDscpVal = iph->tos >> 2; + bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t)); + bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0); + } + + } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons() + Ipv6RuleEntry* v6Policy; + if (*selectedMap == MAP_A) { + v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie); + } else { + v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie); + } + + if (!v6Policy) + return TC_ACT_PIPE; + + // TODO: Add code to process IPv6 packet. + } + + // Always return TC_ACT_PIPE + return TC_ACT_PIPE; +} + +LICENSE("Apache 2.0"); +CRITICAL("Connectivity");
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c new file mode 100644 index 0000000..c1a74e7 --- /dev/null +++ b/bpf_progs/netd.c
@@ -0,0 +1,396 @@ +/* + * 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 <bpf_helpers.h> +#include <linux/bpf.h> +#include <linux/if.h> +#include <linux/if_ether.h> +#include <linux/if_packet.h> +#include <linux/in.h> +#include <linux/in6.h> +#include <linux/ip.h> +#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" +#include "bpf_shared.h" + +// This is defined for cgroup bpf filter only. +#define BPF_DROP_UNLESS_DNS 2 +#define BPF_PASS 1 +#define BPF_DROP 0 + +// This is used for xt_bpf program only. +#define BPF_NOMATCH 0 +#define BPF_MATCH 1 + +#define BPF_EGRESS 0 +#define BPF_INGRESS 1 + +#define IP_PROTO_OFF offsetof(struct iphdr, protocol) +#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr) +#define IPPROTO_IHL_OFF 0 +#define TCP_FLAG_OFF 13 +#define RST_OFFSET 2 + +DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE, + AID_NET_BW_ACCT) +DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT) + +/* never actually used from ebpf */ +DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE, + AID_NET_BW_ACCT) + +static __always_inline int is_system_uid(uint32_t uid) { + return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID); +} + +/* + * Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP, + * and that TCP is using the Linux default settings with TCP timestamp option enabled + * which uses 12 TCP option bytes per frame. + * + * These are not unreasonable assumptions: + * + * The internet does not really support MTUs greater than 1500, so most TCP traffic will + * be at that MTU, or slightly below it (worst case our upwards adjustment is too small). + * + * The chance our traffic isn't IP at all is basically zero, so the IP overhead correction + * is bound to be needed. + * + * Furthermore, the likelyhood that we're having to deal with GSO (ie. > MTU) packets that + * are not IP/TCP is pretty small (few other things are supported by Linux) and worse case + * our extra overhead will be slightly off, but probably still better than assuming none. + * + * Most servers are also Linux and thus support/default to using TCP timestamp option + * (and indeed TCP timestamp option comes from RFC 1323 titled "TCP Extensions for High + * Performance" which also defined TCP window scaling and are thus absolutely ancient...). + * + * All together this should be more correct than if we simply ignored GSO frames + * (ie. counted them as single packets with no extra overhead) + * + * Especially since the number of packets is important for any future clat offload correction. + * (which adjusts upward by 20 bytes per packet to account for ipv4 -> ipv6 header conversion) + */ +#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey) \ + static __always_inline inline void update_##the_stats_map(struct __sk_buff* skb, \ + int direction, TypeOfKey* key) { \ + StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \ + if (!value) { \ + StatsValue newValue = {}; \ + bpf_##the_stats_map##_update_elem(key, &newValue, BPF_NOEXIST); \ + value = bpf_##the_stats_map##_lookup_elem(key); \ + } \ + if (value) { \ + const int mtu = 1500; \ + uint64_t packets = 1; \ + uint64_t bytes = skb->len; \ + if (bytes > mtu) { \ + bool is_ipv6 = (skb->protocol == htons(ETH_P_IPV6)); \ + int ip_overhead = (is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr)); \ + int tcp_overhead = ip_overhead + sizeof(struct tcphdr) + 12; \ + int mss = mtu - tcp_overhead; \ + uint64_t payload = bytes - tcp_overhead; \ + packets = (payload + mss - 1) / mss; \ + bytes = tcp_overhead * packets + payload; \ + } \ + if (direction == BPF_EGRESS) { \ + __sync_fetch_and_add(&value->txPackets, packets); \ + __sync_fetch_and_add(&value->txBytes, bytes); \ + } else if (direction == BPF_INGRESS) { \ + __sync_fetch_and_add(&value->rxPackets, packets); \ + __sync_fetch_and_add(&value->rxBytes, bytes); \ + } \ + } \ + } + +DEFINE_UPDATE_STATS(app_uid_stats_map, uint32_t) +DEFINE_UPDATE_STATS(iface_stats_map, uint32_t) +DEFINE_UPDATE_STATS(stats_map_A, StatsKey) +DEFINE_UPDATE_STATS(stats_map_B, StatsKey) + +static inline bool skip_owner_match(struct __sk_buff* skb) { + int offset = -1; + int ret = 0; + if (skb->protocol == htons(ETH_P_IP)) { + offset = IP_PROTO_OFF; + uint8_t proto, ihl; + uint8_t flag; + ret = bpf_skb_load_bytes(skb, offset, &proto, 1); + if (!ret) { + if (proto == IPPROTO_ESP) { + return true; + } else if (proto == IPPROTO_TCP) { + ret = bpf_skb_load_bytes(skb, IPPROTO_IHL_OFF, &ihl, 1); + ihl = ihl & 0x0F; + ret = bpf_skb_load_bytes(skb, ihl * 4 + TCP_FLAG_OFF, &flag, 1); + if (ret == 0 && (flag >> RST_OFFSET & 1)) { + return true; + } + } + } + } else if (skb->protocol == htons(ETH_P_IPV6)) { + offset = IPV6_PROTO_OFF; + uint8_t proto; + ret = bpf_skb_load_bytes(skb, offset, &proto, 1); + if (!ret) { + if (proto == IPPROTO_ESP) { + return true; + } else if (proto == IPPROTO_TCP) { + uint8_t flag; + ret = bpf_skb_load_bytes(skb, sizeof(struct ipv6hdr) + TCP_FLAG_OFF, &flag, 1); + if (ret == 0 && (flag >> RST_OFFSET & 1)) { + return true; + } + } + } + } + return false; +} + +static __always_inline BpfConfig getConfig(uint32_t configKey) { + uint32_t mapSettingKey = configKey; + BpfConfig* config = bpf_configuration_map_lookup_elem(&mapSettingKey); + if (!config) { + // Couldn't read configuration entry. Assume everything is disabled. + return DEFAULT_CONFIG; + } + return *config; +} + +static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) { + if (skip_owner_match(skb)) return BPF_PASS; + + if (is_system_uid(uid)) return BPF_PASS; + + BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY); + + UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid); + uint8_t uidRules = uidEntry ? uidEntry->rule : 0; + uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0; + + if (enabledRules) { + if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) { + return BPF_DROP; + } + if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) { + return BPF_DROP; + } + if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) { + return BPF_DROP; + } + if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) { + return BPF_DROP; + } + if ((enabledRules & LOW_POWER_STANDBY_MATCH) && !(uidRules & LOW_POWER_STANDBY_MATCH)) { + return BPF_DROP; + } + } + if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) { + // Drops packets not coming from lo nor the allowlisted interface + if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) { + return BPF_DROP_UNLESS_DNS; + } + } + return BPF_PASS; +} + +static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction, + StatsKey* key, uint8_t selectedMap) { + if (selectedMap == SELECT_MAP_A) { + update_stats_map_A(skb, direction, key); + } else if (selectedMap == SELECT_MAP_B) { + update_stats_map_B(skb, direction, key); + } +} + +static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, int direction) { + uint32_t sock_uid = bpf_get_socket_uid(skb); + uint64_t cookie = bpf_get_socket_cookie(skb); + UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie); + uint32_t uid, tag; + if (utag) { + uid = utag->uid; + tag = utag->tag; + } else { + uid = sock_uid; + tag = 0; + } + + // Always allow and never count clat traffic. Only the IPv4 traffic on the stacked + // interface is accounted for and subject to usage restrictions. + // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat. + if (sock_uid == AID_CLAT || uid == AID_CLAT) { + return BPF_PASS; + } + + int match = bpf_owner_match(skb, sock_uid, direction); + if ((direction == BPF_EGRESS) && (match == BPF_DROP)) { + // If an outbound packet is going to be dropped, we do not count that + // traffic. + return match; + } + +// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details. +// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h +// and TrafficStatsConstants.java +#define TAG_SYSTEM_DNS 0xFFFFFF82 + if (tag == TAG_SYSTEM_DNS && uid == AID_DNS) { + uid = sock_uid; + if (match == BPF_DROP_UNLESS_DNS) match = BPF_PASS; + } else { + if (match == BPF_DROP_UNLESS_DNS) match = BPF_DROP; + } + + StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex}; + + uint8_t* counterSet = bpf_uid_counterset_map_lookup_elem(&uid); + if (counterSet) key.counterSet = (uint32_t)*counterSet; + + uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY; + uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey); + + // Use asm("%0 &= 1" : "+r"(match)) before return match, + // to help kernel's bpf verifier, so that it can be 100% certain + // that the returned value is always BPF_NOMATCH(0) or BPF_MATCH(1). + if (!selectedMap) { + asm("%0 &= 1" : "+r"(match)); + return match; + } + + if (key.tag) { + update_stats_with_config(skb, direction, &key, *selectedMap); + key.tag = 0; + } + + update_stats_with_config(skb, direction, &key, *selectedMap); + update_app_uid_stats_map(skb, direction, &uid); + asm("%0 &= 1" : "+r"(match)); + return match; +} + +DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_ingress) +(struct __sk_buff* skb) { + return bpf_traffic_account(skb, BPF_INGRESS); +} + +DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_egress) +(struct __sk_buff* skb) { + return bpf_traffic_account(skb, BPF_EGRESS); +} + +DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog) +(struct __sk_buff* skb) { + // Clat daemon does not generate new traffic, all its traffic is accounted for already + // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead, + // but that can be corrected for later when merging v4-foo stats into interface foo's). + // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat. + uint32_t sock_uid = bpf_get_socket_uid(skb); + if (sock_uid == AID_CLAT) return BPF_NOMATCH; + if (sock_uid == AID_SYSTEM) { + uint64_t cookie = bpf_get_socket_cookie(skb); + UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie); + if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH; + } + + uint32_t key = skb->ifindex; + update_iface_stats_map(skb, BPF_EGRESS, &key); + return BPF_MATCH; +} + +DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog) +(struct __sk_buff* skb) { + // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule + // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain). + // It will be accounted for on the v4-* clat interface instead. + // Keep that in mind when moving this out of iptables xt_bpf and into tc ingress (or xdp). + + uint32_t key = skb->ifindex; + update_iface_stats_map(skb, BPF_INGRESS, &key); + return BPF_MATCH; +} + +DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog) +(struct __sk_buff* skb) { + // Account for ingress traffic before tc drops it. + uint32_t key = skb->ifindex; + update_iface_stats_map(skb, BPF_INGRESS, &key); + return TC_ACT_UNSPEC; +} + +DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog) +(struct __sk_buff* skb) { + uint32_t sock_uid = bpf_get_socket_uid(skb); + if (is_system_uid(sock_uid)) return BPF_MATCH; + + // 65534 is the overflow 'nobody' uid, usually this being returned means + // that skb->sk is NULL during RX (early decap socket lookup failure), + // which commonly happens for incoming packets to an unconnected udp socket. + // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL + if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb)) + return BPF_MATCH; + + UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid); + if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH; + return BPF_NOMATCH; +} + +DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog) +(struct __sk_buff* skb) { + uint32_t sock_uid = bpf_get_socket_uid(skb); + UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid); + if (denylistMatch) return denylistMatch->rule & PENALTY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH; + return BPF_NOMATCH; +} + +DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create, + KVER(4, 14, 0)) +(struct bpf_sock* sk) { + uint64_t gid_uid = bpf_get_current_uid_gid(); + /* + * A given app is guaranteed to have the same app ID in all the profiles in + * which it is installed, and install permission is granted to app for all + * user at install time so we only check the appId part of a request uid at + * run time. See UserHandle#isSameApp for detail. + */ + uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE; + uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId); + if (!permissions) { + // UID not in map. Default to just INTERNET permission. + return 1; + } + + // A return value of 1 means allow, everything else means deny. + return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET; +} + +LICENSE("Apache 2.0"); +CRITICAL("netd");
diff --git a/buildstubs-t/Android.bp b/buildstubs-t/Android.bp new file mode 100644 index 0000000..dfca6b3 --- /dev/null +++ b/buildstubs-t/Android.bp
@@ -0,0 +1,49 @@ +// +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// Placeholder empty filegroups to avoid merge conflicts on build rules +// on a branch that does not have the filegroups + +filegroup { + name: "framework-connectivity-tiramisu-updatable-sources", + srcs: [], +} + +filegroup { + name: "services.connectivity-tiramisu-updatable-sources", + srcs: ["stubs-src/**/*.java"], +} + +// Empty replacement for framework-connectivity-tiramisu.impl and stubs, +// as framework-connectivity is disabled in the branch +java_library { + name: "framework-connectivity-tiramisu.impl", + min_sdk_version: "Tiramisu", + sdk_version: "module_current", + srcs: [], +} + +java_library { + name: "framework-connectivity-tiramisu.stubs.module_lib", + min_sdk_version: "Tiramisu", + sdk_version: "module_current", + srcs: [], +}
diff --git a/buildstubs-t/stubs-src/com/android/server/NsdService.java b/buildstubs-t/stubs-src/com/android/server/NsdService.java new file mode 100644 index 0000000..8e32ffd --- /dev/null +++ b/buildstubs-t/stubs-src/com/android/server/NsdService.java
@@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.content.Context; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +import java.io.FileDescriptor; + +/** + * Fake NsdService class for sc-mainline-prod, + * to allow building the T service-connectivity before sources + * are moved to the branch + */ +public final class NsdService implements IBinder { + /** Create instance */ + public static NsdService create(Context ctx) throws InterruptedException { + throw new RuntimeException("This is a stub class"); + } + + @Override + public String getInterfaceDescriptor() throws RemoteException { + return null; + } + + @Override + public boolean pingBinder() { + return false; + } + + @Override + public boolean isBinderAlive() { + return false; + } + + @Override + public IInterface queryLocalInterface(String descriptor) { + return null; + } + + @Override + public void dump(FileDescriptor fd, String[] args) throws RemoteException {} + + @Override + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException {} + + @Override + public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + return false; + } + + @Override + public void linkToDeath(DeathRecipient recipient, int flags) throws RemoteException {} + + @Override + public boolean unlinkToDeath(DeathRecipient recipient, int flags) { + return false; + } +}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp new file mode 100644 index 0000000..a08c06a --- /dev/null +++ b/framework-t/Android.bp
@@ -0,0 +1,102 @@ +// +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "enable-framework-connectivity-t-targets", + enabled: false, +} +// The above defaults can be used to disable framework-connectivity t +// targets while minimizing merge conflicts in the build rules. + +// SDK library for connectivity bootclasspath classes that were part of the non-updatable API before +// T, and were moved to the module in T. Other bootclasspath classes in connectivity should go to +// framework-connectivity. +java_sdk_library { + name: "framework-connectivity-tiramisu", + sdk_version: "module_current", + min_sdk_version: "Tiramisu", + defaults: [ + "framework-module-defaults", + "enable-framework-connectivity-t-targets", + ], + srcs: [ + ":framework-connectivity-tiramisu-updatable-sources", + ":framework-nearby-java-sources", + ], + // Do not add static_libs to this library: put them in framework-connectivity instead. + // The jarjar rules are only so that references to jarjared utils in + // framework-connectivity-pre-jarjar match at runtime. + jarjar_rules: ":connectivity-jarjar-rules", + stub_only_libs: [ + // Use prebuilt framework-connectivity stubs to avoid circular dependencies + "sdk_module-lib_current_framework-connectivity", + ], + libs: [ + "unsupportedappusage", + "app-compat-annotations", + "sdk_module-lib_current_framework-connectivity", + ], + impl_only_libs: [ + // The build system will use framework-bluetooth module_current stubs, because + // of sdk_version: "module_current" above. + "framework-bluetooth", + // Compile against the entire implementation of framework-connectivity, + // including hidden methods. This is safe because if framework-connectivity-t is + // on the bootclasspath (i.e., T), then framework-connectivity is also on the + // bootclasspath (because it shipped in S). + // + // This compiles against the pre-jarjar target so that this code can use + // non-jarjard names of widely-used packages such as com.android.net.module.util. + "framework-connectivity-pre-jarjar", + ], + permitted_packages: [ + "android.net", + "android.net.nsd", + "android.nearby", + "com.android.connectivity", + "com.android.nearby", + ], + apex_available: [ + "com.android.tethering", + ], + impl_library_visibility: [ + "//packages/modules/Connectivity/Tethering/apex", + // In preparation for future move + "//packages/modules/Connectivity/apex", + "//packages/modules/Connectivity/service-t", + "//packages/modules/Nearby/service", + "//frameworks/base", + + // Tests using hidden APIs + "//cts/tests/netlegacy22.api", + "//cts/tests/tests/app.usage", // NetworkUsageStatsTest + "//external/sl4a:__subpackages__", + "//frameworks/libs/net/common/testutils", + "//frameworks/libs/net/common/tests:__subpackages__", + "//frameworks/opt/telephony/tests/telephonytests", + "//packages/modules/CaptivePortalLogin/tests", + "//packages/modules/Connectivity/Tethering/tests:__subpackages__", + "//packages/modules/Connectivity/tests:__subpackages__", + "//packages/modules/NetworkStack/tests:__subpackages__", + "//packages/modules/Nearby/tests:__subpackages__", + "//packages/modules/Wifi/service/tests/wifitests", + ], +}
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt new file mode 100644 index 0000000..7977cc5 --- /dev/null +++ b/framework-t/api/current.txt
@@ -0,0 +1,64 @@ +// Signature format: 2.0 +package android.net.nsd { + + public final class NsdManager { + method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener); + method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull android.net.nsd.NsdManager.DiscoveryListener); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull android.net.nsd.NsdManager.DiscoveryListener); + method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener); + method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener); + method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener); + method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener); + field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED"; + field public static final String EXTRA_NSD_STATE = "nsd_state"; + field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3 + field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0 + field public static final int FAILURE_MAX_LIMIT = 4; // 0x4 + field public static final int NSD_STATE_DISABLED = 1; // 0x1 + field public static final int NSD_STATE_ENABLED = 2; // 0x2 + field public static final int PROTOCOL_DNS_SD = 1; // 0x1 + } + + public static interface NsdManager.DiscoveryListener { + method public void onDiscoveryStarted(String); + method public void onDiscoveryStopped(String); + method public void onServiceFound(android.net.nsd.NsdServiceInfo); + method public void onServiceLost(android.net.nsd.NsdServiceInfo); + method public void onStartDiscoveryFailed(String, int); + method public void onStopDiscoveryFailed(String, int); + } + + public static interface NsdManager.RegistrationListener { + method public void onRegistrationFailed(android.net.nsd.NsdServiceInfo, int); + method public void onServiceRegistered(android.net.nsd.NsdServiceInfo); + method public void onServiceUnregistered(android.net.nsd.NsdServiceInfo); + method public void onUnregistrationFailed(android.net.nsd.NsdServiceInfo, int); + } + + public static interface NsdManager.ResolveListener { + method public void onResolveFailed(android.net.nsd.NsdServiceInfo, int); + method public void onServiceResolved(android.net.nsd.NsdServiceInfo); + } + + public final class NsdServiceInfo implements android.os.Parcelable { + ctor public NsdServiceInfo(); + method public int describeContents(); + method public java.util.Map<java.lang.String,byte[]> getAttributes(); + method public java.net.InetAddress getHost(); + method @Nullable public android.net.Network getNetwork(); + method public int getPort(); + method public String getServiceName(); + method public String getServiceType(); + method public void removeAttribute(String); + method public void setAttribute(String, String); + method public void setHost(java.net.InetAddress); + method public void setNetwork(@Nullable android.net.Network); + method public void setPort(int); + method public void setServiceName(String); + method public void setServiceType(String); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR; + } + +} +
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt new file mode 100644 index 0000000..81d89c6 --- /dev/null +++ b/framework-t/api/module-lib-current.txt
@@ -0,0 +1,9 @@ +// Signature format: 2.0 +package android.net { + + public final class ConnectivityFrameworkInitializerTiramisu { + method public static void registerServiceWrappers(); + } + +} +
diff --git a/framework-t/api/module-lib-removed.txt b/framework-t/api/module-lib-removed.txt new file mode 100644 index 0000000..d802177 --- /dev/null +++ b/framework-t/api/module-lib-removed.txt
@@ -0,0 +1 @@ +// Signature format: 2.0
diff --git a/framework-t/api/removed.txt b/framework-t/api/removed.txt new file mode 100644 index 0000000..d802177 --- /dev/null +++ b/framework-t/api/removed.txt
@@ -0,0 +1 @@ +// Signature format: 2.0
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt new file mode 100644 index 0000000..d802177 --- /dev/null +++ b/framework-t/api/system-current.txt
@@ -0,0 +1 @@ +// Signature format: 2.0
diff --git a/framework-t/api/system-removed.txt b/framework-t/api/system-removed.txt new file mode 100644 index 0000000..d802177 --- /dev/null +++ b/framework-t/api/system-removed.txt
@@ -0,0 +1 @@ +// Signature format: 2.0
diff --git a/framework/Android.bp b/framework/Android.bp index 028701a..d3e46fa 100644 --- a/framework/Android.bp +++ b/framework/Android.bp
@@ -55,12 +55,11 @@ ], } -java_sdk_library { - name: "framework-connectivity", +java_defaults { + name: "framework-connectivity-defaults", + defaults: ["framework-module-defaults"], sdk_version: "module_current", min_sdk_version: "30", - defaults: ["framework-module-defaults"], - installable: true, srcs: [ ":framework-connectivity-sources", ":net-utils-framework-common-srcs", @@ -76,6 +75,9 @@ "frameworks/native/aidl/binder", // For PersistableBundle.aidl ], }, + stub_only_libs: [ + "framework-connectivity-tiramisu.stubs.module_lib", + ], impl_only_libs: [ "framework-tethering.stubs.module_lib", "framework-wifi.stubs.module_lib", @@ -83,22 +85,50 @@ ], static_libs: [ "modules-utils-build", + "modules-utils-preconditions", ], libs: [ + "framework-connectivity-tiramisu.stubs.module_lib", "unsupportedappusage", ], - jarjar_rules: "jarjar-rules.txt", + apex_available: [ + "com.android.tethering", + ], + lint: { strict_updatability_linting: true }, +} + +java_library { + name: "framework-connectivity-pre-jarjar", + defaults: ["framework-connectivity-defaults"], + libs: [ + // This cannot be in the defaults clause above because if it were, it would be used + // to generate the connectivity stubs. That would create a circular dependency + // because the tethering stubs depend on the connectivity stubs (e.g., + // TetheringRequest depends on LinkAddress). + "framework-tethering.stubs.module_lib", + ], + visibility: ["//packages/modules/Connectivity:__subpackages__"] +} + +java_sdk_library { + name: "framework-connectivity", + defaults: ["framework-connectivity-defaults"], + installable: true, + jarjar_rules: ":connectivity-jarjar-rules", permitted_packages: ["android.net"], impl_library_visibility: [ "//packages/modules/Connectivity/Tethering/apex", // In preparation for future move "//packages/modules/Connectivity/apex", + "//packages/modules/Connectivity/framework-t", "//packages/modules/Connectivity/service", + "//packages/modules/Connectivity/service-t", "//frameworks/base/packages/Connectivity/service", "//frameworks/base", // Tests using hidden APIs "//cts/tests/netlegacy22.api", + "//cts/tests/tests/app.usage", // NetworkUsageStatsTest "//external/sl4a:__subpackages__", "//frameworks/base/packages/Connectivity/tests:__subpackages__", "//frameworks/libs/net/common/testutils", @@ -110,10 +140,6 @@ "//packages/modules/NetworkStack/tests:__subpackages__", "//packages/modules/Wifi/service/tests/wifitests", ], - apex_available: [ - "com.android.tethering", - ], - lint: { strict_updatability_linting: true }, } cc_library_shared {
diff --git a/framework/aidl-export/android/net/DscpPolicy.aidl b/framework/aidl-export/android/net/DscpPolicy.aidl new file mode 100644 index 0000000..8da42ca --- /dev/null +++ b/framework/aidl-export/android/net/DscpPolicy.aidl
@@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +@JavaOnlyStableParcelable parcelable DscpPolicy;
diff --git a/framework/api/current.txt b/framework/api/current.txt index 827da6d..547b7e2 100644 --- a/framework/api/current.txt +++ b/framework/api/current.txt
@@ -205,6 +205,21 @@ method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String); } + public final class IpConfiguration implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.net.ProxyInfo getHttpProxy(); + method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR; + } + + public static final class IpConfiguration.Builder { + ctor public IpConfiguration.Builder(); + method @NonNull public android.net.IpConfiguration build(); + method @NonNull public android.net.IpConfiguration.Builder setHttpProxy(@Nullable android.net.ProxyInfo); + method @NonNull public android.net.IpConfiguration.Builder setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration); + } + public final class IpPrefix implements android.os.Parcelable { ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int); method public boolean contains(@NonNull java.net.InetAddress); @@ -294,7 +309,7 @@ ctor public NetworkCapabilities(android.net.NetworkCapabilities); method public int describeContents(); method @NonNull public int[] getCapabilities(); - method @NonNull public int[] getEnterpriseCapabilitySubLevels(); + method @NonNull public int[] getEnterpriseIds(); method public int getLinkDownstreamBandwidthKbps(); method public int getLinkUpstreamBandwidthKbps(); method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier(); @@ -302,6 +317,7 @@ method public int getSignalStrength(); method @Nullable public android.net.TransportInfo getTransportInfo(); method public boolean hasCapability(int); + method public boolean hasEnterpriseId(int); method public boolean hasTransport(int); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR; @@ -325,6 +341,8 @@ field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12 field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15 field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf + field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23 + field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22 field public static final int NET_CAPABILITY_RCS = 8; // 0x8 field public static final int NET_CAPABILITY_SUPL = 1; // 0x1 field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19 @@ -332,6 +350,11 @@ field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10 field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6 field public static final int NET_CAPABILITY_XCAP = 9; // 0x9 + field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1 + field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2 + field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3 + field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4 + field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5 field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000 field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2 field public static final int TRANSPORT_CELLULAR = 0; // 0x0 @@ -477,6 +500,25 @@ method public void onStopped(); } + public final class StaticIpConfiguration implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<java.net.InetAddress> getDnsServers(); + method @Nullable public String getDomains(); + method @Nullable public java.net.InetAddress getGateway(); + method @NonNull public android.net.LinkAddress getIpAddress(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR; + } + + public static final class StaticIpConfiguration.Builder { + ctor public StaticIpConfiguration.Builder(); + method @NonNull public android.net.StaticIpConfiguration build(); + method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>); + method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String); + method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress); + method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@NonNull android.net.LinkAddress); + } + public interface TransportInfo { }
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt index fea880a..64e159b 100644 --- a/framework/api/module-lib-current.txt +++ b/framework/api/module-lib-current.txt
@@ -12,11 +12,13 @@ method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange(); method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo); method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean); method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable); @@ -24,7 +26,9 @@ method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void swapActiveStatsMap(); method public void systemReady(); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateFirewallRule(int, int, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkAllowList(int, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void updateMeteredNetworkDenyList(int, boolean); field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE"; @@ -39,10 +43,17 @@ field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1 field public static final int BLOCKED_REASON_DOZE = 2; // 0x2 field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10 + field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20 field public static final int BLOCKED_REASON_NONE = 0; // 0x0 field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8 + field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1 + field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5 + field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3 + field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4 + field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2 field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0 field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1 + field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; // 0x2 } public static class ConnectivityManager.NetworkCallback { @@ -113,22 +124,21 @@ public final class NetworkAgentConfig implements android.os.Parcelable { method @Nullable public String getSubscriberId(); + method public boolean getVpnRequiresValidation(); method public boolean isBypassableVpn(); } public static final class NetworkAgentConfig.Builder { method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setExcludeLocalRoutesVpn(boolean); method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String); + method @NonNull public android.net.NetworkAgentConfig.Builder setVpnRequiresValidation(boolean); } public final class NetworkCapabilities implements android.os.Parcelable { + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public java.util.Set<java.lang.Integer> getAccessUids(); method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids(); method public boolean hasForbiddenCapability(int); - field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1; // 0x1 - field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2; // 0x2 - field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3; // 0x3 - field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4; // 0x4 - field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5; // 0x5 field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L @@ -138,11 +148,14 @@ } public static final class NetworkCapabilities.Builder { + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAccessUids(@NonNull java.util.Set<java.lang.Integer>); method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>); } public class NetworkRequest implements android.os.Parcelable { + method @NonNull public int[] getEnterpriseIds(); method @NonNull public int[] getForbiddenCapabilities(); + method public boolean hasEnterpriseId(int); method public boolean hasForbiddenCapability(int); } @@ -154,7 +167,10 @@ public final class ProfileNetworkPreference implements android.os.Parcelable { method public int describeContents(); + method @NonNull public java.util.List<java.lang.Integer> getExcludedUids(); + method @NonNull public java.util.List<java.lang.Integer> getIncludedUids(); method public int getPreference(); + method public int getPreferenceEnterpriseId(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR; } @@ -162,7 +178,10 @@ public static final class ProfileNetworkPreference.Builder { ctor public ProfileNetworkPreference.Builder(); method @NonNull public android.net.ProfileNetworkPreference build(); + method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@Nullable java.util.List<java.lang.Integer>); + method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@Nullable java.util.List<java.lang.Integer>); method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int); + method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int); } public final class TestNetworkInterface implements android.os.Parcelable {
diff --git a/framework/api/module-lib-lint-baseline.txt b/framework/api/module-lib-lint-baseline.txt deleted file mode 100644 index c7b0db5..0000000 --- a/framework/api/module-lib-lint-baseline.txt +++ /dev/null
@@ -1,7 +0,0 @@ -// Baseline format: 1.0 -NoByteOrShort: android.net.DhcpOption#DhcpOption(byte, byte[]) parameter #0: - Should avoid odd sized primitives; use `int` instead of `byte` in parameter type in android.net.DhcpOption(byte type, byte[] value) -NoByteOrShort: android.net.DhcpOption#describeContents(): - Should avoid odd sized primitives; use `int` instead of `byte` in method android.net.DhcpOption.describeContents() -NoByteOrShort: android.net.DhcpOption#getType(): - Should avoid odd sized primitives; use `int` instead of `byte` in method android.net.DhcpOption.getType()
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt index 8d084e6..764cffa 100644 --- a/framework/api/system-current.txt +++ b/framework/api/system-current.txt
@@ -93,6 +93,35 @@ method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network); } + public final class DscpPolicy implements android.os.Parcelable { + method @Nullable public java.net.InetAddress getDestinationAddress(); + method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange(); + method public int getDscpValue(); + method public int getPolicyId(); + method public int getProtocol(); + method @Nullable public java.net.InetAddress getSourceAddress(); + method public int getSourcePort(); + field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR; + field public static final int PROTOCOL_ANY = -1; // 0xffffffff + field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff + field public static final int STATUS_DELETED = 4; // 0x4 + field public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3 + field public static final int STATUS_POLICY_NOT_FOUND = 5; // 0x5 + field public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2 + field public static final int STATUS_REQUEST_DECLINED = 1; // 0x1 + field public static final int STATUS_SUCCESS = 0; // 0x0 + } + + public static final class DscpPolicy.Builder { + ctor public DscpPolicy.Builder(int, int); + method @NonNull public android.net.DscpPolicy build(); + method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress); + method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>); + method @NonNull public android.net.DscpPolicy.Builder setProtocol(int); + method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress); + method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int); + } + public final class InvalidPacketException extends java.lang.Exception { ctor public InvalidPacketException(int); method public int getError(); @@ -104,17 +133,12 @@ public final class IpConfiguration implements android.os.Parcelable { ctor public IpConfiguration(); ctor public IpConfiguration(@NonNull android.net.IpConfiguration); - method public int describeContents(); - method @Nullable public android.net.ProxyInfo getHttpProxy(); method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment(); method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings(); - method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration(); method public void setHttpProxy(@Nullable android.net.ProxyInfo); method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment); method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings); method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration); - method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR; } public enum IpConfiguration.IpAssignment { @@ -217,6 +241,7 @@ method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData); method public void onAutomaticReconnectDisabled(); method public void onBandwidthUpdateRequested(); + method public void onDscpPolicyStatusUpdated(int, int); method public void onNetworkCreated(); method public void onNetworkDestroyed(); method public void onNetworkUnwanted(); @@ -229,6 +254,7 @@ method public void onStopSocketKeepalive(int); method public void onValidationStatus(int, @Nullable android.net.Uri); method @NonNull public android.net.Network register(); + method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy); method public final void sendLinkProperties(@NonNull android.net.LinkProperties); method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities); method public final void sendNetworkScore(@NonNull android.net.NetworkScore); @@ -236,6 +262,8 @@ method public final void sendQosCallbackError(int, int); method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes); method public final void sendQosSessionLost(int, int, int); + method public void sendRemoveAllDscpPolicies(); + method public void sendRemoveDscpPolicy(int); method public final void sendSocketKeepaliveEvent(int, int); method @Deprecated public void setLegacySubtype(int, @NonNull String); method public void setLingerDuration(@NonNull java.time.Duration); @@ -294,11 +322,11 @@ ctor public NetworkCapabilities.Builder(); ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities); method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int); - method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseCapabilitySubLevel(int); + method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int); method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int); method @NonNull public android.net.NetworkCapabilities build(); method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int); - method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseCapabilitySubLevel(int); + method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int); method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int); method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]); method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int); @@ -451,23 +479,7 @@ ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration); method public void addDnsServer(@NonNull java.net.InetAddress); method public void clear(); - method public int describeContents(); - method @NonNull public java.util.List<java.net.InetAddress> getDnsServers(); - method @Nullable public String getDomains(); - method @Nullable public java.net.InetAddress getGateway(); - method @Nullable public android.net.LinkAddress getIpAddress(); method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String); - method public void writeToParcel(android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR; - } - - public static final class StaticIpConfiguration.Builder { - ctor public StaticIpConfiguration.Builder(); - method @NonNull public android.net.StaticIpConfiguration build(); - method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>); - method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String); - method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress); - method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@Nullable android.net.LinkAddress); } public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt deleted file mode 100644 index ae6b9c3..0000000 --- a/framework/jarjar-rules.txt +++ /dev/null
@@ -1,3 +0,0 @@ -rule com.android.net.module.util.** android.net.connectivity.framework.util.@1 -rule com.android.modules.utils.** android.net.connectivity.framework.modules.utils.@1 -rule android.net.NetworkFactory* android.net.connectivity.framework.NetworkFactory@1
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 465595f..5246623 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,7 @@ package android.net; import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST; import static android.net.NetworkRequest.Type.LISTEN; import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST; @@ -876,6 +877,15 @@ public static final int BLOCKED_REASON_LOCKDOWN_VPN = 1 << 4; /** + * Flag to indicate that an app is subject to Low Power Standby restrictions that would + * result in its network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 1 << 5; + + /** * Flag to indicate that an app is subject to Data saver restrictions that would * result in its metered network access being blocked. * @@ -913,6 +923,7 @@ BLOCKED_REASON_APP_STANDBY, BLOCKED_REASON_RESTRICTED_MODE, BLOCKED_REASON_LOCKDOWN_VPN, + BLOCKED_REASON_LOW_POWER_STANDBY, BLOCKED_METERED_REASON_DATA_SAVER, BLOCKED_METERED_REASON_USER_RESTRICTED, BLOCKED_METERED_REASON_ADMIN_DISABLED, @@ -930,6 +941,59 @@ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) private final IConnectivityManager mService; + // LINT.IfChange(firewall_chain) + /** + * Firewall chain for device idle (doze mode). + * Allowlist of apps that have network access in device idle. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int FIREWALL_CHAIN_DOZABLE = 1; + + /** + * Firewall chain used for app standby. + * Denylist of apps that do not have network access. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int FIREWALL_CHAIN_STANDBY = 2; + + /** + * Firewall chain used for battery saver. + * Allowlist of apps that have network access when battery saver is on. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int FIREWALL_CHAIN_POWERSAVE = 3; + + /** + * Firewall chain used for restricted networking mode. + * Allowlist of apps that have access in restricted networking mode. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int FIREWALL_CHAIN_RESTRICTED = 4; + + /** + * Firewall chain used for low power standby. + * Allowlist of apps that have access in low power standby. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = { + FIREWALL_CHAIN_DOZABLE, + FIREWALL_CHAIN_STANDBY, + FIREWALL_CHAIN_POWERSAVE, + FIREWALL_CHAIN_RESTRICTED, + FIREWALL_CHAIN_LOW_POWER_STANDBY + }) + public @interface FirewallChain {} + // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h) + /** * A kludge to facilitate static access where a Context pointer isn't available, like in the * case of the static set/getProcessDefaultNetwork methods and from the Network class. @@ -1078,7 +1142,8 @@ } /** - * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}. + * Preference for {@link ProfileNetworkPreference#setPreference(int)}. + * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)} * Specify that the traffic for this user should by follow the default rules. * @hide */ @@ -1086,7 +1151,8 @@ public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; /** - * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}. + * Preference for {@link ProfileNetworkPreference#setPreference(int)}. + * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)} * Specify that the traffic for this user should by default go on a network with * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE}, and on the system default network * if no such network is available. @@ -1095,11 +1161,23 @@ @SystemApi(client = MODULE_LIBRARIES) public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; + /** + * Preference for {@link ProfileNetworkPreference#setPreference(int)}. + * {@see #setProfileNetworkPreferences(UserHandle, List, Executor, Runnable)} + * Specify that the traffic for this user should by default go on a network with + * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE} and if no such network is available + * should not go on the system default network + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK = 2; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = { PROFILE_NETWORK_PREFERENCE_DEFAULT, - PROFILE_NETWORK_PREFERENCE_ENTERPRISE + PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK }) public @interface ProfileNetworkPreferencePolicy { } @@ -5481,6 +5559,9 @@ ProfileNetworkPreference.Builder preferenceBuilder = new ProfileNetworkPreference.Builder(); preferenceBuilder.setPreference(preference); + if (preference != PROFILE_NETWORK_PREFERENCE_DEFAULT) { + preferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + } setProfileNetworkPreferences(profile, List.of(preferenceBuilder.build()), executor, listener); } @@ -5552,9 +5633,11 @@ } /** - * Allow target application using metered network. + * Sets whether the specified UID is allowed to use data on metered networks even when + * background data is restricted. * * @param uid uid of target app + * @throws IllegalStateException if updating allow list failed. * @hide */ @SystemApi(client = MODULE_LIBRARIES) @@ -5568,15 +5651,15 @@ mService.updateMeteredNetworkAllowList(uid, add); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); - } catch (IllegalStateException ie) { - throw ie; } } /** - * Disallow target application using metered network. + * Sets whether the specified UID is prevented from using background data on metered networks. + * Takes precedence over {@link #updateMeteredNetworkAllowList}. * * @param uid uid of target app + * @throws IllegalStateException if updating deny list failed. * @hide */ @SystemApi(client = MODULE_LIBRARIES) @@ -5590,8 +5673,99 @@ mService.updateMeteredNetworkDenyList(uid, add); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); - } catch (IllegalStateException ie) { - throw ie; + } + } + + /** + * Sets a firewall rule for the specified UID on the specified chain. + * + * @param chain target chain. + * @param uid uid to allow/deny. + * @param allow whether networking is allowed or denied. + * @throws IllegalStateException if updating firewall rule failed. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void updateFirewallRule(@FirewallChain final int chain, final int uid, + final boolean allow) { + try { + mService.updateFirewallRule(chain, uid, allow); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enables or disables the specified firewall chain. + * + * @param chain target chain. + * @param enable whether the chain should be enabled. + * @throws IllegalStateException if enabling or disabling the firewall chain failed. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void setFirewallChainEnabled(@FirewallChain final int chain, final boolean enable) { + try { + mService.setFirewallChainEnabled(chain, enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Replaces the contents of the specified UID-based firewall chain. + * + * @param chain target chain to replace. + * @param uids The list of UIDs to be placed into chain. + * @throws IllegalStateException if replacing the firewall chain failed. + * @throws IllegalArgumentException if {@code chain} is not a valid chain. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void replaceFirewallChain(@FirewallChain final int chain, @NonNull final int[] uids) { + Objects.requireNonNull(uids); + try { + mService.replaceFirewallChain(chain, uids); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Request to change the current active network stats map. + * STOPSHIP: Remove this API before T sdk finalized, this API is temporary added for the + * NetworkStatsFactory which is platform code but will be moved into connectivity (tethering) + * mainline module. + * + * @throws IllegalStateException if swapping active stats map failed. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void swapActiveStatsMap() { + try { + mService.swapActiveStatsMap(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } }
diff --git a/framework/src/android/net/DhcpOption.java b/framework/src/android/net/DhcpOption.java index a125290..b30470a 100644 --- a/framework/src/android/net/DhcpOption.java +++ b/framework/src/android/net/DhcpOption.java
@@ -18,6 +18,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -35,12 +36,13 @@ /** * Constructs a DhcpOption object. * - * @param type the type of this option + * @param type the type of this option. For more information, see + * https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml. * @param value the value of this option. If {@code null}, DHCP packets containing this option * will include the option type in the Parameter Request List. Otherwise, DHCP * packets containing this option will include the option in the options section. */ - public DhcpOption(byte type, @Nullable byte[] value) { + public DhcpOption(@SuppressLint("NoByteOrShort") byte type, @Nullable byte[] value) { mType = type; mValue = value; } @@ -69,6 +71,7 @@ }; /** Get the type of DHCP option */ + @SuppressLint("NoByteOrShort") public byte getType() { return mType; }
diff --git a/framework/src/android/net/DscpPolicy.java b/framework/src/android/net/DscpPolicy.java new file mode 100644 index 0000000..cda8205 --- /dev/null +++ b/framework/src/android/net/DscpPolicy.java
@@ -0,0 +1,397 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Range; + +import com.android.net.module.util.InetAddressUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Objects; + + +/** + * DSCP policy to be set on the requesting NetworkAgent. + * @hide + */ +@SystemApi +public final class DscpPolicy implements Parcelable { + /** + * Indicates that the policy does not specify a protocol. + */ + public static final int PROTOCOL_ANY = -1; + + /** + * Indicates that the policy does not specify a port. + */ + public static final int SOURCE_PORT_ANY = -1; + + /** + * Policy was successfully added. + */ + public static final int STATUS_SUCCESS = 0; + + /** + * Policy was rejected for any reason besides invalid classifier or insufficient resources. + */ + public static final int STATUS_REQUEST_DECLINED = 1; + + /** + * Requested policy contained a classifier which is not supported. + */ + public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; + + /** + * TODO: should this error case be supported? + */ + public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; + + /** + * Policy was deleted. + */ + public static final int STATUS_DELETED = 4; + + /** + * Policy was not found during deletion. + */ + public static final int STATUS_POLICY_NOT_FOUND = 5; + + /** The unique policy ID. Each requesting network is responsible for maintaining policy IDs + * unique within that network. In the case where a policy with an existing ID is created, the + * new policy will update the existing policy with the same ID. + */ + private final int mPolicyId; + + /** The QoS DSCP marking to be added to packets matching the policy. */ + private final int mDscp; + + /** The source IP address. */ + private final @Nullable InetAddress mSrcAddr; + + /** The destination IP address. */ + private final @Nullable InetAddress mDstAddr; + + /** The source port. */ + private final int mSrcPort; + + /** The IP protocol that the policy requires. */ + private final int mProtocol; + + /** Destination port range. Inclusive range. */ + private final @Nullable Range<Integer> mDstPortRange; + + /** + * Implement the Parcelable interface + * + * @hide + */ + public int describeContents() { + return 0; + } + + /** @hide */ + @IntDef(prefix = "STATUS_", value = { + STATUS_SUCCESS, + STATUS_REQUEST_DECLINED, + STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED, + STATUS_INSUFFICIENT_PROCESSING_RESOURCES, + STATUS_DELETED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DscpPolicyStatus {} + + /* package */ DscpPolicy( + int policyId, + int dscp, + @Nullable InetAddress srcAddr, + @Nullable InetAddress dstAddr, + int srcPort, + int protocol, + Range<Integer> dstPortRange) { + this.mPolicyId = policyId; + this.mDscp = dscp; + this.mSrcAddr = srcAddr; + this.mDstAddr = dstAddr; + this.mSrcPort = srcPort; + this.mProtocol = protocol; + this.mDstPortRange = dstPortRange; + + if (mPolicyId < 1 || mPolicyId > 255) { + throw new IllegalArgumentException("Policy ID not in valid range: " + mPolicyId); + } + if (mDscp < 0 || mDscp > 63) { + throw new IllegalArgumentException("DSCP value not in valid range: " + mDscp); + } + // Since SOURCE_PORT_ANY is the default source port value need to allow it as well. + // TODO: Move the default value into this constructor or throw an error from the + // instead. + if (mSrcPort < -1 || mSrcPort > 65535) { + throw new IllegalArgumentException("Source port not in valid range: " + mSrcPort); + } + if (mDstPortRange != null + && (dstPortRange.getLower() < 0 || mDstPortRange.getLower() > 65535) + && (mDstPortRange.getUpper() < 0 || mDstPortRange.getUpper() > 65535)) { + throw new IllegalArgumentException("Destination port not in valid range"); + } + if (mSrcAddr != null && mDstAddr != null && (mSrcAddr instanceof Inet6Address) + != (mDstAddr instanceof Inet6Address)) { + throw new IllegalArgumentException("Source/destination address of different family"); + } + } + + /** + * The unique policy ID. + * + * Each requesting network is responsible for maintaining unique + * policy IDs. In the case where a policy with an existing ID is created, the new + * policy will update the existing policy with the same ID + * + * @return Policy ID set in Builder. + */ + public int getPolicyId() { + return mPolicyId; + } + + /** + * The QoS DSCP marking to be added to packets matching the policy. + * + * @return DSCP value set in Builder. + */ + public int getDscpValue() { + return mDscp; + } + + /** + * The source IP address. + * + * @return Source IP address set in Builder or {@code null} if none was set. + */ + public @Nullable InetAddress getSourceAddress() { + return mSrcAddr; + } + + /** + * The destination IP address. + * + * @return Destination IP address set in Builder or {@code null} if none was set. + */ + public @Nullable InetAddress getDestinationAddress() { + return mDstAddr; + } + + /** + * The source port. + * + * @return Source port set in Builder or {@link #SOURCE_PORT_ANY} if no port was set. + */ + public int getSourcePort() { + return mSrcPort; + } + + /** + * The IP protocol that the policy requires. + * + * @return Protocol set in Builder or {@link #PROTOCOL_ANY} if no protocol was set. + * {@link #PROTOCOL_ANY} indicates that any protocol will be matched. + */ + public int getProtocol() { + return mProtocol; + } + + /** + * Destination port range. Inclusive range. + * + * @return Range<Integer> set in Builder or {@code null} if none was set. + */ + public @Nullable Range<Integer> getDestinationPortRange() { + return mDstPortRange; + } + + @Override + public String toString() { + return "DscpPolicy { " + + "policyId = " + mPolicyId + ", " + + "dscp = " + mDscp + ", " + + "srcAddr = " + mSrcAddr + ", " + + "dstAddr = " + mDstAddr + ", " + + "srcPort = " + mSrcPort + ", " + + "protocol = " + mProtocol + ", " + + "dstPortRange = " + + (mDstPortRange == null ? "none" : mDstPortRange.toString()) + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof DscpPolicy)) return false; + DscpPolicy that = (DscpPolicy) o; + return true + && mPolicyId == that.mPolicyId + && mDscp == that.mDscp + && Objects.equals(mSrcAddr, that.mSrcAddr) + && Objects.equals(mDstAddr, that.mDstAddr) + && mSrcPort == that.mSrcPort + && mProtocol == that.mProtocol + && Objects.equals(mDstPortRange, that.mDstPortRange); + } + + @Override + public int hashCode() { + return Objects.hash(mPolicyId, mDscp, mSrcAddr.hashCode(), + mDstAddr.hashCode(), mSrcPort, mProtocol, mDstPortRange.hashCode()); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mPolicyId); + dest.writeInt(mDscp); + InetAddressUtils.parcelInetAddress(dest, mSrcAddr, flags); + InetAddressUtils.parcelInetAddress(dest, mDstAddr, flags); + dest.writeInt(mSrcPort); + dest.writeInt(mProtocol); + dest.writeBoolean(mDstPortRange != null ? true : false); + if (mDstPortRange != null) { + dest.writeInt(mDstPortRange.getLower()); + dest.writeInt(mDstPortRange.getUpper()); + } + } + + /** @hide */ + DscpPolicy(@NonNull Parcel in) { + this.mPolicyId = in.readInt(); + this.mDscp = in.readInt(); + this.mSrcAddr = InetAddressUtils.unparcelInetAddress(in); + this.mDstAddr = InetAddressUtils.unparcelInetAddress(in); + this.mSrcPort = in.readInt(); + this.mProtocol = in.readInt(); + if (in.readBoolean()) { + this.mDstPortRange = new Range<Integer>(in.readInt(), in.readInt()); + } else { + this.mDstPortRange = null; + } + } + + /** @hide */ + public @SystemApi static final @NonNull Parcelable.Creator<DscpPolicy> CREATOR = + new Parcelable.Creator<DscpPolicy>() { + @Override + public DscpPolicy[] newArray(int size) { + return new DscpPolicy[size]; + } + + @Override + public DscpPolicy createFromParcel(@NonNull android.os.Parcel in) { + return new DscpPolicy(in); + } + }; + + /** + * A builder for {@link DscpPolicy} + * + */ + public static final class Builder { + + private final int mPolicyId; + private final int mDscp; + private @Nullable InetAddress mSrcAddr; + private @Nullable InetAddress mDstAddr; + private int mSrcPort = SOURCE_PORT_ANY; + private int mProtocol = PROTOCOL_ANY; + private @Nullable Range<Integer> mDstPortRange; + + private long mBuilderFieldsSet = 0L; + + /** + * Creates a new Builder. + * + * @param policyId The unique policy ID. Each requesting network is responsible for + * maintaining unique policy IDs. In the case where a policy with an + * existing ID is created, the new policy will update the existing + * policy with the same ID + * @param dscpValue The DSCP value to set. + */ + public Builder(int policyId, int dscpValue) { + mPolicyId = policyId; + mDscp = dscpValue; + } + + /** + * Specifies that this policy matches packets with the specified source IP address. + */ + public @NonNull Builder setSourceAddress(@NonNull InetAddress value) { + mSrcAddr = value; + return this; + } + + /** + * Specifies that this policy matches packets with the specified destination IP address. + */ + public @NonNull Builder setDestinationAddress(@NonNull InetAddress value) { + mDstAddr = value; + return this; + } + + /** + * Specifies that this policy matches packets with the specified source port. + */ + public @NonNull Builder setSourcePort(int value) { + mSrcPort = value; + return this; + } + + /** + * Specifies that this policy matches packets with the specified protocol. + */ + public @NonNull Builder setProtocol(int value) { + mProtocol = value; + return this; + } + + /** + * Specifies that this policy matches packets with the specified destination port range. + */ + public @NonNull Builder setDestinationPortRange(@NonNull Range<Integer> range) { + mDstPortRange = range; + return this; + } + + /** + * Constructs a DscpPolicy with the specified parameters. + */ + public @NonNull DscpPolicy build() { + return new DscpPolicy( + mPolicyId, + mDscp, + mSrcAddr, + mDstAddr, + mSrcPort, + mProtocol, + mDstPortRange); + } + } +}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl index 5740d85..df4663f 100644 --- a/framework/src/android/net/IConnectivityManager.aidl +++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -234,4 +234,12 @@ void updateMeteredNetworkAllowList(int uid, boolean add); void updateMeteredNetworkDenyList(int uid, boolean add); + + void updateFirewallRule(int chain, int uid, boolean allow); + + void setFirewallChainEnabled(int chain, boolean enable); + + void replaceFirewallChain(int chain, in int[] uids); + + void swapActiveStatsMap(); }
diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl index d941d4b..fa5175c 100644 --- a/framework/src/android/net/INetworkAgent.aidl +++ b/framework/src/android/net/INetworkAgent.aidl
@@ -48,4 +48,5 @@ void onQosCallbackUnregistered(int qosCallbackId); void onNetworkCreated(); void onNetworkDestroyed(); + void onDscpPolicyStatusUpdated(int policyId, int status); }
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl index 9a58add..08536ca 100644 --- a/framework/src/android/net/INetworkAgentRegistry.aidl +++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -15,6 +15,7 @@ */ package android.net; +import android.net.DscpPolicy; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; @@ -43,4 +44,7 @@ void sendQosCallbackError(int qosCallbackId, int exceptionType); void sendTeardownDelayMs(int teardownDelayMs); void sendLingerDuration(int durationMs); + void sendAddDscpPolicy(in DscpPolicy policy); + void sendRemoveDscpPolicy(int policyId); + void sendRemoveAllDscpPolicies(); }
diff --git a/framework/src/android/net/IpConfiguration.java b/framework/src/android/net/IpConfiguration.java index d5f8b2e..99835aa 100644 --- a/framework/src/android/net/IpConfiguration.java +++ b/framework/src/android/net/IpConfiguration.java
@@ -28,16 +28,16 @@ import java.util.Objects; /** - * A class representing a configured network. - * @hide + * A class representing the IP configuration of a network. */ -@SystemApi public final class IpConfiguration implements Parcelable { private static final String TAG = "IpConfiguration"; // This enum has been used by apps through reflection for many releases. // Therefore they can't just be removed. Duplicating these constants to // give an alternate SystemApi is a worse option than exposing them. + /** @hide */ + @SystemApi @SuppressLint("Enum") public enum IpAssignment { /* Use statically configured IP settings. Configuration can be accessed @@ -59,6 +59,8 @@ // This enum has been used by apps through reflection for many releases. // Therefore they can't just be removed. Duplicating these constants to // give an alternate SystemApi is a worse option than exposing them. + /** @hide */ + @SystemApi @SuppressLint("Enum") public enum ProxySettings { /* No proxy is to be used. Any existing proxy settings @@ -94,6 +96,8 @@ null : new ProxyInfo(httpProxy); } + /** @hide */ + @SystemApi public IpConfiguration() { init(IpAssignment.UNASSIGNED, ProxySettings.UNASSIGNED, null, null); } @@ -107,6 +111,8 @@ init(ipAssignment, proxySettings, staticIpConfiguration, httpProxy); } + /** @hide */ + @SystemApi public IpConfiguration(@NonNull IpConfiguration source) { this(); if (source != null) { @@ -115,34 +121,58 @@ } } + /** @hide */ + @SystemApi public @NonNull IpAssignment getIpAssignment() { return ipAssignment; } + /** @hide */ + @SystemApi public void setIpAssignment(@NonNull IpAssignment ipAssignment) { this.ipAssignment = ipAssignment; } + /** + * Get the current static IP configuration (possibly null). Configured via + * {@link Builder#setStaticIpConfiguration(StaticIpConfiguration)}. + * + * @return Current static IP configuration. + */ public @Nullable StaticIpConfiguration getStaticIpConfiguration() { return staticIpConfiguration; } + /** @hide */ + @SystemApi public void setStaticIpConfiguration(@Nullable StaticIpConfiguration staticIpConfiguration) { this.staticIpConfiguration = staticIpConfiguration; } + /** @hide */ + @SystemApi public @NonNull ProxySettings getProxySettings() { return proxySettings; } + /** @hide */ + @SystemApi public void setProxySettings(@NonNull ProxySettings proxySettings) { this.proxySettings = proxySettings; } + /** + * The proxy configuration of this object. + * + * @return The proxy information of this object configured via + * {@link Builder#setHttpProxy(ProxyInfo)}. + */ public @Nullable ProxyInfo getHttpProxy() { return httpProxy; } + /** @hide */ + @SystemApi public void setHttpProxy(@Nullable ProxyInfo httpProxy) { this.httpProxy = httpProxy; } @@ -220,4 +250,56 @@ return new IpConfiguration[size]; } }; + + /** + * Builder used to construct {@link IpConfiguration} objects. + */ + public static final class Builder { + private StaticIpConfiguration mStaticIpConfiguration; + private ProxyInfo mProxyInfo; + + /** + * Set a static IP configuration. + * + * @param config Static IP configuration. + * @return A {@link Builder} object to allow chaining. + */ + public @NonNull Builder setStaticIpConfiguration(@Nullable StaticIpConfiguration config) { + mStaticIpConfiguration = config; + return this; + } + + /** + * Set a proxy configuration. + * + * @param proxyInfo Proxy configuration. + * @return A {@link Builder} object to allow chaining. + */ + public @NonNull Builder setHttpProxy(@Nullable ProxyInfo proxyInfo) { + mProxyInfo = proxyInfo; + return this; + } + + /** + * Construct an {@link IpConfiguration}. + * + * @return A new {@link IpConfiguration} object. + */ + public @NonNull IpConfiguration build() { + IpConfiguration config = new IpConfiguration(); + config.setStaticIpConfiguration(mStaticIpConfiguration); + config.setIpAssignment( + mStaticIpConfiguration == null ? IpAssignment.DHCP : IpAssignment.STATIC); + + config.setHttpProxy(mProxyInfo); + if (mProxyInfo == null) { + config.setProxySettings(ProxySettings.NONE); + } else { + config.setProxySettings( + mProxyInfo.getPacFileUrl() == null ? ProxySettings.STATIC + : ProxySettings.PAC); + } + return config; + } + } }
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java index adcf338..945e670 100644 --- a/framework/src/android/net/NetworkAgent.java +++ b/framework/src/android/net/NetworkAgent.java
@@ -25,6 +25,7 @@ import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.net.DscpPolicy.DscpPolicyStatus; import android.os.Build; import android.os.Bundle; import android.os.ConditionVariable; @@ -404,6 +405,35 @@ */ public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24; + /** + * Sent by the NetworkAgent to ConnectivityService to set add a DSCP policy. + * + * @hide + */ + public static final int EVENT_ADD_DSCP_POLICY = BASE + 25; + + /** + * Sent by the NetworkAgent to ConnectivityService to set remove a DSCP policy. + * + * @hide + */ + public static final int EVENT_REMOVE_DSCP_POLICY = BASE + 26; + + /** + * Sent by the NetworkAgent to ConnectivityService to remove all DSCP policies. + * + * @hide + */ + public static final int EVENT_REMOVE_ALL_DSCP_POLICIES = BASE + 27; + + /** + * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent of an updated + * status for a DSCP policy. + * + * @hide + */ + public static final int CMD_DSCP_POLICY_STATUS = BASE + 28; + private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) { final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType, config.legacyTypeName, config.legacySubTypeName); @@ -611,6 +641,12 @@ onNetworkDestroyed(); break; } + case CMD_DSCP_POLICY_STATUS: { + onDscpPolicyStatusUpdated( + msg.arg1 /* Policy ID */, + msg.arg2 /* DSCP Policy Status */); + break; + } } } } @@ -761,6 +797,13 @@ public void onNetworkDestroyed() { mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_DESTROYED)); } + + @Override + public void onDscpPolicyStatusUpdated(final int policyId, + @DscpPolicyStatus final int status) { + mHandler.sendMessage(mHandler.obtainMessage( + CMD_DSCP_POLICY_STATUS, policyId, status)); + } } /** @@ -1104,6 +1147,11 @@ public void onNetworkDestroyed() {} /** + * Called when when the DSCP Policy status has changed. + */ + public void onDscpPolicyStatusUpdated(int policyId, @DscpPolicyStatus int status) {} + + /** * Requests that the network hardware send the specified packet at the specified interval. * * @param slot the hardware slot on which to start the keepalive. @@ -1317,6 +1365,30 @@ queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs)); } + /** + * Add a DSCP Policy. + * @param policy the DSCP policy to be added. + */ + public void sendAddDscpPolicy(@NonNull final DscpPolicy policy) { + Objects.requireNonNull(policy); + queueOrSendMessage(ra -> ra.sendAddDscpPolicy(policy)); + } + + /** + * Remove the specified DSCP policy. + * @param policyId the ID corresponding to a specific DSCP Policy. + */ + public void sendRemoveDscpPolicy(final int policyId) { + queueOrSendMessage(ra -> ra.sendRemoveDscpPolicy(policyId)); + } + + /** + * Remove all the DSCP policies on this network. + */ + public void sendRemoveAllDscpPolicies() { + queueOrSendMessage(ra -> ra.sendRemoveAllDscpPolicies()); + } + /** @hide */ protected void log(final String s) { Log.d(LOG_TAG, "NetworkAgent: " + s);
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java index 93fc379..3f5d5e5 100644 --- a/framework/src/android/net/NetworkAgentConfig.java +++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -34,6 +34,8 @@ */ @SystemApi public final class NetworkAgentConfig implements Parcelable { + // TODO : make this object immutable. The fields that should stay mutable should likely + // migrate to NetworkAgentInfo. /** * If the {@link Network} is a VPN, whether apps are allowed to bypass the @@ -246,6 +248,27 @@ return excludeLocalRouteVpn; } + /** + * Whether network validation should be performed for this VPN network. + * {@see #getVpnRequiresValidation} + * @hide + */ + private boolean mVpnRequiresValidation = false; + + /** + * Whether network validation should be performed for this VPN network. + * + * If this network isn't a VPN this should always be {@code false}, and will be ignored + * if set. + * If this network is a VPN, false means this network should always be considered validated; + * true means it follows the same validation semantics as general internet networks. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public boolean getVpnRequiresValidation() { + return mVpnRequiresValidation; + } + /** @hide */ public NetworkAgentConfig() { } @@ -266,6 +289,7 @@ legacySubTypeName = nac.legacySubTypeName; mLegacyExtraInfo = nac.mLegacyExtraInfo; excludeLocalRouteVpn = nac.excludeLocalRouteVpn; + mVpnRequiresValidation = nac.mVpnRequiresValidation; } } @@ -409,6 +433,25 @@ } /** + * Sets whether network validation should be performed for this VPN network. + * + * Only agents registering a VPN network should use this setter. On other network + * types it will be ignored. + * False means this network should always be considered validated; + * true means it follows the same validation semantics as general internet. + * + * @param vpnRequiresValidation whether this VPN requires validation. + * Default is {@code false}. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + public Builder setVpnRequiresValidation(boolean vpnRequiresValidation) { + mConfig.mVpnRequiresValidation = vpnRequiresValidation; + return this; + } + + /** * Sets whether the apps can bypass the VPN connection. * * @return this builder, to facilitate chaining. @@ -425,8 +468,10 @@ * Sets whether the local traffic is exempted from VPN. * * @return this builder, to facilitate chaining. - * @hide TODO(184750836): Unhide once the implementation is completed. + * @hide */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) public Builder setExcludeLocalRoutesVpn(boolean excludeLocalRoutes) { mConfig.excludeLocalRouteVpn = excludeLocalRoutes; return this; @@ -456,14 +501,16 @@ && Objects.equals(subscriberId, that.subscriberId) && Objects.equals(legacyTypeName, that.legacyTypeName) && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo) - && excludeLocalRouteVpn == that.excludeLocalRouteVpn; + && excludeLocalRouteVpn == that.excludeLocalRouteVpn + && mVpnRequiresValidation == that.mVpnRequiresValidation; } @Override public int hashCode() { return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated, acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId, - skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo, excludeLocalRouteVpn); + skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo, excludeLocalRouteVpn, + mVpnRequiresValidation); } @Override @@ -481,6 +528,7 @@ + ", legacyTypeName = '" + legacyTypeName + '\'' + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\'' + ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\'' + + ", vpnRequiresValidation = '" + mVpnRequiresValidation + '\'' + "}"; } @@ -504,6 +552,7 @@ out.writeString(legacySubTypeName); out.writeString(mLegacyExtraInfo); out.writeInt(excludeLocalRouteVpn ? 1 : 0); + out.writeInt(mVpnRequiresValidation ? 1 : 0); } public static final @NonNull Creator<NetworkAgentConfig> CREATOR = @@ -524,6 +573,7 @@ networkAgentConfig.legacySubTypeName = in.readString(); networkAgentConfig.mLegacyExtraInfo = in.readString(); networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0; + networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0; return networkAgentConfig; }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java index 84f7cbb..b6cd760 100644 --- a/framework/src/android/net/NetworkCapabilities.java +++ b/framework/src/android/net/NetworkCapabilities.java
@@ -16,8 +16,6 @@ package android.net; -import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; - import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import android.annotation.IntDef; @@ -149,67 +147,83 @@ private String mRequestorPackageName; /** - * enterprise capability sub level 1 - * @hide + * Enterprise capability identifier 1. */ - @SystemApi(client = MODULE_LIBRARIES) - public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1; + public static final int NET_ENTERPRISE_ID_1 = 1; /** - * enterprise capability sub level 2 - * @hide + * Enterprise capability identifier 2. */ - @SystemApi(client = MODULE_LIBRARIES) - public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2; + public static final int NET_ENTERPRISE_ID_2 = 2; /** - * enterprise capability sub level 3 - * @hide + * Enterprise capability identifier 3. */ - @SystemApi(client = MODULE_LIBRARIES) - public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3; + public static final int NET_ENTERPRISE_ID_3 = 3; /** - * enterprise capability sub level 4 - * @hide + * Enterprise capability identifier 4. */ - @SystemApi(client = MODULE_LIBRARIES) - public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4; + public static final int NET_ENTERPRISE_ID_4 = 4; /** - * enterprise capability sub level 5 - * @hide + * Enterprise capability identifier 5. */ - @SystemApi(client = MODULE_LIBRARIES) - public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5; + public static final int NET_ENTERPRISE_ID_5 = 5; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "NET_CAPABILITY_ENTERPRISE_SUB_LEVEL" }, value = { - NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1, - NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2, - NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3, - NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4, - NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5, + NET_ENTERPRISE_ID_1, + NET_ENTERPRISE_ID_2, + NET_ENTERPRISE_ID_3, + NET_ENTERPRISE_ID_4, + NET_ENTERPRISE_ID_5, }) - public @interface EnterpriseCapabilitySubLevel { + public @interface EnterpriseId { } /** - * Bitfield representing the network's enterprise capability sublevel. If any are specified + * Bitfield representing the network's enterprise capability identifier. If any are specified * they will be satisfied by any Network that matches all of them. - * {@see addEnterpriseCapabilitySubLevel} for details on how masks are added + * {@see addEnterpriseId} for details on how masks are added */ - private int mEnterpriseCapabilitySubLevel; + private int mEnterpriseId; /** - * @return all the enteprise capabilities sub level set on this {@code NetworkCapability} - * instance. + * Get enteprise identifiers set. + * + * Get all the enterprise capabilities identifier set on this {@code NetworkCapability} + * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is + * considered to have NET_CAPABILITY_ENTERPRISE by default. + * @return all the enterprise capabilities identifier set. * */ - public @NonNull @EnterpriseCapabilitySubLevel int[] getEnterpriseCapabilitySubLevels() { - return NetworkCapabilitiesUtils.unpackBits(mEnterpriseCapabilitySubLevel); + public @NonNull @EnterpriseId int[] getEnterpriseIds() { + if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) { + return new int[]{NET_ENTERPRISE_ID_1}; + } + return NetworkCapabilitiesUtils.unpackBits(mEnterpriseId); + } + + /** + * Tests for the presence of an enterprise capability identifier on this instance. + * + * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is + * considered to have NET_CAPABILITY_ENTERPRISE by default. + * @param enterpriseId the enterprise capability identifier to be tested for. + * @return {@code true} if set on this instance. + */ + public boolean hasEnterpriseId( + @EnterpriseId int enterpriseId) { + if (enterpriseId == NET_ENTERPRISE_ID_1) { + if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) { + return true; + } + } + return isValidEnterpriseId(enterpriseId) + && ((mEnterpriseId & (1L << enterpriseId)) != 0); } public NetworkCapabilities() { @@ -250,6 +264,7 @@ mTransportInfo = null; mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED; mUids = null; + mAccessUids.clear(); mAdministratorUids = new int[0]; mOwnerUid = Process.INVALID_UID; mSSID = null; @@ -258,7 +273,7 @@ mRequestorPackageName = null; mSubIds = new ArraySet<>(); mUnderlyingNetworks = null; - mEnterpriseCapabilitySubLevel = 0; + mEnterpriseId = 0; } /** @@ -280,6 +295,7 @@ } mSignalStrength = nc.mSignalStrength; mUids = (nc.mUids == null) ? null : new ArraySet<>(nc.mUids); + setAccessUids(nc.mAccessUids); setAdministratorUids(nc.getAdministratorUids()); mOwnerUid = nc.mOwnerUid; mForbiddenNetworkCapabilities = nc.mForbiddenNetworkCapabilities; @@ -291,7 +307,7 @@ // mUnderlyingNetworks is an unmodifiable list if non-null, so a defensive copy is not // necessary. mUnderlyingNetworks = nc.mUnderlyingNetworks; - mEnterpriseCapabilitySubLevel = nc.mEnterpriseCapabilitySubLevel; + mEnterpriseId = nc.mEnterpriseId; } /** @@ -343,6 +359,8 @@ NET_CAPABILITY_BIP, NET_CAPABILITY_HEAD_UNIT, NET_CAPABILITY_MMTEL, + NET_CAPABILITY_PRIORITIZE_LATENCY, + NET_CAPABILITY_PRIORITIZE_BANDWIDTH, }) public @interface NetCapability { } @@ -586,29 +604,39 @@ */ public static final int NET_CAPABILITY_MMTEL = 33; + /** + * Indicates that this network should be able to prioritize latency for the internet. + */ + public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; + + /** + * Indicates that this network should be able to prioritize bandwidth for the internet. + */ + public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; + private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS; - private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_MMTEL; + private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH; /** * Network capabilities that are expected to be mutable, i.e., can change while a particular * network is connected. */ - private static final long MUTABLE_CAPABILITIES = + private static final long MUTABLE_CAPABILITIES = NetworkCapabilitiesUtils.packBitList( // TRUSTED can change when user explicitly connects to an untrusted network in Settings. // http://b/18206275 - (1 << NET_CAPABILITY_TRUSTED) - | (1 << NET_CAPABILITY_VALIDATED) - | (1 << NET_CAPABILITY_CAPTIVE_PORTAL) - | (1 << NET_CAPABILITY_NOT_ROAMING) - | (1 << NET_CAPABILITY_FOREGROUND) - | (1 << NET_CAPABILITY_NOT_CONGESTED) - | (1 << NET_CAPABILITY_NOT_SUSPENDED) - | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY) - | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED) - | (1 << NET_CAPABILITY_NOT_VCN_MANAGED) + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_VALIDATED, + NET_CAPABILITY_CAPTIVE_PORTAL, + NET_CAPABILITY_NOT_ROAMING, + NET_CAPABILITY_FOREGROUND, + NET_CAPABILITY_NOT_CONGESTED, + NET_CAPABILITY_NOT_SUSPENDED, + NET_CAPABILITY_PARTIAL_CONNECTIVITY, + NET_CAPABILITY_TEMPORARILY_NOT_METERED, + NET_CAPABILITY_NOT_VCN_MANAGED, // The value of NET_CAPABILITY_HEAD_UNIT is 32, which cannot use int to do bit shift, // otherwise there will be an overflow. Use long to do bit shift instead. - | (1L << NET_CAPABILITY_HEAD_UNIT); + NET_CAPABILITY_HEAD_UNIT); /** * Network capabilities that are not allowed in NetworkRequests. This exists because the @@ -622,25 +650,26 @@ // in an infinite loop about these. private static final long NON_REQUESTABLE_CAPABILITIES = MUTABLE_CAPABILITIES - & ~(1 << NET_CAPABILITY_TRUSTED) - & ~(1 << NET_CAPABILITY_NOT_VCN_MANAGED); + & ~(1L << NET_CAPABILITY_TRUSTED) + & ~(1L << NET_CAPABILITY_NOT_VCN_MANAGED); /** * Capabilities that are set by default when the object is constructed. */ - private static final long DEFAULT_CAPABILITIES = - (1 << NET_CAPABILITY_NOT_RESTRICTED) - | (1 << NET_CAPABILITY_TRUSTED) - | (1 << NET_CAPABILITY_NOT_VPN); + private static final long DEFAULT_CAPABILITIES = NetworkCapabilitiesUtils.packBitList( + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_NOT_VPN); /** * Capabilities that are managed by ConnectivityService. */ private static final long CONNECTIVITY_MANAGED_CAPABILITIES = - (1 << NET_CAPABILITY_VALIDATED) - | (1 << NET_CAPABILITY_CAPTIVE_PORTAL) - | (1 << NET_CAPABILITY_FOREGROUND) - | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY); + NetworkCapabilitiesUtils.packBitList( + NET_CAPABILITY_VALIDATED, + NET_CAPABILITY_CAPTIVE_PORTAL, + NET_CAPABILITY_FOREGROUND, + NET_CAPABILITY_PARTIAL_CONNECTIVITY); /** * Capabilities that are allowed for test networks. This list must be set so that it is safe @@ -649,14 +678,15 @@ * INTERNET, IMS, SUPL, etc. */ private static final long TEST_NETWORKS_ALLOWED_CAPABILITIES = - (1 << NET_CAPABILITY_NOT_METERED) - | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED) - | (1 << NET_CAPABILITY_NOT_RESTRICTED) - | (1 << NET_CAPABILITY_NOT_VPN) - | (1 << NET_CAPABILITY_NOT_ROAMING) - | (1 << NET_CAPABILITY_NOT_CONGESTED) - | (1 << NET_CAPABILITY_NOT_SUSPENDED) - | (1 << NET_CAPABILITY_NOT_VCN_MANAGED); + NetworkCapabilitiesUtils.packBitList( + NET_CAPABILITY_NOT_METERED, + NET_CAPABILITY_TEMPORARILY_NOT_METERED, + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_NOT_ROAMING, + NET_CAPABILITY_NOT_CONGESTED, + NET_CAPABILITY_NOT_SUSPENDED, + NET_CAPABILITY_NOT_VCN_MANAGED); /** * Adds the given capability to this {@code NetworkCapability} instance. @@ -784,34 +814,34 @@ } /** - * Adds the given enterprise capability sub level to this {@code NetworkCapability} instance. - * Note that when searching for a network to satisfy a request, all capabilities sub level + * Adds the given enterprise capability identifier to this {@code NetworkCapability} instance. + * Note that when searching for a network to satisfy a request, all capabilities identifier * requested must be satisfied. * - * @param enterpriseCapabilitySubLevel the enterprise capability sub level to be added. + * @param enterpriseId the enterprise capability identifier to be added. * @return This NetworkCapabilities instance, to facilitate chaining. * @hide */ - private @NonNull NetworkCapabilities addEnterpriseCapabilitySubLevel( - @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel); - mEnterpriseCapabilitySubLevel |= 1 << enterpriseCapabilitySubLevel; + public @NonNull NetworkCapabilities addEnterpriseId( + @EnterpriseId int enterpriseId) { + checkValidEnterpriseId(enterpriseId); + mEnterpriseId |= 1 << enterpriseId; return this; } /** - * Removes (if found) the given enterprise capability sublevel from this - * {@code NetworkCapability} instance that were added via addEnterpriseCapabilitySubLevel(int) + * Removes (if found) the given enterprise capability identifier from this + * {@code NetworkCapability} instance that were added via addEnterpriseId(int) * - * @param enterpriseCapabilitySubLevel the enterprise capability sublevel to be removed. + * @param enterpriseId the enterprise capability identifier to be removed. * @return This NetworkCapabilities instance, to facilitate chaining. * @hide */ - private @NonNull NetworkCapabilities removeEnterpriseCapabilitySubLevel( - @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel); - final int mask = ~(1 << enterpriseCapabilitySubLevel); - mEnterpriseCapabilitySubLevel &= mask; + private @NonNull NetworkCapabilities removeEnterpriseId( + @EnterpriseId int enterpriseId) { + checkValidEnterpriseId(enterpriseId); + final int mask = ~(1 << enterpriseId); + mEnterpriseId &= mask; return this; } @@ -915,16 +945,19 @@ return null; } - private boolean equalsEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) { - return nc.mEnterpriseCapabilitySubLevel == this.mEnterpriseCapabilitySubLevel; + private boolean equalsEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) { + return nc.mEnterpriseId == this.mEnterpriseId; } - private boolean satisfiedByEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) { - final int requestedEnterpriseCapabilitiesSubLevel = mEnterpriseCapabilitySubLevel; - final int providedEnterpriseCapabailitiesSubLevel = nc.mEnterpriseCapabilitySubLevel; + private boolean satisfiedByEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) { + final int requestedEnterpriseCapabilitiesId = mEnterpriseId; + final int providedEnterpriseCapabailitiesId = nc.mEnterpriseId; - if ((providedEnterpriseCapabailitiesSubLevel & requestedEnterpriseCapabilitiesSubLevel) - == requestedEnterpriseCapabilitiesSubLevel) { + if ((providedEnterpriseCapabailitiesId & requestedEnterpriseCapabilitiesId) + == requestedEnterpriseCapabilitiesId) { + return true; + } else if (providedEnterpriseCapabailitiesId == 0 + && (requestedEnterpriseCapabilitiesId == (1L << NET_ENTERPRISE_ID_1))) { return true; } else { return false; @@ -996,6 +1029,7 @@ final int[] originalAdministratorUids = getAdministratorUids(); final TransportInfo originalTransportInfo = getTransportInfo(); final Set<Integer> originalSubIds = getSubscriptionIds(); + final Set<Integer> originalAccessUids = new ArraySet<>(mAccessUids); clearAll(); if (0 != (originalCapabilities & (1 << NET_CAPABILITY_NOT_RESTRICTED))) { // If the test network is not restricted, then it is only allowed to declare some @@ -1015,6 +1049,7 @@ mNetworkSpecifier = originalSpecifier; mSignalStrength = originalSignalStrength; mTransportInfo = originalTransportInfo; + mAccessUids.addAll(originalAccessUids); // Only retain the owner and administrator UIDs if they match the app registering the remote // caller that registered the network. @@ -1123,12 +1158,13 @@ /** * Allowed transports on an unrestricted test network (in addition to TRANSPORT_TEST). */ - private static final int UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS = - 1 << TRANSPORT_TEST - // Test ethernet networks can be created with EthernetManager#setIncludeTestInterfaces - | 1 << TRANSPORT_ETHERNET - // Test VPN networks can be created but their UID ranges must be empty. - | 1 << TRANSPORT_VPN; + private static final long UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS = + NetworkCapabilitiesUtils.packBitList( + TRANSPORT_TEST, + // Test eth networks are created with EthernetManager#setIncludeTestInterfaces + TRANSPORT_ETHERNET, + // Test VPN networks can be created but their UID ranges must be empty. + TRANSPORT_VPN); /** * Adds the given transport type to this {@code NetworkCapability} instance. @@ -1579,8 +1615,8 @@ * <p> * Note that when used to register a network callback, this specifies the minimum acceptable * signal strength. When received as the state of an existing network it specifies the current - * value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means no value when received and has no - * effect when requesting a callback. + * value. A value of {@link #SIGNAL_STRENGTH_UNSPECIFIED} means no value when received and has + * no effect when requesting a callback. * * @param signalStrength the bearer-specific signal strength. * @hide @@ -1780,6 +1816,86 @@ } /** + * List of UIDs that can always access this network. + * <p> + * UIDs in this list have access to this network, even if the network doesn't have the + * {@link #NET_CAPABILITY_NOT_RESTRICTED} capability and the UID does not hold the + * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission. + * This is only useful for restricted networks. For non-restricted networks it has no effect. + * <p> + * This is disallowed in {@link NetworkRequest}, and can only be set by network agents. Network + * agents also have restrictions on how they can set these ; they can only back a public + * Android API. As such, Ethernet agents can set this when backing the per-UID access API, and + * Telephony can set exactly one UID which has to match the manager app for the associated + * subscription. Failure to comply with these rules will see this member cleared. + * <p> + * This member is never null, but can be empty. + * @hide + */ + @NonNull + private final ArraySet<Integer> mAccessUids = new ArraySet<>(); + + /** + * Set the list of UIDs that can always access this network. + * @param uids + * @hide + */ + public void setAccessUids(@NonNull final Set<Integer> uids) { + // could happen with nc.set(nc), cheaper than always making a defensive copy + if (uids == mAccessUids) return; + + Objects.requireNonNull(uids); + mAccessUids.clear(); + mAccessUids.addAll(uids); + } + + /** + * The list of UIDs that can always access this network. + * + * The UIDs in this list can always access this network, even if it is restricted and + * the UID doesn't hold the USE_RESTRICTED_NETWORKS permission. This is defined by the + * network agent in charge of creating the network. + * + * The UIDs are only visible to network factories and the system server, since the system + * server makes sure to redact them before sending a NetworkCapabilities to a process + * that doesn't hold the permission. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public @NonNull Set<Integer> getAccessUids() { + return new ArraySet<>(mAccessUids); + } + + /** @hide */ + // For internal clients that know what they are doing and need to avoid the performance hit + // of the defensive copy. + public @NonNull ArraySet<Integer> getAccessUidsNoCopy() { + return mAccessUids; + } + + /** + * Test whether this UID has special permission to access this network, as per mAccessUids. + * @hide + */ + public boolean isAccessUid(int uid) { + return mAccessUids.contains(uid); + } + + /** + * @return whether any UID is in the list of access UIDs + * @hide + */ + public boolean hasAccessUids() { + return !mAccessUids.isEmpty(); + } + + private boolean equalsAccessUids(@NonNull NetworkCapabilities other) { + return mAccessUids.equals(other.mAccessUids); + } + + /** * The SSID of the network, or null if not applicable or unknown. * <p> * This is filled in by wifi code. @@ -1836,7 +1952,7 @@ && satisfiedByTransportTypes(nc) && (onlyImmutable || satisfiedByLinkBandwidths(nc)) && satisfiedBySpecifier(nc) - && satisfiedByEnterpriseCapabilitiesSubLevel(nc) + && satisfiedByEnterpriseCapabilitiesId(nc) && (onlyImmutable || satisfiedBySignalStrength(nc)) && (onlyImmutable || satisfiedByUids(nc)) && (onlyImmutable || satisfiedBySSID(nc)) @@ -1933,6 +2049,7 @@ && equalsSpecifier(that) && equalsTransportInfo(that) && equalsUids(that) + && equalsAccessUids(that) && equalsSSID(that) && equalsOwnerUid(that) && equalsPrivateDnsBroken(that) @@ -1940,7 +2057,7 @@ && equalsAdministratorUids(that) && equalsSubscriptionIds(that) && equalsUnderlyingNetworks(that) - && equalsEnterpriseCapabilitiesSubLevel(that); + && equalsEnterpriseCapabilitiesId(that); } @Override @@ -1957,15 +2074,16 @@ + mSignalStrength * 29 + mOwnerUid * 31 + Objects.hashCode(mUids) * 37 - + Objects.hashCode(mSSID) * 41 - + Objects.hashCode(mTransportInfo) * 43 - + Objects.hashCode(mPrivateDnsBroken) * 47 - + Objects.hashCode(mRequestorUid) * 53 - + Objects.hashCode(mRequestorPackageName) * 59 - + Arrays.hashCode(mAdministratorUids) * 61 - + Objects.hashCode(mSubIds) * 67 - + Objects.hashCode(mUnderlyingNetworks) * 71 - + mEnterpriseCapabilitySubLevel * 73; + + Objects.hashCode(mAccessUids) * 41 + + Objects.hashCode(mSSID) * 43 + + Objects.hashCode(mTransportInfo) * 47 + + Objects.hashCode(mPrivateDnsBroken) * 53 + + Objects.hashCode(mRequestorUid) * 59 + + Objects.hashCode(mRequestorPackageName) * 61 + + Arrays.hashCode(mAdministratorUids) * 67 + + Objects.hashCode(mSubIds) * 71 + + Objects.hashCode(mUnderlyingNetworks) * 73 + + mEnterpriseId * 79; } @Override @@ -1993,6 +2111,7 @@ dest.writeParcelable((Parcelable) mTransportInfo, flags); dest.writeInt(mSignalStrength); writeParcelableArraySet(dest, mUids, flags); + dest.writeIntArray(CollectionUtils.toIntArray(mAccessUids)); dest.writeString(mSSID); dest.writeBoolean(mPrivateDnsBroken); dest.writeIntArray(getAdministratorUids()); @@ -2001,11 +2120,11 @@ dest.writeString(mRequestorPackageName); dest.writeIntArray(CollectionUtils.toIntArray(mSubIds)); dest.writeTypedList(mUnderlyingNetworks); - dest.writeInt(mEnterpriseCapabilitySubLevel); + dest.writeInt(mEnterpriseId); } public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR = - new Creator<NetworkCapabilities>() { + new Creator<>() { @Override public NetworkCapabilities createFromParcel(Parcel in) { NetworkCapabilities netCap = new NetworkCapabilities(); @@ -2019,6 +2138,11 @@ netCap.mTransportInfo = in.readParcelable(null); netCap.mSignalStrength = in.readInt(); netCap.mUids = readParcelableArraySet(in, null /* ClassLoader, null for default */); + final int[] accessUids = in.createIntArray(); + netCap.mAccessUids.ensureCapacity(accessUids.length); + for (int uid : accessUids) { + netCap.mAccessUids.add(uid); + } netCap.mSSID = in.readString(); netCap.mPrivateDnsBroken = in.readBoolean(); netCap.setAdministratorUids(in.createIntArray()); @@ -2031,7 +2155,7 @@ netCap.mSubIds.add(subIdInts[i]); } netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR)); - netCap.mEnterpriseCapabilitySubLevel = in.readInt(); + netCap.mEnterpriseId = in.readInt(); return netCap; } @Override @@ -2095,6 +2219,11 @@ sb.append(" Uids: <").append(mUids).append(">"); } } + + if (hasAccessUids()) { + sb.append(" AccessUids: <").append(mAccessUids).append(">"); + } + if (mOwnerUid != Process.INVALID_UID) { sb.append(" OwnerUid: ").append(mOwnerUid); } @@ -2123,10 +2252,10 @@ sb.append(" SubscriptionIds: ").append(mSubIds); } - if (0 != mEnterpriseCapabilitySubLevel) { - sb.append(" EnterpriseCapabilitySublevel: "); - appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseCapabilitySubLevel, - NetworkCapabilities::enterpriseCapabilitySublevelNameOf, "&"); + if (0 != mEnterpriseId) { + sb.append(" EnterpriseId: "); + appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseId, + NetworkCapabilities::enterpriseIdNameOf, "&"); } sb.append(" UnderlyingNetworks: "); @@ -2224,11 +2353,13 @@ case NET_CAPABILITY_BIP: return "BIP"; case NET_CAPABILITY_HEAD_UNIT: return "HEAD_UNIT"; case NET_CAPABILITY_MMTEL: return "MMTEL"; + case NET_CAPABILITY_PRIORITIZE_LATENCY: return "PRIORITIZE_LATENCY"; + case NET_CAPABILITY_PRIORITIZE_BANDWIDTH: return "PRIORITIZE_BANDWIDTH"; default: return Integer.toString(capability); } } - private static @NonNull String enterpriseCapabilitySublevelNameOf( + private static @NonNull String enterpriseIdNameOf( @NetCapability int capability) { return Integer.toString(capability); } @@ -2269,21 +2400,21 @@ private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) { if (!isValidCapability(capability)) { - throw new IllegalArgumentException("NetworkCapability " + capability + "out of range"); + throw new IllegalArgumentException("NetworkCapability " + capability + " out of range"); } } - private static boolean isValidEnterpriseCapabilitySubLevel( - @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - return enterpriseCapabilitySubLevel >= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 - && enterpriseCapabilitySubLevel <= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5; + private static boolean isValidEnterpriseId( + @NetworkCapabilities.EnterpriseId int enterpriseId) { + return enterpriseId >= NET_ENTERPRISE_ID_1 + && enterpriseId <= NET_ENTERPRISE_ID_5; } - private static void checkValidEnterpriseCapabilitySublevel( - @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - if (!isValidEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel)) { - throw new IllegalArgumentException("enterprise capability sublevel " - + enterpriseCapabilitySubLevel + " is out of range"); + private static void checkValidEnterpriseId( + @NetworkCapabilities.EnterpriseId int enterpriseId) { + if (!isValidEnterpriseId(enterpriseId)) { + throw new IllegalArgumentException("enterprise capability identifier " + + enterpriseId + " is out of range"); } } @@ -2613,33 +2744,33 @@ } /** - * Adds the given enterprise capability sub level. - * Note that when searching for a network to satisfy a request, all capabilities sub level - * requested must be satisfied. Enterprise capability sub-level is applicable only + * Adds the given enterprise capability identifier. + * Note that when searching for a network to satisfy a request, all capabilities identifier + * requested must be satisfied. Enterprise capability identifier is applicable only * for NET_CAPABILITY_ENTERPRISE capability * - * @param enterpriseCapabilitySubLevel enterprise capability sub-level. + * @param enterpriseId enterprise capability identifier. * * @return this builder */ @NonNull - public Builder addEnterpriseCapabilitySubLevel( - @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - mCaps.addEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel); + public Builder addEnterpriseId( + @EnterpriseId int enterpriseId) { + mCaps.addEnterpriseId(enterpriseId); return this; } /** - * Removes the given enterprise capability sub level. Enterprise capability sub-level is + * Removes the given enterprise capability identifier. Enterprise capability identifier is * applicable only for NET_CAPABILITY_ENTERPRISE capability * - * @param enterpriseCapabilitySubLevel the enterprise capability subLevel + * @param enterpriseId the enterprise capability identifier * @return this builder */ @NonNull - public Builder removeEnterpriseCapabilitySubLevel( - @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) { - mCaps.removeEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel); + public Builder removeEnterpriseId( + @EnterpriseId int enterpriseId) { + mCaps.removeEnterpriseId(enterpriseId); return this; } @@ -2878,6 +3009,44 @@ } /** + * Set a list of UIDs that can always access this network + * <p> + * Provide a list of UIDs that can access this network even if the network doesn't have the + * {@link #NET_CAPABILITY_NOT_RESTRICTED} capability and the UID does not hold the + * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission. + * <p> + * This is disallowed in {@link NetworkRequest}, and can only be set by + * {@link NetworkAgent}s, who hold the + * {@link android.Manifest.permission.NETWORK_FACTORY} permission. + * Network agents also have restrictions on how they can set these ; they can only back + * a public Android API. As such, Ethernet agents can set this when backing the per-UID + * access API, and Telephony can set exactly one UID which has to match the manager app for + * the associated subscription. Failure to comply with these rules will see this member + * cleared. + * <p> + * These UIDs are only visible to network factories and the system server, since the system + * server makes sure to redact them before sending a {@link NetworkCapabilities} instance + * to a process that doesn't hold the {@link android.Manifest.permission.NETWORK_FACTORY} + * permission. + * <p> + * This list cannot be null, but it can be empty to mean that no UID without the + * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission + * gets to access this network. + * + * @param uids the list of UIDs that can always access this network + * @return this builder + * @hide + */ + @NonNull + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setAccessUids(@NonNull Set<Integer> uids) { + Objects.requireNonNull(uids); + mCaps.setAccessUids(uids); + return this; + } + + /** * Set the underlying networks of this network. * * @param networks The underlying networks of this network. @@ -2902,12 +3071,12 @@ } } - if ((mCaps.getEnterpriseCapabilitySubLevels().length != 0) + if ((mCaps.getEnterpriseIds().length != 0) && !mCaps.hasCapability(NET_CAPABILITY_ENTERPRISE)) { - throw new IllegalStateException("Enterprise capability sublevel is applicable only" - + " with ENTERPRISE capability."); + throw new IllegalStateException("Enterprise capability identifier is applicable" + + " only with ENTERPRISE capability."); } return new NetworkCapabilities(mCaps); } } -} +} \ No newline at end of file
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java index afc76d6..b7a6076 100644 --- a/framework/src/android/net/NetworkRequest.java +++ b/framework/src/android/net/NetworkRequest.java
@@ -725,6 +725,33 @@ } /** + * Get the enteprise identifiers. + * + * Get all the enterprise identifiers set on this {@code NetworkCapability} + * @return array of all the enterprise identifiers. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public @NonNull @NetworkCapabilities.EnterpriseId int[] getEnterpriseIds() { + // No need to make a defensive copy here as NC#getCapabilities() already returns + // a new array. + return networkCapabilities.getEnterpriseIds(); + } + + /** + * Tests for the presence of an enterprise identifier on this instance. + * + * @param enterpriseId the enterprise capability identifier to be tested for. + * @return {@code true} if set on this instance. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public boolean hasEnterpriseId( + @NetworkCapabilities.EnterpriseId int enterpriseId) { + return networkCapabilities.hasEnterpriseId(enterpriseId); + } + + /** * Gets all the forbidden capabilities set on this {@code NetworkRequest} instance. * * @return an array of forbidden capability values for this instance.
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java index d580209..f43acce 100644 --- a/framework/src/android/net/ProfileNetworkPreference.java +++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -18,13 +18,20 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.net.ConnectivityManager.ProfileNetworkPreferencePolicy; import android.os.Parcel; import android.os.Parcelable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + /** * Network preferences to be set for the user profile * {@link ProfileNetworkPreferencePolicy}. @@ -33,23 +40,91 @@ @SystemApi(client = MODULE_LIBRARIES) public final class ProfileNetworkPreference implements Parcelable { private final @ProfileNetworkPreferencePolicy int mPreference; + private final @NetworkCapabilities.EnterpriseId int mPreferenceEnterpriseId; + private final List<Integer> mIncludedUids; + private final List<Integer> mExcludedUids; - private ProfileNetworkPreference(int preference) { + private ProfileNetworkPreference(int preference, List<Integer> includedUids, + List<Integer> excludedUids, + @NetworkCapabilities.EnterpriseId int preferenceEnterpriseId) { mPreference = preference; + mPreferenceEnterpriseId = preferenceEnterpriseId; + if (includedUids != null) { + mIncludedUids = new ArrayList<>(includedUids); + } else { + mIncludedUids = new ArrayList<>(); + } + + if (excludedUids != null) { + mExcludedUids = new ArrayList<>(excludedUids); + } else { + mExcludedUids = new ArrayList<>(); + } } private ProfileNetworkPreference(Parcel in) { mPreference = in.readInt(); + mIncludedUids = in.readArrayList(Integer.class.getClassLoader()); + mExcludedUids = in.readArrayList(Integer.class.getClassLoader()); + mPreferenceEnterpriseId = in.readInt(); } public int getPreference() { return mPreference; } + /** + * Get the list of UIDs subject to this preference. + * + * Included UIDs and Excluded UIDs can't both be non-empty. + * if both are empty, it means this request applies to all uids in the user profile. + * if included is not empty, then only included UIDs are applied. + * if excluded is not empty, then it is all uids in the user profile except these UIDs. + * @return List of uids included for the profile preference. + * {@see #getExcludedUids()} + */ + public @NonNull List<Integer> getIncludedUids() { + return new ArrayList<>(mIncludedUids); + } + + /** + * Get the list of UIDS excluded from this preference. + * + * <ul>Included UIDs and Excluded UIDs can't both be non-empty.</ul> + * <ul>If both are empty, it means this request applies to all uids in the user profile.</ul> + * <ul>If included is not empty, then only included UIDs are applied.</ul> + * <ul>If excluded is not empty, then it is all uids in the user profile except these UIDs.</ul> + * @return List of uids not included for the profile preference. + * {@see #getIncludedUids()} + */ + public @NonNull List<Integer> getExcludedUids() { + return new ArrayList<>(mExcludedUids); + } + + /** + * Get preference enterprise identifier. + * + * Preference enterprise identifier will be used to create different network preferences + * within enterprise preference category. + * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to + * NetworkCapabilities.NET_ENTERPRISE_ID_5. + * Preference identifier is not applicable if preference is set as + * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is + * NetworkCapabilities.NET_ENTERPRISE_ID_1. + * @return Preference enterprise identifier. + * + */ + public @NetworkCapabilities.EnterpriseId int getPreferenceEnterpriseId() { + return mPreferenceEnterpriseId; + } + @Override public String toString() { return "ProfileNetworkPreference{" + "mPreference=" + getPreference() + + "mIncludedUids=" + mIncludedUids.toString() + + "mExcludedUids=" + mExcludedUids.toString() + + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId + '}'; } @@ -58,12 +133,18 @@ if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final ProfileNetworkPreference that = (ProfileNetworkPreference) o; - return mPreference == that.mPreference; + return mPreference == that.mPreference + && (Objects.equals(mIncludedUids, that.mIncludedUids)) + && (Objects.equals(mExcludedUids, that.mExcludedUids)) + && mPreferenceEnterpriseId == that.mPreferenceEnterpriseId; } @Override public int hashCode() { - return (mPreference); + return mPreference + + mPreferenceEnterpriseId * 2 + + (Objects.hashCode(mIncludedUids) * 11) + + (Objects.hashCode(mExcludedUids) * 13); } /** @@ -73,6 +154,9 @@ public static final class Builder { private @ProfileNetworkPreferencePolicy int mPreference = PROFILE_NETWORK_PREFERENCE_DEFAULT; + private @NonNull List<Integer> mIncludedUids = new ArrayList<>(); + private @NonNull List<Integer> mExcludedUids = new ArrayList<>(); + private int mPreferenceEnterpriseId; /** * Constructs an empty Builder with PROFILE_NETWORK_PREFERENCE_DEFAULT profile preference @@ -91,19 +175,114 @@ mPreference = preference; return this; } + + /** + * This is a list of uids for which profile perefence is set. + * Null would mean that this preference applies to all uids in the profile. + * {@see #setExcludedUids(List<Integer>)} + * Included UIDs and Excluded UIDs can't both be non-empty. + * if both are empty, it means this request applies to all uids in the user profile. + * if included is not empty, then only included UIDs are applied. + * if excluded is not empty, then it is all uids in the user profile except these UIDs. + * @param uids list of uids that are included + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setIncludedUids(@Nullable List<Integer> uids) { + if (uids != null) { + mIncludedUids = new ArrayList<Integer>(uids); + } else { + mIncludedUids = new ArrayList<Integer>(); + } + return this; + } + + + /** + * This is a list of uids that are excluded for the profile perefence. + * {@see #setIncludedUids(List<Integer>)} + * Included UIDs and Excluded UIDs can't both be non-empty. + * if both are empty, it means this request applies to all uids in the user profile. + * if included is not empty, then only included UIDs are applied. + * if excluded is not empty, then it is all uids in the user profile except these UIDs. + * @param uids list of uids that are not included + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setExcludedUids(@Nullable List<Integer> uids) { + if (uids != null) { + mExcludedUids = new ArrayList<Integer>(uids); + } else { + mExcludedUids = new ArrayList<Integer>(); + } + return this; + } + + /** + * Check if given preference enterprise identifier is valid + * + * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to + * NetworkCapabilities.NET_ENTERPRISE_ID_5. + * @return True if valid else false + * @hide + */ + private boolean isEnterpriseIdentifierValid( + @NetworkCapabilities.EnterpriseId int identifier) { + if ((identifier >= NET_ENTERPRISE_ID_1) + && (identifier <= NET_ENTERPRISE_ID_5)) { + return true; + } + return false; + } + /** * Returns an instance of {@link ProfileNetworkPreference} created from the * fields set on this builder. */ @NonNull public ProfileNetworkPreference build() { - return new ProfileNetworkPreference(mPreference); + if (mIncludedUids.size() > 0 && mExcludedUids.size() > 0) { + throw new IllegalArgumentException("Both includedUids and excludedUids " + + "cannot be nonempty"); + } + + if (((mPreference != PROFILE_NETWORK_PREFERENCE_DEFAULT) + && (!isEnterpriseIdentifierValid(mPreferenceEnterpriseId))) + || ((mPreference == PROFILE_NETWORK_PREFERENCE_DEFAULT) + && (mPreferenceEnterpriseId != 0))) { + throw new IllegalStateException("Invalid preference enterprise identifier"); + } + return new ProfileNetworkPreference(mPreference, mIncludedUids, + mExcludedUids, mPreferenceEnterpriseId); + } + + /** + * Set the preference enterprise identifier. + * + * Preference enterprise identifier will be used to create different network preferences + * within enterprise preference category. + * Valid values starts from NetworkCapabilities.NET_ENTERPRISE_ID_1 to + * NetworkCapabilities.NET_ENTERPRISE_ID_5. + * Preference identifier is not applicable if preference is set as + * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is + * NetworkCapabilities.NET_ENTERPRISE_ID_1. + * @param preferenceId preference sub level + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setPreferenceEnterpriseId( + @NetworkCapabilities.EnterpriseId int preferenceId) { + mPreferenceEnterpriseId = preferenceId; + return this; } } @Override public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { dest.writeInt(mPreference); + dest.writeList(mIncludedUids); + dest.writeList(mExcludedUids); + dest.writeInt(mPreferenceEnterpriseId); } @Override
diff --git a/framework/src/android/net/StaticIpConfiguration.java b/framework/src/android/net/StaticIpConfiguration.java index 7904f7a..194cffd 100644 --- a/framework/src/android/net/StaticIpConfiguration.java +++ b/framework/src/android/net/StaticIpConfiguration.java
@@ -26,6 +26,7 @@ import com.android.net.module.util.InetAddressUtils; +import java.net.Inet4Address; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; @@ -33,24 +34,7 @@ /** * Class that describes static IP configuration. - * - * <p>This class is different from {@link LinkProperties} because it represents - * configuration intent. The general contract is that if we can represent - * a configuration here, then we should be able to configure it on a network. - * The intent is that it closely match the UI we have for configuring networks. - * - * <p>In contrast, {@link LinkProperties} represents current state. It is much more - * expressive. For example, it supports multiple IP addresses, multiple routes, - * stacked interfaces, and so on. Because LinkProperties is so expressive, - * using it to represent configuration intent as well as current state causes - * problems. For example, we could unknowingly save a configuration that we are - * not in fact capable of applying, or we could save a configuration that the - * UI cannot display, which has the potential for malicious code to hide - * hostile or unexpected configuration from the user. - * - * @hide */ -@SystemApi public final class StaticIpConfiguration implements Parcelable { /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -69,10 +53,14 @@ @Nullable public String domains; + /** @hide */ + @SystemApi public StaticIpConfiguration() { dnsServers = new ArrayList<>(); } + /** @hide */ + @SystemApi public StaticIpConfiguration(@Nullable StaticIpConfiguration source) { this(); if (source != null) { @@ -84,6 +72,8 @@ } } + /** @hide */ + @SystemApi public void clear() { ipAddress = null; gateway = null; @@ -94,7 +84,7 @@ /** * Get the static IP address included in the configuration. */ - public @Nullable LinkAddress getIpAddress() { + public @NonNull LinkAddress getIpAddress() { return ipAddress; } @@ -130,10 +120,15 @@ private String mDomains; /** - * Set the IP address to be included in the configuration; null by default. + * Set the IP address to be included in the configuration. + * * @return The {@link Builder} for chaining. */ - public @NonNull Builder setIpAddress(@Nullable LinkAddress ipAddress) { + public @NonNull Builder setIpAddress(@NonNull LinkAddress ipAddress) { + if (ipAddress != null && !(ipAddress.getAddress() instanceof Inet4Address)) { + throw new IllegalArgumentException( + "Only IPv4 addresses can be used for the IP configuration"); + } mIpAddress = ipAddress; return this; } @@ -143,6 +138,10 @@ * @return The {@link Builder} for chaining. */ public @NonNull Builder setGateway(@Nullable InetAddress gateway) { + if (gateway != null && !(gateway instanceof Inet4Address)) { + throw new IllegalArgumentException( + "Only IPv4 addresses can be used for the gateway configuration"); + } mGateway = gateway; return this; } @@ -153,6 +152,12 @@ */ public @NonNull Builder setDnsServers(@NonNull Iterable<InetAddress> dnsServers) { Objects.requireNonNull(dnsServers); + for (InetAddress inetAddress: dnsServers) { + if (!(inetAddress instanceof Inet4Address)) { + throw new IllegalArgumentException( + "Only IPv4 addresses can be used for the DNS server configuration"); + } + } mDnsServers = dnsServers; return this; } @@ -171,6 +176,8 @@ /** * Create a {@link StaticIpConfiguration} from the parameters in this {@link Builder}. * @return The newly created StaticIpConfiguration. + * @throws IllegalArgumentException if an invalid configuration is attempted, e.g. + * if an IP Address was not configured via {@link #setIpAddress(LinkAddress)}. */ public @NonNull StaticIpConfiguration build() { final StaticIpConfiguration config = new StaticIpConfiguration(); @@ -188,7 +195,9 @@ /** * Add a DNS server to this configuration. + * @hide */ + @SystemApi public void addDnsServer(@NonNull InetAddress server) { dnsServers.add(server); } @@ -197,7 +206,9 @@ * Returns the network routes specified by this object. Will typically include a * directly-connected route for the IP address's local subnet and a default route. * @param iface Interface to include in the routes. + * @hide */ + @SystemApi public @NonNull List<RouteInfo> getRoutes(@Nullable String iface) { List<RouteInfo> routes = new ArrayList<RouteInfo>(3); if (ipAddress != null) { @@ -305,7 +316,7 @@ /** Implement the Parcelable interface */ @Override - public void writeToParcel(Parcel dest, int flags) { + public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelable(ipAddress, flags); InetAddressUtils.parcelInetAddress(dest, gateway, flags); dest.writeInt(dnsServers.size()); @@ -316,7 +327,7 @@ } /** @hide */ - public static StaticIpConfiguration readFromParcel(Parcel in) { + public static @NonNull StaticIpConfiguration readFromParcel(Parcel in) { final StaticIpConfiguration s = new StaticIpConfiguration(); s.ipAddress = in.readParcelable(null); s.gateway = InetAddressUtils.unparcelInetAddress(in);
diff --git a/nearby/Android.bp b/nearby/Android.bp new file mode 100644 index 0000000..baa0740 --- /dev/null +++ b/nearby/Android.bp
@@ -0,0 +1,39 @@ +// +// 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// Empty sources and libraries to avoid merge conflicts with downstream +// branches +// TODO: remove once the Nearby sources are available in this branch +filegroup { + name: "framework-nearby-java-sources", + srcs: [], + visibility: ["//packages/modules/Connectivity:__subpackages__"], +} + + +java_library { + name: "service-nearby", + srcs: [], + sdk_version: "module_current", + min_sdk_version: "30", + apex_available: ["com.android.tethering"], + visibility: ["//packages/modules/Connectivity:__subpackages__"], +}
diff --git a/netd/Android.bp b/netd/Android.bp new file mode 100644 index 0000000..b98a859 --- /dev/null +++ b/netd/Android.bp
@@ -0,0 +1,79 @@ +// +// 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", + ], + shared_libs: [ + "libbase", + "liblog", + "libnetdutils", + ], + 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: + *; +};
diff --git a/service-t/Android.bp b/service-t/Android.bp new file mode 100644 index 0000000..f33be63 --- /dev/null +++ b/service-t/Android.bp
@@ -0,0 +1,58 @@ +// +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// This builds T+ services depending on framework-connectivity-tiramisu +// hidden symbols separately from the S+ services, to ensure that S+ +// services cannot accidentally depend on T+ hidden symbols from +// framework-connectivity-tiramisu. +java_library { + name: "service-connectivity-tiramisu-pre-jarjar", + sdk_version: "system_server_current", + // TODO(b/210962470): Bump this to at least S, and then T. + min_sdk_version: "30", + srcs: [ + "src/**/*.java", + // TODO: This is necessary just for LocalLog, remove after removing NativeDaemonConnector. + ":framework-connectivity-shared-srcs", + ":services.connectivity-tiramisu-updatable-sources", + ], + libs: [ + "framework-annotations-lib", + "framework-connectivity-pre-jarjar", + "framework-connectivity-tiramisu.impl", + "service-connectivity-pre-jarjar", + "unsupportedappusage", + ], + static_libs: [ + // Do not add static_libs here if they are already included in framework-connectivity + // or in service-connectivity. They are not necessary (included via + // service-connectivity-pre-jarjar), and in the case of code that is already in + // framework-connectivity, the classes would be included in the apex twice. + "modules-utils-statemachine", + ], + apex_available: [ + "com.android.tethering", + ], + visibility: [ + "//packages/modules/Connectivity/service", + "//packages/modules/Connectivity/tests:__subpackages__", + ], +}
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp new file mode 100644 index 0000000..d24b14b --- /dev/null +++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -0,0 +1,72 @@ +// +// 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", + "libnetworkstats", + ], + shared_libs: [ + "libbase", + "liblog", + ], +}
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..4974b96d --- /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/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java similarity index 68% rename from service/src/com/android/server/ConnectivityServiceInitializer.java rename to service-t/src/com/android/server/ConnectivityServiceInitializer.java index b1a56ae..23d8bdc 100644 --- a/service/src/com/android/server/ConnectivityServiceInitializer.java +++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -19,6 +19,8 @@ import android.content.Context; import android.util.Log; +import com.android.modules.utils.build.SdkLevel; + /** * Connectivity service initializer for core networking. This is called by system server to create * a new instance of ConnectivityService. @@ -26,12 +28,14 @@ public final class ConnectivityServiceInitializer extends SystemService { private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName(); private final ConnectivityService mConnectivity; + private final NsdService mNsdService; public ConnectivityServiceInitializer(Context context) { super(context); // Load JNI libraries used by ConnectivityService and its dependencies System.loadLibrary("service-connectivity"); mConnectivity = new ConnectivityService(context); + mNsdService = createNsdService(context); } @Override @@ -39,5 +43,20 @@ Log.i(TAG, "Registering " + Context.CONNECTIVITY_SERVICE); publishBinderService(Context.CONNECTIVITY_SERVICE, mConnectivity, /* allowIsolated= */ false); + if (mNsdService != null) { + Log.i(TAG, "Registering " + Context.NSD_SERVICE); + publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false); + } + } + + /** Return NsdService instance or null if current SDK is lower than T */ + private NsdService createNsdService(final Context context) { + if (!SdkLevel.isAtLeastT()) return null; + try { + return NsdService.create(context); + } catch (InterruptedException e) { + Log.d(TAG, "Unable to get NSD service", e); + return null; + } } }
diff --git a/service/Android.bp b/service/Android.bp index 1ec7daa..d600231 100644 --- a/service/Android.bp +++ b/service/Android.bp
@@ -20,9 +20,9 @@ } // The library name match the service-connectivity jarjar rules that put the JNI utils in the -// com.android.connectivity.com.android.net.module.util package. +// android.net.connectivity.com.android.net.module.util package. cc_library_shared { - name: "libcom_android_connectivity_com_android_net_module_util_jni", + name: "libandroid_net_connectivity_com_android_net_module_util_jni", min_sdk_version: "30", cflags: [ "-Wall", @@ -33,7 +33,6 @@ srcs: [ "jni/com_android_net_module_util/onload.cpp", ], - stl: "libc++_static", static_libs: [ "libnet_utils_device_common_bpfjni", ], @@ -56,14 +55,24 @@ "-Wthread-safety", ], srcs: [ + "jni/com_android_server_BpfNetMaps.cpp", + "jni/com_android_server_connectivity_ClatCoordinator.cpp", "jni/com_android_server_TestNetworkService.cpp", "jni/onload.cpp", ], - stl: "libc++_static", header_libs: [ - "libbase_headers", + "bpf_connectivity_headers", + ], + static_libs: [ + "libclat", + "libip_checksum", + "libnetjniutils", + "libtraffic_controller", + "netd_aidl_interface-lateststable-ndk", ], shared_libs: [ + "libbase", + "libnetdutils", "liblog", "libnativehelper", ], @@ -85,29 +94,34 @@ ], libs: [ "framework-annotations-lib", - "framework-connectivity.impl", + "framework-connectivity-pre-jarjar", "framework-tethering.stubs.module_lib", "framework-wifi.stubs.module_lib", "unsupportedappusage", "ServiceConnectivityResources", ], static_libs: [ + // Do not add libs here if they are already included + // in framework-connectivity "dnsresolver_aidl_interface-V9-java", - "modules-utils-build", "modules-utils-shell-command-handler", "net-utils-device-common", "net-utils-device-common-bpf", "net-utils-device-common-netlink", - "net-utils-framework-common", "netd-client", "networkstack-client", "PlatformProperties", "service-connectivity-protos", + "NetworkStackApiStableShims", ], apex_available: [ "com.android.tethering", ], lint: { strict_updatability_linting: true }, + visibility: [ + "//packages/modules/Connectivity/service-t", + "//packages/modules/Connectivity/tests:__subpackages__", + ], } java_library { @@ -132,10 +146,16 @@ sdk_version: "system_server_current", min_sdk_version: "30", installable: true, + // This library combines system server jars that have access to different bootclasspath jars. + // Lower SDK service jars must not depend on higher SDK jars as that would let them + // transitively depend on the wrong bootclasspath jars. Sources also cannot be added here as + // they would transitively depend on bootclasspath jars that may not be available. static_libs: [ "service-connectivity-pre-jarjar", + "service-connectivity-tiramisu-pre-jarjar", + "service-nearby", ], - jarjar_rules: "jarjar-rules.txt", + jarjar_rules: ":connectivity-jarjar-rules", apex_available: [ "com.android.tethering", ], @@ -147,3 +167,11 @@ srcs: ["jarjar-rules.txt"], visibility: ["//packages/modules/Connectivity:__subpackages__"], } + +// TODO: This filegroup temporary exposes for NetworkStats. It should be +// removed right after NetworkStats moves into mainline module. +filegroup { + name: "traffic-controller-utils", + srcs: ["src/com/android/server/BpfNetMaps.java"], + visibility: ["//packages/modules/Connectivity:__subpackages__"], +}
diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml index 9d26c40..ce063a5 100644 --- a/service/ServiceConnectivityResources/res/values-lv/strings.xml +++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml
@@ -23,7 +23,7 @@ <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) --> <skip /> <string name="wifi_no_internet" msgid="1326348603404555475">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string> - <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Pieskarieties, lai skatītu iespējas."</string> + <string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Pieskarieties, lai skatītu opcijas."</string> <string name="mobile_no_internet" msgid="4087718456753201450">"Mobilajā tīklā nav piekļuves internetam."</string> <string name="other_networks_no_internet" msgid="5693932964749676542">"Tīklā nav piekļuves internetam."</string> <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Nevar piekļūt privātam DNS serverim."</string>
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt index e5d1a88..e3b26fd 100644 --- a/service/jarjar-rules.txt +++ b/service/jarjar-rules.txt
@@ -1,6 +1,15 @@ +# Classes in framework-connectivity are restricted to the android.net package. +# This cannot be changed because it is harcoded in ART in S. +# Any missing jarjar rule for framework-connectivity would be caught by the +# build as an unexpected class outside of the android.net package. +rule com.android.net.module.util.** android.net.connectivity.@0 +rule com.android.modules.utils.** android.net.connectivity.@0 +rule android.net.NetworkFactory* android.net.connectivity.@0 + +# From modules-utils-preconditions +rule com.android.internal.util.Preconditions* android.net.connectivity.@0 + rule android.sysprop.** com.android.connectivity.@0 -rule com.android.net.module.util.** com.android.connectivity.@0 -rule com.android.modules.utils.** com.android.connectivity.@0 # internal util classes from framework-connectivity-shared-srcs rule android.util.LocalLog* com.android.connectivity.@0 @@ -23,9 +32,6 @@ rule android.net.ResolverParamsParcel* com.android.connectivity.@0 # Also includes netd event listener AIDL, but this is handled by netd-client rules -# From net-utils-device-common -rule android.net.NetworkFactory* com.android.connectivity.@0 - # From netd-client (newer AIDLs should go to android.net.netd.aidl) rule android.net.netd.aidl.** com.android.connectivity.@0 # Avoid including android.net.INetdEventCallback, used in tests but not part of the module @@ -90,5 +96,12 @@ # From services-connectivity-shared-srcs rule android.net.util.NetworkConstants* com.android.connectivity.@0 +# From modules-utils-statemachine +rule com.android.internal.util.IState* com.android.connectivity.@0 +rule com.android.internal.util.State* com.android.connectivity.@0 + +# From the API shims +rule com.android.networkstack.apishim.** com.android.connectivity.@0 + # Remaining are connectivity sources in com.android.server and com.android.server.connectivity: # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp index 1d17622..2f09e55 100644 --- a/service/jni/com_android_net_module_util/onload.cpp +++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -20,6 +20,7 @@ namespace android { int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); +int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -29,7 +30,10 @@ } if (register_com_android_net_module_util_BpfMap(env, - "com/android/connectivity/com/android/net/module/util/BpfMap") < 0) return JNI_ERR; + "android/net/connectivity/com/android/net/module/util/BpfMap") < 0) return JNI_ERR; + + if (register_com_android_net_module_util_TcUtils(env, + "android/net/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR; return JNI_VERSION_1_6; }
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp new file mode 100644 index 0000000..2aaa4c3 --- /dev/null +++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -0,0 +1,266 @@ +/* + * 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 "TrafficControllerJni" + +#include "TrafficController.h" + +#include <bpf_shared.h> +#include <jni.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedUtfChars.h> +#include <nativehelper/ScopedPrimitiveArray.h> +#include <net/if.h> +#include <vector> + + +using android::net::TrafficController; +using android::netdutils::Status; + +using UidOwnerMatchType::PENALTY_BOX_MATCH; +using UidOwnerMatchType::HAPPY_BOX_MATCH; + +static android::net::TrafficController mTc; + +namespace android { + +static void native_init(JNIEnv* env, jobject clazz) { + Status status = mTc.start(); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } +} + +static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) { + const uint32_t appUids = static_cast<uint32_t>(abs(uid)); + Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOp::IptOpInsert); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) { + const uint32_t appUids = static_cast<uint32_t>(abs(uid)); + Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOp::IptOpDelete); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) { + const uint32_t appUids = static_cast<uint32_t>(abs(uid)); + Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH, + TrafficController::IptOp::IptOpInsert); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) { + const uint32_t appUids = static_cast<uint32_t>(abs(uid)); + Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH, + TrafficController::IptOp::IptOpDelete); + if (!isOk(status)) { + ALOGD("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) { + auto chain = static_cast<ChildChain>(childChain); + int res = mTc.toggleUidOwnerMap(chain, enable); + if (res) { + ALOGE("%s failed, error code = %d", __func__, res); + } + return (jint)res; +} + +static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist, + jintArray jUids) { + const ScopedUtfChars chainNameUtf8(env, name); + if (chainNameUtf8.c_str() == nullptr) { + return -EINVAL; + } + const std::string chainName(chainNameUtf8.c_str()); + + ScopedIntArrayRO uids(env, jUids); + if (uids.get() == nullptr) { + return -EINVAL; + } + + size_t size = uids.size(); + std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]); + int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data); + if (res) { + ALOGE("%s failed, error code = %d", __func__, res); + } + return (jint)res; +} + +static FirewallType getFirewallType(ChildChain chain) { + switch (chain) { + case DOZABLE: + return ALLOWLIST; + case STANDBY: + return DENYLIST; + case POWERSAVE: + return ALLOWLIST; + case RESTRICTED: + return ALLOWLIST; + case NONE: + default: + return DENYLIST; + } +} + +static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid, + jint firewallRule) { + auto chain = static_cast<ChildChain>(childChain); + auto rule = static_cast<FirewallRule>(firewallRule); + FirewallType fType = getFirewallType(chain); + + int res = mTc.changeUidOwnerRule(chain, uid, rule, fType); + if (res) { + ALOGE("%s failed, error code = %d", __func__, res); + } + return (jint)res; +} + +static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName, + jintArray jUids) { + const ScopedUtfChars ifNameUtf8(env, ifName); + if (ifNameUtf8.c_str() == nullptr) { + return -EINVAL; + } + const std::string interfaceName(ifNameUtf8.c_str()); + const int ifIndex = if_nametoindex(interfaceName.c_str()); + + ScopedIntArrayRO uids(env, jUids); + if (uids.get() == nullptr) { + return -EINVAL; + } + + size_t size = uids.size(); + std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]); + Status status = mTc.addUidInterfaceRules(ifIndex, data); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) { + ScopedIntArrayRO uids(env, jUids); + if (uids.get() == nullptr) { + return -EINVAL; + } + + size_t size = uids.size(); + std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]); + Status status = mTc.removeUidInterfaceRules(data); + if (!isOk(status)) { + ALOGE("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) { + Status status = mTc.swapActiveStatsMap(); + if (!isOk(status)) { + ALOGD("%s failed, error code = %d", __func__, status.code()); + } + return (jint)status.code(); +} + +static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission, + jintArray jUids) { + ScopedIntArrayRO uids(env, jUids); + if (uids.get() == nullptr) return; + + size_t size = uids.size(); + static_assert(sizeof(*(uids.get())) == sizeof(uid_t)); + std::vector<uid_t> data ((uid_t *)&uids[0], (uid_t*)&uids[size]); + mTc.setPermissionForUids(permission, data); +} + +static jint native_setCounterSet(JNIEnv* env, jobject clazz, jint setNum, jint uid) { + uid_t callingUid = getuid(); + int res = mTc.setCounterSet(setNum, (uid_t)uid, callingUid); + if (res) { + ALOGE("%s failed, error code = %d", __func__, res); + } + return (jint)res; +} + +static jint native_deleteTagData(JNIEnv* env, jobject clazz, jint tagNum, jint uid) { + uid_t callingUid = getuid(); + int res = mTc.deleteTagData(tagNum, (uid_t)uid, callingUid); + if (res) { + ALOGE("%s failed, error code = %d", __func__, res); + } + return (jint)res; +} + +/* + * JNI registration. + */ +// clang-format off +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"native_init", "()V", + (void*)native_init}, + {"native_addNaughtyApp", "(I)I", + (void*)native_addNaughtyApp}, + {"native_removeNaughtyApp", "(I)I", + (void*)native_removeNaughtyApp}, + {"native_addNiceApp", "(I)I", + (void*)native_addNiceApp}, + {"native_removeNiceApp", "(I)I", + (void*)native_removeNiceApp}, + {"native_setChildChain", "(IZ)I", + (void*)native_setChildChain}, + {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I", + (void*)native_replaceUidChain}, + {"native_setUidRule", "(III)I", + (void*)native_setUidRule}, + {"native_addUidInterfaceRules", "(Ljava/lang/String;[I)I", + (void*)native_addUidInterfaceRules}, + {"native_removeUidInterfaceRules", "([I)I", + (void*)native_removeUidInterfaceRules}, + {"native_swapActiveStatsMap", "()I", + (void*)native_swapActiveStatsMap}, + {"native_setPermissionForUids", "(I[I)V", + (void*)native_setPermissionForUids}, + {"native_setCounterSet", "(II)I", + (void*)native_setCounterSet}, + {"native_deleteTagData", "(II)I", + (void*)native_deleteTagData}, +}; +// clang-format on + +int register_com_android_server_BpfNetMaps(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/server/BpfNetMaps", + gMethods, NELEM(gMethods)); +} + +}; // namespace android
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp index 1a0de32..4efd0e1 100644 --- a/service/jni/com_android_server_TestNetworkService.cpp +++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -98,7 +98,7 @@ {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create}, }; -int register_android_server_TestNetworkService(JNIEnv* env) { +int register_com_android_server_TestNetworkService(JNIEnv* env) { return jniRegisterNativeMethods(env, "com/android/server/TestNetworkService", gMethods, NELEM(gMethods)); }
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp new file mode 100644 index 0000000..ee512ec --- /dev/null +++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -0,0 +1,527 @@ +/* + * 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 "jniClatCoordinator" + +#include <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <linux/if_tun.h> +#include <linux/ioctl.h> +#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> + +#include "libclat/bpfhelper.h" +#include "libclat/clatutils.h" +#include "nativehelper/scoped_utf_chars.h" + +// 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)); +} + +jstring com_android_server_connectivity_ClatCoordinator_selectIpv4Address(JNIEnv* env, + jobject clazz, + jstring v4addr, + jint prefixlen) { + ScopedUtfChars address(env, v4addr); + in_addr ip; + if (inet_pton(AF_INET, address.c_str(), &ip) != 1) { + throwIOException(env, "invalid address", EINVAL); + return nullptr; + } + + // Pick an IPv4 address. + // TODO: this picks the address based on other addresses that are assigned to interfaces, but + // the address is only actually assigned to an interface once clatd starts up. So we could end + // up with two clatd instances with the same IPv4 address. + // Stop doing this and instead pick a free one from the kV4Addr pool. + in_addr v4 = {net::clat::selectIpv4Address(ip, prefixlen)}; + if (v4.s_addr == INADDR_NONE) { + jniThrowExceptionFmt(env, "java/io/IOException", "No free IPv4 address in %s/%d", + address.c_str(), prefixlen); + return nullptr; + } + + char addrstr[INET_ADDRSTRLEN]; + if (!inet_ntop(AF_INET, (void*)&v4, addrstr, sizeof(addrstr))) { + throwIOException(env, "invalid address", EADDRNOTAVAIL); + return nullptr; + } + return env->NewStringUTF(addrstr); +} + +// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix. +jstring com_android_server_connectivity_ClatCoordinator_generateIpv6Address( + JNIEnv* env, jobject clazz, jstring ifaceStr, jstring v4Str, jstring prefix64Str) { + ScopedUtfChars iface(env, ifaceStr); + ScopedUtfChars addr4(env, v4Str); + ScopedUtfChars prefix64(env, prefix64Str); + + if (iface.c_str() == nullptr) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid null interface name"); + return nullptr; + } + + in_addr v4; + if (inet_pton(AF_INET, addr4.c_str(), &v4) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid clat v4 address %s", + addr4.c_str()); + return nullptr; + } + + in6_addr nat64Prefix; + if (inet_pton(AF_INET6, prefix64.c_str(), &nat64Prefix) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid prefix %s", prefix64.c_str()); + return nullptr; + } + + in6_addr v6; + if (net::clat::generateIpv6Address(iface.c_str(), v4, nat64Prefix, &v6)) { + jniThrowExceptionFmt(env, "java/io/IOException", + "Unable to find global source address on %s for %s", iface.c_str(), + prefix64.c_str()); + return nullptr; + } + + char addrstr[INET6_ADDRSTRLEN]; + if (!inet_ntop(AF_INET6, (void*)&v6, addrstr, sizeof(addrstr))) { + throwIOException(env, "invalid address", EADDRNOTAVAIL); + return nullptr; + } + return env->NewStringUTF(addrstr); +} + +static jint com_android_server_connectivity_ClatCoordinator_createTunInterface(JNIEnv* env, + jobject clazz, + jstring tuniface) { + ScopedUtfChars v4interface(env, tuniface); + + // open the tun device in non blocking mode as required by clatd + jint fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)", + strerror(errno)); + return -1; + } + + struct ifreq ifr = { + .ifr_flags = IFF_TUN, + }; + strlcpy(ifr.ifr_name, v4interface.c_str(), sizeof(ifr.ifr_name)); + + if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr))) { + close(fd); + jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)", + strerror(errno)); + return -1; + } + + return fd; +} + +static jint com_android_server_connectivity_ClatCoordinator_detectMtu(JNIEnv* env, jobject clazz, + jstring platSubnet, + jint plat_suffix, jint mark) { + ScopedUtfChars platSubnetStr(env, platSubnet); + + in6_addr plat_subnet; + if (inet_pton(AF_INET6, platSubnetStr.c_str(), &plat_subnet) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid plat prefix address %s", + platSubnetStr.c_str()); + return -1; + } + + int ret = net::clat::detect_mtu(&plat_subnet, plat_suffix, mark); + if (ret < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "detect mtu failed: %s", strerror(-ret)); + return -1; + } + + return ret; +} + +static jint com_android_server_connectivity_ClatCoordinator_openPacketSocket(JNIEnv* env, + jobject clazz) { + // Will eventually be bound to htons(ETH_P_IPV6) protocol, + // but only after appropriate bpf filter is attached. + int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + throwIOException(env, "packet socket failed", errno); + return -1; + } + return sock; +} + +static jint com_android_server_connectivity_ClatCoordinator_openRawSocket6(JNIEnv* env, + jobject clazz, + jint mark) { + int sock = socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_RAW); + if (sock < 0) { + throwIOException(env, "raw socket failed", errno); + return -1; + } + + // TODO: check the mark validation + if (mark != MARK_UNSET && setsockopt(sock, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)) < 0) { + throwIOException(env, "could not set mark on raw socket", errno); + close(sock); + return -1; + } + + return sock; +} + +static void com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt( + JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) { + int sock = netjniutils::GetNativeFileDescriptor(env, javaFd); + if (sock < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + + ScopedUtfChars addrStr(env, addr6); + + in6_addr addr; + if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s", + addrStr.c_str()); + return; + } + + struct ipv6_mreq mreq = {addr, ifindex}; + int ret = setsockopt(sock, SOL_IPV6, IPV6_JOIN_ANYCAST, &mreq, sizeof(mreq)); + if (ret) { + jniThrowExceptionFmt(env, "java/io/IOException", "setsockopt IPV6_JOIN_ANYCAST failed: %s", + strerror(errno)); + return; + } +} + +static void com_android_server_connectivity_ClatCoordinator_configurePacketSocket( + JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) { + ScopedUtfChars addrStr(env, addr6); + + int sock = netjniutils::GetNativeFileDescriptor(env, javaFd); + if (sock < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + + in6_addr addr; + if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s", + addrStr.c_str()); + return; + } + + int ret = net::clat::configure_packet_socket(sock, &addr, ifindex); + if (ret < 0) { + throwIOException(env, "configure packet socket failed", -ret); + return; + } +} + +int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4, + const std::string& v6, net::clat::ClatdTracker* output) { + strlcpy(output->iface, iface.c_str(), sizeof(output->iface)); + output->ifIndex = if_nametoindex(iface.c_str()); + if (output->ifIndex == 0) { + ALOGE("interface %s not found", output->iface); + return -1; + } + + unsigned len = snprintf(output->v4iface, sizeof(output->v4iface), + "%s%s", DEVICEPREFIX, iface.c_str()); + if (len >= sizeof(output->v4iface)) { + ALOGE("interface name too long '%s'", output->v4iface); + return -1; + } + + output->v4ifIndex = if_nametoindex(output->v4iface); + if (output->v4ifIndex == 0) { + ALOGE("v4-interface %s not found", output->v4iface); + return -1; + } + + if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) { + ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str()); + return -1; + } + + if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) { + ALOGE("Invalid IPv4 address %s", v4.c_str()); + return -1; + } + + if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) { + ALOGE("Invalid source address %s", v6.c_str()); + return -1; + } + + return 0; +} + +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); + ScopedUtfChars pfx96Str(env, pfx96); + ScopedUtfChars v4Str(env, v4); + ScopedUtfChars v6Str(env, v6); + + 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(), + &tracker)) { + net::clat::maybeStartBpf(tracker); + } + } + + return pid; +} + +// Stop clatd process. SIGTERM with timeout first, if fail, SIGKILL. +// See stopProcess() in system/netd/server/NetdConstants.cpp. +// TODO: have a function stopProcess(int pid, const char *name) in common location and call it. +static constexpr int WAITPID_ATTEMPTS = 50; +static constexpr int WAITPID_RETRY_INTERVAL_US = 100000; + +static void stopClatdProcess(int pid) { + int err = kill(pid, SIGTERM); + if (err) { + err = errno; + } + if (err == ESRCH) { + ALOGE("clatd child process %d unexpectedly disappeared", pid); + return; + } + if (err) { + ALOGE("Error killing clatd child process %d: %s", pid, strerror(err)); + } + int status = 0; + int ret = 0; + for (int count = 0; ret == 0 && count < WAITPID_ATTEMPTS; count++) { + usleep(WAITPID_RETRY_INTERVAL_US); + ret = waitpid(pid, &status, WNOHANG); + } + if (ret == 0) { + ALOGE("Failed to SIGTERM clatd pid=%d, try SIGKILL", pid); + // TODO: fix that kill failed or waitpid doesn't return. + kill(pid, SIGKILL); + ret = waitpid(pid, &status, 0); + } + if (ret == -1) { + ALOGE("Error waiting for clatd child process %d: %s", pid, strerror(errno)); + } else { + ALOGD("clatd process %d terminated status=%d", pid, status); + } +} + +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(), + &tracker)) { + net::clat::maybeStopBpf(tracker); + } + } + + stopClatdProcess(pid); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"native_selectIpv4Address", "(Ljava/lang/String;I)Ljava/lang/String;", + (void*)com_android_server_connectivity_ClatCoordinator_selectIpv4Address}, + {"native_generateIpv6Address", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (void*)com_android_server_connectivity_ClatCoordinator_generateIpv6Address}, + {"native_createTunInterface", "(Ljava/lang/String;)I", + (void*)com_android_server_connectivity_ClatCoordinator_createTunInterface}, + {"native_detectMtu", "(Ljava/lang/String;II)I", + (void*)com_android_server_connectivity_ClatCoordinator_detectMtu}, + {"native_openPacketSocket", "()I", + (void*)com_android_server_connectivity_ClatCoordinator_openPacketSocket}, + {"native_openRawSocket6", "(I)I", + (void*)com_android_server_connectivity_ClatCoordinator_openRawSocket6}, + {"native_addAnycastSetsockopt", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V", + (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_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_startClatd}, + {"native_stopClatd", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V", + (void*)com_android_server_connectivity_ClatCoordinator_stopClatd}, +}; + +int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/connectivity/ClatCoordinator", + gMethods, NELEM(gMethods)); +} + +}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp index 0012879..facdad7 100644 --- a/service/jni/onload.cpp +++ b/service/jni/onload.cpp
@@ -19,7 +19,9 @@ namespace android { -int register_android_server_TestNetworkService(JNIEnv* env); +int register_com_android_server_TestNetworkService(JNIEnv* env); +int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env); +int register_com_android_server_BpfNetMaps(JNIEnv* env); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -28,7 +30,15 @@ return JNI_ERR; } - if (register_android_server_TestNetworkService(env) < 0) { + if (register_com_android_server_TestNetworkService(env) < 0) { + return JNI_ERR; + } + + if (register_com_android_server_connectivity_ClatCoordinator(env) < 0) { + return JNI_ERR; + } + + if (register_com_android_server_BpfNetMaps(env) < 0) { return JNI_ERR; }
diff --git a/service/native/Android.bp b/service/native/Android.bp new file mode 100644 index 0000000..cb26bc3 --- /dev/null +++ b/service/native/Android.bp
@@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library { + name: "libtraffic_controller", + defaults: ["netd_defaults"], + srcs: [ + "TrafficController.cpp", + ], + header_libs: [ + "bpf_connectivity_headers", + ], + static_libs: [ + // TrafficController would use the constants of INetd so that add + // netd_aidl_interface-lateststable-ndk. + "netd_aidl_interface-lateststable-ndk", + ], + shared_libs: [ + // TODO: Find a good way to remove libbase. + "libbase", + "libcutils", + "libnetdutils", + "libutils", + "liblog", + ], + export_include_dirs: ["include"], + sanitize: { + cfi: true, + }, + apex_available: [ + "com.android.tethering", + ], + min_sdk_version: "30", +} + +cc_test { + name: "traffic_controller_unit_test", + test_suites: ["general-tests"], + require_root: true, + local_include_dirs: ["include"], + header_libs: [ + "bpf_connectivity_headers", + ], + srcs: [ + "TrafficControllerTest.cpp", + ], + static_libs: [ + "libbase", + "libgmock", + "liblog", + "libnetdutils", + "libtraffic_controller", + "libutils", + "libnetd_updatable", + "netd_aidl_interface-lateststable-ndk", + ], +}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp new file mode 100644 index 0000000..5981906 --- /dev/null +++ b/service/native/TrafficController.cpp
@@ -0,0 +1,914 @@ +/* + * 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 "TrafficController" +#include <inttypes.h> +#include <linux/if_ether.h> +#include <linux/in.h> +#include <linux/inet_diag.h> +#include <linux/netlink.h> +#include <linux/sock_diag.h> +#include <linux/unistd.h> +#include <net/if.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/utsname.h> +#include <sys/wait.h> +#include <map> +#include <mutex> +#include <unordered_set> +#include <vector> + +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#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> + +#include "TrafficController.h" +#include "bpf/BpfMap.h" +#include "netdutils/DumpWriter.h" + +namespace android { +namespace net { + +using base::StringPrintf; +using base::unique_fd; +using bpf::BpfMap; +using bpf::OVERFLOW_COUNTERSET; +using bpf::synchronizeKernelRCU; +using netdutils::DumpWriter; +using netdutils::getIfaceList; +using netdutils::NetlinkListener; +using netdutils::NetlinkListenerInterface; +using netdutils::ScopedIndent; +using netdutils::Slice; +using netdutils::sSyscalls; +using netdutils::Status; +using netdutils::statusFromErrno; +using netdutils::StatusOr; +using netdutils::status::ok; + +constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY; +constexpr int kSockDiagDoneMsgType = NLMSG_DONE; + +const char* TrafficController::LOCAL_DOZABLE = "fw_dozable"; +const char* TrafficController::LOCAL_STANDBY = "fw_standby"; +const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave"; +const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted"; +const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby"; + +static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET, + "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET"); +static_assert(BPF_PERMISSION_UPDATE_DEVICE_STATS == INetd::PERMISSION_UPDATE_DEVICE_STATS, + "Mismatch between BPF and AIDL permissions: PERMISSION_UPDATE_DEVICE_STATS"); + +#define FLAG_MSG_TRANS(result, flag, value) \ + do { \ + if ((value) & (flag)) { \ + (result).append(" " #flag); \ + (value) &= ~(flag); \ + } \ + } while (0) + +const std::string uidMatchTypeToString(uint8_t match) { + std::string matchType; + FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match); + FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match); + FLAG_MSG_TRANS(matchType, DOZABLE_MATCH, match); + FLAG_MSG_TRANS(matchType, STANDBY_MATCH, match); + FLAG_MSG_TRANS(matchType, POWERSAVE_MATCH, match); + FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match); + FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match); + FLAG_MSG_TRANS(matchType, IIF_MATCH, match); + if (match) { + return StringPrintf("Unknown match: %u", match); + } + return matchType; +} + +bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) { + // This implementation is the same logic as method ActivityManager#checkComponentPermission. + // It implies that the calling uid can never be the same as PER_USER_RANGE. + uint32_t appId = uid % PER_USER_RANGE; + return ((appId == AID_ROOT) || (appId == AID_SYSTEM) || + mPrivilegedUser.find(appId) != mPrivilegedUser.end()); +} + +const std::string UidPermissionTypeToString(int permission) { + if (permission == INetd::PERMISSION_NONE) { + return "PERMISSION_NONE"; + } + if (permission == INetd::PERMISSION_UNINSTALLED) { + // This should never appear in the map, complain loudly if it does. + return "PERMISSION_UNINSTALLED error!"; + } + std::string permissionType; + FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_INTERNET, permission); + FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_UPDATE_DEVICE_STATS, permission); + if (permission) { + return StringPrintf("Unknown permission: %u", permission); + } + return permissionType; +} + +StatusOr<std::unique_ptr<NetlinkListenerInterface>> TrafficController::makeSkDestroyListener() { + const auto& sys = sSyscalls.get(); + ASSIGN_OR_RETURN(auto event, sys.eventfd(0, EFD_CLOEXEC)); + const int domain = AF_NETLINK; + const int type = SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK; + const int protocol = NETLINK_INET_DIAG; + ASSIGN_OR_RETURN(auto sock, sys.socket(domain, type, protocol)); + + // TODO: if too many sockets are closed too quickly, we can overflow the socket buffer, and + // some entries in mCookieTagMap will not be freed. In order to fix this we would need to + // periodically dump all sockets and remove the tag entries for sockets that have been closed. + // For now, set a large-enough buffer that we can close hundreds of sockets without getting + // ENOBUFS and leaking mCookieTagMap entries. + int rcvbuf = 512 * 1024; + auto ret = sys.setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + if (!ret.ok()) { + ALOGW("Failed to set SkDestroyListener buffer size to %d: %s", rcvbuf, ret.msg().c_str()); + } + + sockaddr_nl addr = { + .nl_family = AF_NETLINK, + .nl_groups = 1 << (SKNLGRP_INET_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET_UDP_DESTROY - 1) | + 1 << (SKNLGRP_INET6_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1)}; + RETURN_IF_NOT_OK(sys.bind(sock, addr)); + + const sockaddr_nl kernel = {.nl_family = AF_NETLINK}; + RETURN_IF_NOT_OK(sys.connect(sock, kernel)); + + std::unique_ptr<NetlinkListenerInterface> listener = + std::make_unique<NetlinkListener>(std::move(event), std::move(sock), "SkDestroyListen"); + + return listener; +} + +Status TrafficController::initMaps() { + std::lock_guard guard(mMutex); + + RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH)); + RETURN_IF_NOT_OK(mUidCounterSetMap.init(UID_COUNTERSET_MAP_PATH)); + RETURN_IF_NOT_OK(mAppUidStatsMap.init(APP_UID_STATS_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(mIfaceIndexNameMap.init(IFACE_INDEX_NAME_MAP_PATH)); + RETURN_IF_NOT_OK(mIfaceStatsMap.init(IFACE_STATS_MAP_PATH)); + + RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH)); + RETURN_IF_NOT_OK( + mConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY, DEFAULT_CONFIG, BPF_ANY)); + RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A, + BPF_ANY)); + + RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH)); + RETURN_IF_NOT_OK(mUidOwnerMap.clear()); + RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH)); + + return netdutils::status::ok; +} + +Status TrafficController::start() { + RETURN_IF_NOT_OK(initMaps()); + + // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is + // already running, so it will call addInterface() when any new interface appears. + // TODO: Clean-up addInterface() after interface monitoring is in + // NetworkStatsService. + std::map<std::string, uint32_t> ifacePairs; + ASSIGN_OR_RETURN(ifacePairs, getIfaceList()); + for (const auto& ifacePair:ifacePairs) { + addInterface(ifacePair.first.c_str(), ifacePair.second); + } + + auto result = makeSkDestroyListener(); + if (!isOk(result)) { + ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str()); + } else { + mSkDestroyListener = std::move(result.value()); + } + // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function. + const auto rxHandler = [this](const nlmsghdr&, const Slice msg) { + std::lock_guard guard(mMutex); + inet_diag_msg diagmsg = {}; + if (extract(msg, diagmsg) < sizeof(inet_diag_msg)) { + ALOGE("Unrecognized netlink message: %s", toString(msg).c_str()); + return; + } + uint64_t sock_cookie = static_cast<uint64_t>(diagmsg.id.idiag_cookie[0]) | + (static_cast<uint64_t>(diagmsg.id.idiag_cookie[1]) << 32); + + Status s = mCookieTagMap.deleteValue(sock_cookie); + if (!isOk(s) && s.code() != ENOENT) { + ALOGE("Failed to delete cookie %" PRIx64 ": %s", sock_cookie, toString(s).c_str()); + return; + } + }; + expectOk(mSkDestroyListener->subscribe(kSockDiagMsgType, rxHandler)); + + // In case multiple netlink message comes in as a stream, we need to handle the rxDone message + // properly. + const auto rxDoneHandler = [](const nlmsghdr&, const Slice msg) { + // Ignore NLMSG_DONE messages + inet_diag_msg diagmsg = {}; + extract(msg, diagmsg); + }; + expectOk(mSkDestroyListener->subscribe(kSockDiagDoneMsgType, rxDoneHandler)); + + return netdutils::status::ok; +} + +int TrafficController::setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) { + if (counterSetNum < 0 || counterSetNum >= OVERFLOW_COUNTERSET) return -EINVAL; + + std::lock_guard guard(mMutex); + if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM; + + // The default counter set for all uid is 0, so deleting the current counterset for that uid + // will automatically set it to 0. + if (counterSetNum == 0) { + Status res = mUidCounterSetMap.deleteValue(uid); + if (isOk(res) || (!isOk(res) && res.code() == ENOENT)) { + return 0; + } else { + ALOGE("Failed to delete the counterSet: %s\n", strerror(res.code())); + return -res.code(); + } + } + uint8_t tmpCounterSetNum = (uint8_t)counterSetNum; + Status res = mUidCounterSetMap.writeValue(uid, tmpCounterSetNum, BPF_ANY); + if (!isOk(res)) { + ALOGE("Failed to set the counterSet: %s, fd: %d", strerror(res.code()), + mUidCounterSetMap.getMap().get()); + return -res.code(); + } + return 0; +} + +// This method only get called by system_server when an app get uinstalled, it +// is called inside removeUidsLocked() while holding mStatsLock. So it is safe +// to iterate and modify the stats maps. +int TrafficController::deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) { + std::lock_guard guard(mMutex); + if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM; + + // First we go through the cookieTagMap to delete the target uid tag combination. Or delete all + // the tags related to the uid if the tag is 0. + const auto deleteMatchedCookieEntries = [uid, tag](const uint64_t& key, + const UidTagValue& value, + BpfMap<uint64_t, UidTagValue>& map) { + if (value.uid == uid && (value.tag == tag || tag == 0)) { + auto res = map.deleteValue(key); + if (res.ok() || (res.error().code() == ENOENT)) { + return base::Result<void>(); + } + ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key, + strerror(res.error().code())); + } + // Move forward to next cookie in the map. + return base::Result<void>(); + }; + mCookieTagMap.iterateWithValue(deleteMatchedCookieEntries); + // Now we go through the Tag stats map and delete the data entry with correct uid and tag + // combination. Or all tag stats under that uid if the target tag is 0. + const auto deleteMatchedUidTagEntries = [uid, tag](const StatsKey& key, + BpfMap<StatsKey, StatsValue>& map) { + if (key.uid == uid && (key.tag == tag || tag == 0)) { + auto res = map.deleteValue(key); + if (res.ok() || (res.error().code() == ENOENT)) { + //Entry is deleted, use the current key to get a new nextKey; + return base::Result<void>(); + } + ALOGE("Failed to delete data(uid=%u, tag=%u): %s\n", key.uid, key.tag, + strerror(res.error().code())); + } + return base::Result<void>(); + }; + mStatsMapB.iterate(deleteMatchedUidTagEntries); + mStatsMapA.iterate(deleteMatchedUidTagEntries); + // If the tag is not zero, we already deleted all the data entry required. If tag is 0, we also + // need to delete the stats stored in uidStatsMap and counterSet map. + if (tag != 0) return 0; + + auto res = mUidCounterSetMap.deleteValue(uid); + if (!res.ok() && res.error().code() != ENOENT) { + ALOGE("Failed to delete counterSet data(uid=%u, tag=%u): %s\n", uid, tag, + strerror(res.error().code())); + } + + auto deleteAppUidStatsEntry = [uid](const uint32_t& key, + BpfMap<uint32_t, StatsValue>& map) -> base::Result<void> { + if (key == uid) { + auto res = map.deleteValue(key); + if (res.ok() || (res.error().code() == ENOENT)) { + return {}; + } + ALOGE("Failed to delete data(uid=%u): %s", key, strerror(res.error().code())); + } + return {}; + }; + mAppUidStatsMap.iterate(deleteAppUidStatsEntry); + return 0; +} + +int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) { + IfaceValue iface; + if (ifaceIndex == 0) { + ALOGE("Unknown interface %s(%d)", name, ifaceIndex); + return -1; + } + + strlcpy(iface.name, name, sizeof(IfaceValue)); + Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY); + if (!isOk(res)) { + ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code())); + return -res.code(); + } + return 0; +} + +Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule, + FirewallType type) { + std::lock_guard guard(mMutex); + if ((rule == ALLOW && type == ALLOWLIST) || (rule == DENY && type == DENYLIST)) { + RETURN_IF_NOT_OK(addRule(uid, match)); + } else if ((rule == ALLOW && type == DENYLIST) || (rule == DENY && type == ALLOWLIST)) { + RETURN_IF_NOT_OK(removeRule(uid, match)); + } else { + //Cannot happen. + return statusFromErrno(EINVAL, ""); + } + return netdutils::status::ok; +} + +Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) { + auto oldMatch = mUidOwnerMap.readValue(uid); + if (oldMatch.ok()) { + UidOwnerValue newMatch = { + .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif, + .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match), + }; + if (newMatch.rule == 0) { + RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid)); + } else { + RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY)); + } + } else { + return statusFromErrno(ENOENT, StringPrintf("uid: %u does not exist in map", uid)); + } + return netdutils::status::ok; +} + +Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) { + // iif should be non-zero if and only if match == MATCH_IIF + if (match == IIF_MATCH && iif == 0) { + return statusFromErrno(EINVAL, "Interface match must have nonzero interface index"); + } else if (match != IIF_MATCH && iif != 0) { + return statusFromErrno(EINVAL, "Non-interface match must have zero interface index"); + } + auto oldMatch = mUidOwnerMap.readValue(uid); + if (oldMatch.ok()) { + UidOwnerValue newMatch = { + .iif = iif ? iif : oldMatch.value().iif, + .rule = static_cast<uint8_t>(oldMatch.value().rule | match), + }; + RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY)); + } else { + UidOwnerValue newMatch = { + .iif = iif, + .rule = static_cast<uint8_t>(match), + }; + RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY)); + } + return netdutils::status::ok; +} + +Status TrafficController::updateUidOwnerMap(const uint32_t uid, + UidOwnerMatchType matchType, IptOp op) { + std::lock_guard guard(mMutex); + if (op == IptOpDelete) { + RETURN_IF_NOT_OK(removeRule(uid, matchType)); + } else if (op == IptOpInsert) { + RETURN_IF_NOT_OK(addRule(uid, matchType)); + } else { + // Cannot happen. + return statusFromErrno(EINVAL, StringPrintf("invalid IptOp: %d, %d", op, matchType)); + } + return netdutils::status::ok; +} + +FirewallType TrafficController::getFirewallType(ChildChain chain) { + switch (chain) { + case DOZABLE: + return ALLOWLIST; + case STANDBY: + return DENYLIST; + case POWERSAVE: + return ALLOWLIST; + case RESTRICTED: + return ALLOWLIST; + case LOW_POWER_STANDBY: + return ALLOWLIST; + case NONE: + default: + return DENYLIST; + } +} + +int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallRule rule, + FirewallType type) { + Status res; + switch (chain) { + case DOZABLE: + res = updateOwnerMapEntry(DOZABLE_MATCH, uid, rule, type); + break; + case STANDBY: + res = updateOwnerMapEntry(STANDBY_MATCH, uid, rule, type); + break; + case POWERSAVE: + res = updateOwnerMapEntry(POWERSAVE_MATCH, uid, rule, type); + break; + case RESTRICTED: + res = updateOwnerMapEntry(RESTRICTED_MATCH, uid, rule, type); + break; + case LOW_POWER_STANDBY: + res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type); + break; + case NONE: + default: + ALOGW("Unknown child chain: %d", chain); + return -EINVAL; + } + if (!isOk(res)) { + ALOGE("change uid(%u) rule of %d failed: %s, rule: %d, type: %d", uid, chain, + res.msg().c_str(), rule, type); + return -res.code(); + } + return 0; +} + +Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match, + const std::vector<int32_t>& uids) { + std::lock_guard guard(mMutex); + std::set<int32_t> uidSet(uids.begin(), uids.end()); + std::vector<uint32_t> uidsToDelete; + auto getUidsToDelete = [&uidsToDelete, &uidSet](const uint32_t& key, + const BpfMap<uint32_t, UidOwnerValue>&) { + if (uidSet.find((int32_t) key) == uidSet.end()) { + uidsToDelete.push_back(key); + } + return base::Result<void>(); + }; + RETURN_IF_NOT_OK(mUidOwnerMap.iterate(getUidsToDelete)); + + for(auto uid : uidsToDelete) { + RETURN_IF_NOT_OK(removeRule(uid, match)); + } + + for (auto uid : uids) { + RETURN_IF_NOT_OK(addRule(uid, match)); + } + return netdutils::status::ok; +} + +Status TrafficController::addUidInterfaceRules(const int iif, + const std::vector<int32_t>& uidsToAdd) { + if (!iif) { + return statusFromErrno(EINVAL, "Interface rule must specify interface"); + } + std::lock_guard guard(mMutex); + + for (auto uid : uidsToAdd) { + netdutils::Status result = addRule(uid, IIF_MATCH, iif); + if (!isOk(result)) { + ALOGW("addRule failed(%d): uid=%d iif=%d", result.code(), uid, iif); + } + } + return netdutils::status::ok; +} + +Status TrafficController::removeUidInterfaceRules(const std::vector<int32_t>& uidsToDelete) { + std::lock_guard guard(mMutex); + + for (auto uid : uidsToDelete) { + netdutils::Status result = removeRule(uid, IIF_MATCH); + if (!isOk(result)) { + ALOGW("removeRule failed(%d): uid=%d", result.code(), uid); + } + } + return netdutils::status::ok; +} + +int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused, + const std::vector<int32_t>& uids) { + // FirewallRule rule = isAllowlist ? ALLOW : DENY; + // FirewallType type = isAllowlist ? ALLOWLIST : DENYLIST; + Status res; + if (!name.compare(LOCAL_DOZABLE)) { + res = replaceRulesInMap(DOZABLE_MATCH, uids); + } else if (!name.compare(LOCAL_STANDBY)) { + res = replaceRulesInMap(STANDBY_MATCH, uids); + } else if (!name.compare(LOCAL_POWERSAVE)) { + res = replaceRulesInMap(POWERSAVE_MATCH, uids); + } else if (!name.compare(LOCAL_RESTRICTED)) { + res = replaceRulesInMap(RESTRICTED_MATCH, uids); + } else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) { + res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids); + } else { + ALOGE("unknown chain name: %s", name.c_str()); + return -EINVAL; + } + if (!isOk(res)) { + ALOGE("Failed to clean up chain: %s: %s", name.c_str(), res.msg().c_str()); + return -res.code(); + } + return 0; +} + +int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) { + std::lock_guard guard(mMutex); + uint32_t key = UID_RULES_CONFIGURATION_KEY; + auto oldConfiguration = mConfigurationMap.readValue(key); + if (!oldConfiguration.ok()) { + ALOGE("Cannot read the old configuration from map: %s", + oldConfiguration.error().message().c_str()); + return -oldConfiguration.error().code(); + } + Status res; + BpfConfig newConfiguration; + uint8_t match; + switch (chain) { + case DOZABLE: + match = DOZABLE_MATCH; + break; + case STANDBY: + match = STANDBY_MATCH; + break; + case POWERSAVE: + match = POWERSAVE_MATCH; + break; + case RESTRICTED: + match = RESTRICTED_MATCH; + break; + case LOW_POWER_STANDBY: + match = LOW_POWER_STANDBY_MATCH; + break; + default: + return -EINVAL; + } + newConfiguration = + enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match)); + res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST); + if (!isOk(res)) { + ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str()); + } + return -res.code(); +} + +Status TrafficController::swapActiveStatsMap() { + std::lock_guard guard(mMutex); + + uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY; + auto oldConfiguration = mConfigurationMap.readValue(key); + if (!oldConfiguration.ok()) { + ALOGE("Cannot read the old configuration from map: %s", + oldConfiguration.error().message().c_str()); + return Status(oldConfiguration.error().code(), oldConfiguration.error().message()); + } + + // Write to the configuration map to inform the kernel eBPF program to switch + // from using one map to the other. Use flag BPF_EXIST here since the map should + // be already populated in initMaps. + uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A; + auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure, + BPF_EXIST); + if (!res.ok()) { + ALOGE("Failed to toggle the stats map: %s", strerror(res.error().code())); + return res; + } + // After changing the config, we need to make sure all the current running + // eBPF programs are finished and all the CPUs are aware of this config change + // before we modify the old map. So we do a special hack here to wait for + // the kernel to do a synchronize_rcu(). Once the kernel called + // synchronize_rcu(), the config we just updated will be available to all cores + // and the next eBPF programs triggered inside the kernel will use the new + // map configuration. So once this function returns we can safely modify the + // old stats map without concerning about race between the kernel and + // userspace. + int ret = synchronizeKernelRCU(); + if (ret) { + ALOGE("map swap synchronize_rcu() ended with failure: %s", strerror(-ret)); + return statusFromErrno(-ret, "map swap synchronize_rcu() failed"); + } + return netdutils::status::ok; +} + +void TrafficController::setPermissionForUids(int permission, const std::vector<uid_t>& uids) { + std::lock_guard guard(mMutex); + if (permission == INetd::PERMISSION_UNINSTALLED) { + for (uid_t uid : uids) { + // Clean up all permission information for the related uid if all the + // packages related to it are uninstalled. + mPrivilegedUser.erase(uid); + Status ret = mUidPermissionMap.deleteValue(uid); + if (!isOk(ret) && ret.code() != ENOENT) { + ALOGE("Failed to clean up the permission for %u: %s", uid, strerror(ret.code())); + } + } + return; + } + + bool privileged = (permission & INetd::PERMISSION_UPDATE_DEVICE_STATS); + + for (uid_t uid : uids) { + if (privileged) { + mPrivilegedUser.insert(uid); + } else { + mPrivilegedUser.erase(uid); + } + + // The map stores all the permissions that the UID has, except if the only permission + // the UID has is the INTERNET permission, then the UID should not appear in the map. + if (permission != INetd::PERMISSION_INTERNET) { + Status ret = mUidPermissionMap.writeValue(uid, permission, BPF_ANY); + if (!isOk(ret)) { + ALOGE("Failed to set permission: %s of uid(%u) to permission map: %s", + UidPermissionTypeToString(permission).c_str(), uid, strerror(ret.code())); + } + } else { + Status ret = mUidPermissionMap.deleteValue(uid); + if (!isOk(ret) && ret.code() != ENOENT) { + ALOGE("Failed to remove uid %u from permission map: %s", uid, strerror(ret.code())); + } + } + } +} + +std::string getProgramStatus(const char *path) { + int ret = access(path, R_OK); + if (ret == 0) { + return StringPrintf("OK"); + } + if (ret != 0 && errno == ENOENT) { + return StringPrintf("program is missing at: %s", path); + } + return StringPrintf("check Program %s error: %s", path, strerror(errno)); +} + +std::string getMapStatus(const base::unique_fd& map_fd, const char* path) { + if (map_fd.get() < 0) { + return StringPrintf("map fd lost"); + } + if (access(path, F_OK) != 0) { + return StringPrintf("map not pinned to location: %s", path); + } + return StringPrintf("OK"); +} + +// NOLINTNEXTLINE(google-runtime-references): grandfathered pass by non-const reference +void dumpBpfMap(const std::string& mapName, DumpWriter& dw, const std::string& header) { + dw.blankline(); + dw.println("%s:", mapName.c_str()); + if (!header.empty()) { + dw.println(header); + } +} + +void TrafficController::dump(DumpWriter& dw, bool verbose) { + std::lock_guard guard(mMutex); + ScopedIndent indentTop(dw); + dw.println("TrafficController"); + + ScopedIndent indentPreBpfModule(dw); + + dw.blankline(); + dw.println("mCookieTagMap status: %s", + getMapStatus(mCookieTagMap.getMap(), COOKIE_TAG_MAP_PATH).c_str()); + dw.println("mUidCounterSetMap status: %s", + getMapStatus(mUidCounterSetMap.getMap(), UID_COUNTERSET_MAP_PATH).c_str()); + dw.println("mAppUidStatsMap status: %s", + getMapStatus(mAppUidStatsMap.getMap(), APP_UID_STATS_MAP_PATH).c_str()); + dw.println("mStatsMapA status: %s", + getMapStatus(mStatsMapA.getMap(), STATS_MAP_A_PATH).c_str()); + dw.println("mStatsMapB status: %s", + getMapStatus(mStatsMapB.getMap(), STATS_MAP_B_PATH).c_str()); + dw.println("mIfaceIndexNameMap status: %s", + getMapStatus(mIfaceIndexNameMap.getMap(), IFACE_INDEX_NAME_MAP_PATH).c_str()); + dw.println("mIfaceStatsMap status: %s", + getMapStatus(mIfaceStatsMap.getMap(), IFACE_STATS_MAP_PATH).c_str()); + dw.println("mConfigurationMap status: %s", + getMapStatus(mConfigurationMap.getMap(), CONFIGURATION_MAP_PATH).c_str()); + dw.println("mUidOwnerMap status: %s", + getMapStatus(mUidOwnerMap.getMap(), UID_OWNER_MAP_PATH).c_str()); + + dw.blankline(); + dw.println("Cgroup ingress program status: %s", + getProgramStatus(BPF_INGRESS_PROG_PATH).c_str()); + dw.println("Cgroup egress program status: %s", getProgramStatus(BPF_EGRESS_PROG_PATH).c_str()); + dw.println("xt_bpf ingress program status: %s", + getProgramStatus(XT_BPF_INGRESS_PROG_PATH).c_str()); + dw.println("xt_bpf egress program status: %s", + getProgramStatus(XT_BPF_EGRESS_PROG_PATH).c_str()); + dw.println("xt_bpf bandwidth allowlist program status: %s", + getProgramStatus(XT_BPF_ALLOWLIST_PROG_PATH).c_str()); + dw.println("xt_bpf bandwidth denylist program status: %s", + getProgramStatus(XT_BPF_DENYLIST_PROG_PATH).c_str()); + + if (!verbose) { + return; + } + + dw.blankline(); + dw.println("BPF map content:"); + + ScopedIndent indentForMapContent(dw); + + // Print CookieTagMap content. + dumpBpfMap("mCookieTagMap", dw, ""); + const auto printCookieTagInfo = [&dw](const uint64_t& key, const UidTagValue& value, + const BpfMap<uint64_t, UidTagValue>&) { + dw.println("cookie=%" PRIu64 " tag=0x%x uid=%u", key, value.tag, value.uid); + return base::Result<void>(); + }; + base::Result<void> res = mCookieTagMap.iterateWithValue(printCookieTagInfo); + if (!res.ok()) { + dw.println("mCookieTagMap print end with error: %s", res.error().message().c_str()); + } + + // Print UidCounterSetMap content. + dumpBpfMap("mUidCounterSetMap", dw, ""); + const auto printUidInfo = [&dw](const uint32_t& key, const uint8_t& value, + const BpfMap<uint32_t, uint8_t>&) { + dw.println("%u %u", key, value); + return base::Result<void>(); + }; + res = mUidCounterSetMap.iterateWithValue(printUidInfo); + if (!res.ok()) { + dw.println("mUidCounterSetMap print end with error: %s", res.error().message().c_str()); + } + + // Print AppUidStatsMap content. + std::string appUidStatsHeader = StringPrintf("uid rxBytes rxPackets txBytes txPackets"); + dumpBpfMap("mAppUidStatsMap:", dw, appUidStatsHeader); + auto printAppUidStatsInfo = [&dw](const uint32_t& key, const StatsValue& value, + const BpfMap<uint32_t, StatsValue>&) { + dw.println("%u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, value.rxBytes, + value.rxPackets, value.txBytes, value.txPackets); + return base::Result<void>(); + }; + res = mAppUidStatsMap.iterateWithValue(printAppUidStatsInfo); + if (!res.ok()) { + dw.println("mAppUidStatsMap print end with error: %s", res.error().message().c_str()); + } + + // Print uidStatsMap content. + std::string statsHeader = StringPrintf("ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes" + " rxPackets txBytes txPackets"); + dumpBpfMap("mStatsMapA", dw, statsHeader); + const auto printStatsInfo = [&dw, this](const StatsKey& key, const StatsValue& value, + const BpfMap<StatsKey, StatsValue>&) { + uint32_t ifIndex = key.ifaceIndex; + auto ifname = mIfaceIndexNameMap.readValue(ifIndex); + if (!ifname.ok()) { + ifname = IfaceValue{"unknown"}; + } + dw.println("%u %s 0x%x %u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, ifIndex, + ifname.value().name, key.tag, key.uid, key.counterSet, value.rxBytes, + value.rxPackets, value.txBytes, value.txPackets); + return base::Result<void>(); + }; + res = mStatsMapA.iterateWithValue(printStatsInfo); + if (!res.ok()) { + dw.println("mStatsMapA print end with error: %s", res.error().message().c_str()); + } + + // Print TagStatsMap content. + dumpBpfMap("mStatsMapB", dw, statsHeader); + res = mStatsMapB.iterateWithValue(printStatsInfo); + if (!res.ok()) { + dw.println("mStatsMapB print end with error: %s", res.error().message().c_str()); + } + + // Print ifaceIndexToNameMap content. + dumpBpfMap("mIfaceIndexNameMap", dw, ""); + const auto printIfaceNameInfo = [&dw](const uint32_t& key, const IfaceValue& value, + const BpfMap<uint32_t, IfaceValue>&) { + const char* ifname = value.name; + dw.println("ifaceIndex=%u ifaceName=%s", key, ifname); + return base::Result<void>(); + }; + res = mIfaceIndexNameMap.iterateWithValue(printIfaceNameInfo); + if (!res.ok()) { + dw.println("mIfaceIndexNameMap print end with error: %s", res.error().message().c_str()); + } + + // Print ifaceStatsMap content + std::string ifaceStatsHeader = StringPrintf("ifaceIndex ifaceName rxBytes rxPackets txBytes" + " txPackets"); + dumpBpfMap("mIfaceStatsMap:", dw, ifaceStatsHeader); + const auto printIfaceStatsInfo = [&dw, this](const uint32_t& key, const StatsValue& value, + const BpfMap<uint32_t, StatsValue>&) { + auto ifname = mIfaceIndexNameMap.readValue(key); + if (!ifname.ok()) { + ifname = IfaceValue{"unknown"}; + } + dw.println("%u %s %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, ifname.value().name, + value.rxBytes, value.rxPackets, value.txBytes, value.txPackets); + return base::Result<void>(); + }; + res = mIfaceStatsMap.iterateWithValue(printIfaceStatsInfo); + if (!res.ok()) { + dw.println("mIfaceStatsMap print end with error: %s", res.error().message().c_str()); + } + + dw.blankline(); + + uint32_t key = UID_RULES_CONFIGURATION_KEY; + auto configuration = mConfigurationMap.readValue(key); + if (configuration.ok()) { + dw.println("current ownerMatch configuration: %d%s", configuration.value(), + uidMatchTypeToString(configuration.value()).c_str()); + } else { + dw.println("mConfigurationMap read ownerMatch configure failed with error: %s", + configuration.error().message().c_str()); + } + + key = CURRENT_STATS_MAP_CONFIGURATION_KEY; + configuration = mConfigurationMap.readValue(key); + if (configuration.ok()) { + const char* statsMapDescription = "???"; + switch (configuration.value()) { + case SELECT_MAP_A: + statsMapDescription = "SELECT_MAP_A"; + break; + case SELECT_MAP_B: + statsMapDescription = "SELECT_MAP_B"; + break; + // No default clause, so if we ever add a third map, this code will fail to build. + } + dw.println("current statsMap configuration: %d %s", configuration.value(), + statsMapDescription); + } else { + dw.println("mConfigurationMap read stats map configure failed with error: %s", + configuration.error().message().c_str()); + } + dumpBpfMap("mUidOwnerMap", dw, ""); + const auto printUidMatchInfo = [&dw, this](const uint32_t& key, const UidOwnerValue& value, + const BpfMap<uint32_t, UidOwnerValue>&) { + if (value.rule & IIF_MATCH) { + auto ifname = mIfaceIndexNameMap.readValue(value.iif); + if (ifname.ok()) { + dw.println("%u %s %s", key, uidMatchTypeToString(value.rule).c_str(), + ifname.value().name); + } else { + dw.println("%u %s %u", key, uidMatchTypeToString(value.rule).c_str(), value.iif); + } + } else { + dw.println("%u %s", key, uidMatchTypeToString(value.rule).c_str()); + } + return base::Result<void>(); + }; + res = mUidOwnerMap.iterateWithValue(printUidMatchInfo); + if (!res.ok()) { + dw.println("mUidOwnerMap print end with error: %s", res.error().message().c_str()); + } + dumpBpfMap("mUidPermissionMap", dw, ""); + const auto printUidPermissionInfo = [&dw](const uint32_t& key, const int& value, + const BpfMap<uint32_t, uint8_t>&) { + dw.println("%u %s", key, UidPermissionTypeToString(value).c_str()); + return base::Result<void>(); + }; + res = mUidPermissionMap.iterateWithValue(printUidPermissionInfo); + if (!res.ok()) { + dw.println("mUidPermissionMap print end with error: %s", res.error().message().c_str()); + } + + dumpBpfMap("mPrivilegedUser", dw, ""); + for (uid_t uid : mPrivilegedUser) { + dw.println("%u ALLOW_UPDATE_DEVICE_STATS", (uint32_t)uid); + } +} + +} // namespace net +} // namespace android
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp new file mode 100644 index 0000000..d0eca34 --- /dev/null +++ b/service/native/TrafficControllerTest.cpp
@@ -0,0 +1,881 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * TrafficControllerTest.cpp - unit tests for TrafficController.cpp + */ + +#include <cstdint> +#include <string> +#include <vector> + +#include <fcntl.h> +#include <inttypes.h> +#include <linux/inet_diag.h> +#include <linux/sock_diag.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 <binder/Status.h> + +#include <netdutils/MockSyscalls.h> + +#include "TrafficController.h" +#include "bpf/BpfUtils.h" +#include "NetdUpdatablePublic.h" + +using namespace android::bpf; // NOLINT(google-build-using-namespace): grandfathered + +namespace android { +namespace net { + +using android::netdutils::Status; +using base::Result; +using netdutils::isOk; + +constexpr int TEST_MAP_SIZE = 10; +constexpr uid_t TEST_UID = 10086; +constexpr uid_t TEST_UID2 = 54321; +constexpr uid_t TEST_UID3 = 98765; +constexpr uint32_t TEST_TAG = 42; +constexpr uint32_t TEST_COUNTERSET = 1; +constexpr uint32_t DEFAULT_COUNTERSET = 0; + +#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid()) + +class TrafficControllerTest : public ::testing::Test { + protected: + TrafficControllerTest() {} + TrafficController mTc; + BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap; + BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap; + BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap; + BpfMap<StatsKey, StatsValue> mFakeStatsMapA; + BpfMap<uint32_t, uint8_t> mFakeConfigurationMap; + BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap; + BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap; + + void SetUp() { + std::lock_guard guard(mTc.mMutex); + ASSERT_EQ(0, setrlimitForTest()); + + mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue), + TEST_MAP_SIZE, 0)); + ASSERT_VALID(mFakeCookieTagMap); + + mFakeUidCounterSetMap.reset( + createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0)); + ASSERT_VALID(mFakeUidCounterSetMap); + + mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue), + TEST_MAP_SIZE, 0)); + ASSERT_VALID(mFakeAppUidStatsMap); + + 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); + + mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue), + TEST_MAP_SIZE, 0)); + ASSERT_VALID(mFakeUidOwnerMap); + mFakeUidPermissionMap.reset( + createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0)); + ASSERT_VALID(mFakeUidPermissionMap); + + mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap())); + ASSERT_VALID(mTc.mCookieTagMap); + mTc.mUidCounterSetMap.reset(dupFd(mFakeUidCounterSetMap.getMap())); + ASSERT_VALID(mTc.mUidCounterSetMap); + mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap())); + ASSERT_VALID(mTc.mAppUidStatsMap); + mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap())); + ASSERT_VALID(mTc.mStatsMapA); + mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap())); + ASSERT_VALID(mTc.mConfigurationMap); + + // Always write to stats map A by default. + ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, + SELECT_MAP_A, BPF_ANY)); + mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap())); + ASSERT_VALID(mTc.mUidOwnerMap); + mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap())); + ASSERT_VALID(mTc.mUidPermissionMap); + mTc.mPrivilegedUser.clear(); + } + + int dupFd(const android::base::unique_fd& mapFd) { + return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0); + } + + 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}; + uint8_t counterSet = TEST_COUNTERSET; + EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY)); + EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY)); + key->tag = 0; + EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY)); + EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY)); + // put tag information back to statsKey + key->tag = tag; + } + + void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) { + uint32_t uid = TEST_UID; + EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST)); + Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid); + EXPECT_RESULT_OK(value); + EXPECT_TRUE(value.value().rule & match); + + uid = TEST_UID2; + EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, ALLOWLIST)); + value = mFakeUidOwnerMap.readValue(uid); + EXPECT_RESULT_OK(value); + EXPECT_TRUE(value.value().rule & match); + + EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, ALLOWLIST)); + value = mFakeUidOwnerMap.readValue(uid); + EXPECT_FALSE(value.ok()); + EXPECT_EQ(ENOENT, value.error().code()); + + uid = TEST_UID; + EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST)); + value = mFakeUidOwnerMap.readValue(uid); + EXPECT_FALSE(value.ok()); + EXPECT_EQ(ENOENT, value.error().code()); + + uid = TEST_UID3; + EXPECT_EQ(-ENOENT, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST)); + value = mFakeUidOwnerMap.readValue(uid); + EXPECT_FALSE(value.ok()); + EXPECT_EQ(ENOENT, value.error().code()); + } + + void checkEachUidValue(const std::vector<int32_t>& uids, UidOwnerMatchType match) { + for (uint32_t uid : uids) { + Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid); + EXPECT_RESULT_OK(value); + EXPECT_TRUE(value.value().rule & match); + } + std::set<uint32_t> uidSet(uids.begin(), uids.end()); + const auto checkNoOtherUid = [&uidSet](const int32_t& key, + const BpfMap<uint32_t, UidOwnerValue>&) { + EXPECT_NE(uidSet.end(), uidSet.find(key)); + return Result<void>(); + }; + EXPECT_RESULT_OK(mFakeUidOwnerMap.iterate(checkNoOtherUid)); + } + + void checkUidMapReplace(const std::string& name, const std::vector<int32_t>& uids, + UidOwnerMatchType match) { + bool isAllowlist = true; + EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids)); + checkEachUidValue(uids, match); + + isAllowlist = false; + EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids)); + checkEachUidValue(uids, match); + } + + void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule, + uint32_t expectedIif) { + for (uint32_t uid : appUids) { + Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid); + EXPECT_RESULT_OK(value); + EXPECT_EQ(expectedRule, value.value().rule) + << "Expected rule for UID " << uid << " to be " << expectedRule << ", but was " + << value.value().rule; + EXPECT_EQ(expectedIif, value.value().iif) + << "Expected iif for UID " << uid << " to be " << expectedIif << ", but was " + << value.value().iif; + } + } + + template <class Key, class Value> + void expectMapEmpty(BpfMap<Key, Value>& map) { + auto isEmpty = map.isEmpty(); + EXPECT_RESULT_OK(isEmpty); + EXPECT_TRUE(isEmpty.value()); + } + + void expectUidPermissionMapValues(const std::vector<uid_t>& appUids, uint8_t expectedValue) { + for (uid_t uid : appUids) { + Result<uint8_t> value = mFakeUidPermissionMap.readValue(uid); + EXPECT_RESULT_OK(value); + EXPECT_EQ(expectedValue, value.value()) + << "Expected value for UID " << uid << " to be " << expectedValue + << ", but was " << value.value(); + } + } + + void expectPrivilegedUserSet(const std::vector<uid_t>& appUids) { + std::lock_guard guard(mTc.mMutex); + EXPECT_EQ(appUids.size(), mTc.mPrivilegedUser.size()); + for (uid_t uid : appUids) { + EXPECT_NE(mTc.mPrivilegedUser.end(), mTc.mPrivilegedUser.find(uid)); + } + } + + void expectPrivilegedUserSetEmpty() { + std::lock_guard guard(mTc.mMutex); + EXPECT_TRUE(mTc.mPrivilegedUser.empty()); + } + + void addPrivilegedUid(uid_t uid) { + std::vector privilegedUid = {uid}; + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid); + } + + void removePrivilegedUid(uid_t uid) { + std::vector privilegedUid = {uid}; + mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid); + } + + void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid, + StatsKey tagStatsMapKey) { + Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie); + EXPECT_RESULT_OK(cookieMapResult); + EXPECT_EQ(uid, cookieMapResult.value().uid); + EXPECT_EQ(tag, cookieMapResult.value().tag); + Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid); + EXPECT_RESULT_OK(counterSetResult); + EXPECT_EQ(TEST_COUNTERSET, counterSetResult.value()); + Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey); + EXPECT_RESULT_OK(statsMapResult); + EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets); + EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes); + tagStatsMapKey.tag = 0; + statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey); + EXPECT_RESULT_OK(statsMapResult); + EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets); + EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes); + auto appStatsResult = mFakeAppUidStatsMap.readValue(uid); + EXPECT_RESULT_OK(appStatsResult); + EXPECT_EQ((uint64_t)1, appStatsResult.value().rxPackets); + EXPECT_EQ((uint64_t)100, appStatsResult.value().rxBytes); + } + + Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids, + UidOwnerMatchType matchType, TrafficController::IptOp op) { + Status ret(0); + for (auto uid : appUids) { + ret = mTc.updateUidOwnerMap(uid, matchType, op); + if(!isOk(ret)) break; + } + return ret; + } + +}; + +TEST_F(TrafficControllerTest, TestSetCounterSet) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + ASSERT_EQ(0, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, callingUid)); + uid_t uid = TEST_UID; + Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid); + ASSERT_RESULT_OK(counterSetResult); + ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value()); + ASSERT_EQ(0, mTc.setCounterSet(DEFAULT_COUNTERSET, TEST_UID, callingUid)); + ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok()); + expectMapEmpty(mFakeUidCounterSetMap); +} + +TEST_F(TrafficControllerTest, TestSetCounterSetWithoutPermission) { + ASSERT_EQ(-EPERM, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, TEST_UID2)); + uid_t uid = TEST_UID; + ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok()); + expectMapEmpty(mFakeUidCounterSetMap); +} + +TEST_F(TrafficControllerTest, TestSetInvalidCounterSet) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + ASSERT_GT(0, mTc.setCounterSet(OVERFLOW_COUNTERSET, TEST_UID, callingUid)); + uid_t uid = TEST_UID; + ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok()); + expectMapEmpty(mFakeUidCounterSetMap); +} + +TEST_F(TrafficControllerTest, TestDeleteTagDataWithoutPermission) { + uint64_t cookie = 1; + uid_t uid = TEST_UID; + uint32_t tag = TEST_TAG; + StatsKey tagStatsMapKey; + populateFakeStats(cookie, uid, tag, &tagStatsMapKey); + ASSERT_EQ(-EPERM, mTc.deleteTagData(0, TEST_UID, TEST_UID2)); + + expectFakeStatsUnchanged(cookie, tag, uid, tagStatsMapKey); +} + +TEST_F(TrafficControllerTest, TestDeleteTagData) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + uint64_t cookie = 1; + uid_t uid = TEST_UID; + uint32_t tag = TEST_TAG; + StatsKey tagStatsMapKey; + populateFakeStats(cookie, uid, tag, &tagStatsMapKey); + ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid)); + ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok()); + Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid); + ASSERT_RESULT_OK(counterSetResult); + ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value()); + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok()); + tagStatsMapKey.tag = 0; + Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey); + ASSERT_RESULT_OK(statsMapResult); + ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets); + ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes); + auto appStatsResult = mFakeAppUidStatsMap.readValue(TEST_UID); + ASSERT_RESULT_OK(appStatsResult); + ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets); + ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes); +} + +TEST_F(TrafficControllerTest, TestDeleteAllUidData) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + uint64_t cookie = 1; + uid_t uid = TEST_UID; + uint32_t tag = TEST_TAG; + StatsKey tagStatsMapKey; + populateFakeStats(cookie, uid, tag, &tagStatsMapKey); + ASSERT_EQ(0, mTc.deleteTagData(0, TEST_UID, callingUid)); + ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok()); + ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok()); + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok()); + tagStatsMapKey.tag = 0; + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok()); + ASSERT_FALSE(mFakeAppUidStatsMap.readValue(TEST_UID).ok()); +} + +TEST_F(TrafficControllerTest, TestDeleteDataWithTwoTags) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + uint64_t cookie1 = 1; + uint64_t cookie2 = 2; + uid_t uid = TEST_UID; + uint32_t tag1 = TEST_TAG; + uint32_t tag2 = TEST_TAG + 1; + StatsKey tagStatsMapKey1; + StatsKey tagStatsMapKey2; + populateFakeStats(cookie1, uid, tag1, &tagStatsMapKey1); + populateFakeStats(cookie2, uid, tag2, &tagStatsMapKey2); + ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid)); + ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie1).ok()); + Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie2); + ASSERT_RESULT_OK(cookieMapResult); + ASSERT_EQ(TEST_UID, cookieMapResult.value().uid); + ASSERT_EQ(TEST_TAG + 1, cookieMapResult.value().tag); + Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid); + ASSERT_RESULT_OK(counterSetResult); + ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value()); + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok()); + Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey2); + ASSERT_RESULT_OK(statsMapResult); + ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets); + ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes); +} + +TEST_F(TrafficControllerTest, TestDeleteDataWithTwoUids) { + uid_t callingUid = TEST_UID2; + addPrivilegedUid(callingUid); + uint64_t cookie1 = 1; + uint64_t cookie2 = 2; + uid_t uid1 = TEST_UID; + uid_t uid2 = TEST_UID + 1; + uint32_t tag = TEST_TAG; + StatsKey tagStatsMapKey1; + StatsKey tagStatsMapKey2; + populateFakeStats(cookie1, uid1, tag, &tagStatsMapKey1); + populateFakeStats(cookie2, uid2, tag, &tagStatsMapKey2); + + // Delete the stats of one of the uid. Check if it is properly collected by + // removedStats. + ASSERT_EQ(0, mTc.deleteTagData(0, uid2, callingUid)); + ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie2).ok()); + Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid1); + ASSERT_RESULT_OK(counterSetResult); + ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value()); + ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid2).ok()); + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok()); + tagStatsMapKey2.tag = 0; + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok()); + ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid2).ok()); + tagStatsMapKey1.tag = 0; + Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey1); + ASSERT_RESULT_OK(statsMapResult); + ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets); + ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes); + auto appStatsResult = mFakeAppUidStatsMap.readValue(uid1); + ASSERT_RESULT_OK(appStatsResult); + ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets); + ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes); + + // Delete the stats of the other uid. + ASSERT_EQ(0, mTc.deleteTagData(0, uid1, callingUid)); + ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok()); + ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid1).ok()); +} + +TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) { + uint32_t uid = TEST_UID; + ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, DENY, DENYLIST))); + Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid); + ASSERT_RESULT_OK(value); + ASSERT_TRUE(value.value().rule & STANDBY_MATCH); + + ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, ALLOW, ALLOWLIST))); + value = mFakeUidOwnerMap.readValue(uid); + ASSERT_RESULT_OK(value); + ASSERT_TRUE(value.value().rule & DOZABLE_MATCH); + + ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, DENY, ALLOWLIST))); + value = mFakeUidOwnerMap.readValue(uid); + ASSERT_RESULT_OK(value); + ASSERT_FALSE(value.value().rule & DOZABLE_MATCH); + + ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST))); + ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok()); + + uid = TEST_UID2; + ASSERT_FALSE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST))); + ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok()); +} + +TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) { + checkUidOwnerRuleForChain(DOZABLE, DOZABLE_MATCH); + checkUidOwnerRuleForChain(STANDBY, STANDBY_MATCH); + checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH); + checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH); + checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH); + ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST)); + ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST)); +} + +TEST_F(TrafficControllerTest, TestReplaceUidOwnerMap) { + std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3}; + checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH); + checkUidMapReplace("fw_standby", uids, STANDBY_MATCH); + checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH); + checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH); + checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH); + ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids)); +} + +TEST_F(TrafficControllerTest, TestReplaceSameChain) { + std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3}; + checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH); + std::vector<int32_t> newUids = {TEST_UID2, TEST_UID3}; + checkUidMapReplace("fw_dozable", newUids, DOZABLE_MATCH); +} + +TEST_F(TrafficControllerTest, TestDenylistUidMatch) { + std::vector<uint32_t> appUids = {1000, 1001, 10012}; + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOpInsert))); + expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0); + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOpDelete))); + expectMapEmpty(mFakeUidOwnerMap); +} + +TEST_F(TrafficControllerTest, TestAllowlistUidMatch) { + std::vector<uint32_t> appUids = {1000, 1001, 10012}; + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert))); + expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0); + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete))); + expectMapEmpty(mFakeUidOwnerMap); +} + +TEST_F(TrafficControllerTest, TestReplaceMatchUid) { + std::vector<uint32_t> appUids = {1000, 1001, 10012}; + // Add appUids to the denylist and expect that their values are all PENALTY_BOX_MATCH. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOpInsert))); + expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0); + + // Add the same UIDs to the allowlist and expect that we get PENALTY_BOX_MATCH | + // HAPPY_BOX_MATCH. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert))); + expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH | PENALTY_BOX_MATCH, 0); + + // Remove the same UIDs from the allowlist and check the PENALTY_BOX_MATCH is still there. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete))); + expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0); + + // Remove the same UIDs from the denylist and check the map is empty. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOpDelete))); + ASSERT_FALSE(mFakeUidOwnerMap.getFirstKey().ok()); +} + +TEST_F(TrafficControllerTest, TestDeleteWrongMatchSilentlyFails) { + std::vector<uint32_t> appUids = {1000, 1001, 10012}; + // If the uid does not exist in the map, trying to delete a rule about it will fail. + ASSERT_FALSE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, + TrafficController::IptOpDelete))); + expectMapEmpty(mFakeUidOwnerMap); + + // Add denylist rules for appUids. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, + TrafficController::IptOpInsert))); + expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0); + + // Delete (non-existent) denylist rules for appUids, and check that this silently does + // nothing if the uid is in the map but does not have denylist match. This is required because + // NetworkManagementService will try to remove a uid from denylist after adding it to the + // allowlist and if the remove fails it will not update the uid status. + ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH, + TrafficController::IptOpDelete))); + expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0); +} + +TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRules) { + int iif0 = 15; + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001}))); + expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0); + + // Add some non-overlapping new uids. They should coexist with existing rules + int iif1 = 16; + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001}))); + expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0); + expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1); + + // Overwrite some existing uids + int iif2 = 17; + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif2, {1000, 2000}))); + expectUidOwnerMapValues({1001}, IIF_MATCH, iif0); + expectUidOwnerMapValues({2001}, IIF_MATCH, iif1); + expectUidOwnerMapValues({1000, 2000}, IIF_MATCH, iif2); +} + +TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRules) { + int iif0 = 15; + int iif1 = 16; + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001}))); + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001}))); + expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0); + expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1); + + // Rmove some uids + ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001, 2001}))); + expectUidOwnerMapValues({1000}, IIF_MATCH, iif0); + expectUidOwnerMapValues({2000}, IIF_MATCH, iif1); + checkEachUidValue({1000, 2000}, IIF_MATCH); // Make sure there are only two uids remaining + + // Remove non-existent uids shouldn't fail + ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({2000, 3000}))); + expectUidOwnerMapValues({1000}, IIF_MATCH, iif0); + checkEachUidValue({1000}, IIF_MATCH); // Make sure there are only one uid remaining + + // Remove everything + ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000}))); + expectMapEmpty(mFakeUidOwnerMap); +} + +TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) { + // Set up existing PENALTY_BOX_MATCH rules + ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH, + TrafficController::IptOpInsert))); + expectUidOwnerMapValues({1000, 1001, 10012}, PENALTY_BOX_MATCH, 0); + + // Add some partially-overlapping uid owner rules and check result + int iif1 = 32; + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10012, 10013, 10014}))); + expectUidOwnerMapValues({1000, 1001}, PENALTY_BOX_MATCH, 0); + expectUidOwnerMapValues({10012}, PENALTY_BOX_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10013, 10014}, IIF_MATCH, iif1); + + // Removing some PENALTY_BOX_MATCH rules should not change uid interface rule + ASSERT_TRUE(isOk(updateUidOwnerMaps({1001, 10012}, PENALTY_BOX_MATCH, + TrafficController::IptOpDelete))); + expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0); + expectUidOwnerMapValues({10012, 10013, 10014}, IIF_MATCH, iif1); + + // Remove all uid interface rules + ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({10012, 10013, 10014}))); + expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0); + // Make sure these are the only uids left + checkEachUidValue({1000}, PENALTY_BOX_MATCH); +} + +TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatches) { + int iif1 = 56; + // Set up existing uid interface rules + ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10001, 10002}))); + expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1); + + // Add some partially-overlapping doze rules + EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {10002, 10003})); + expectUidOwnerMapValues({10001}, IIF_MATCH, iif1); + expectUidOwnerMapValues({10002}, DOZABLE_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10003}, DOZABLE_MATCH, 0); + + // Introduce a third rule type (powersave) on various existing UIDs + EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {10000, 10001, 10002, 10003})); + expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0); + expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10003}, POWERSAVE_MATCH | DOZABLE_MATCH, 0); + + // Remove all doze rules + EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {})); + expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0); + expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | IIF_MATCH, iif1); + expectUidOwnerMapValues({10003}, POWERSAVE_MATCH, 0); + + // Remove all powersave rules, expect ownerMap to only have uid interface rules left + EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {})); + expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1); + // Make sure these are the only uids left + checkEachUidValue({10001, 10002}, IIF_MATCH); +} + +TEST_F(TrafficControllerTest, TestGrantInternetPermission) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids); + expectMapEmpty(mFakeUidPermissionMap); + expectPrivilegedUserSetEmpty(); +} + +TEST_F(TrafficControllerTest, TestRevokeInternetPermission) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE); +} + +TEST_F(TrafficControllerTest, TestPermissionUninstalled) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS); + expectPrivilegedUserSet(appUids); + + std::vector<uid_t> uidToRemove = {TEST_UID}; + mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidToRemove); + + std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2}; + expectUidPermissionMapValues(uidRemain, INetd::PERMISSION_UPDATE_DEVICE_STATS); + expectPrivilegedUserSet(uidRemain); + + mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidRemain); + expectMapEmpty(mFakeUidPermissionMap); + expectPrivilegedUserSetEmpty(); +} + +TEST_F(TrafficControllerTest, TestGrantUpdateStatsPermission) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS); + expectPrivilegedUserSet(appUids); + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids); + expectPrivilegedUserSetEmpty(); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE); +} + +TEST_F(TrafficControllerTest, TestRevokeUpdateStatsPermission) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids); + expectPrivilegedUserSet(appUids); + + std::vector<uid_t> uidToRemove = {TEST_UID}; + mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidToRemove); + + std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2}; + expectPrivilegedUserSet(uidRemain); + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidRemain); + expectPrivilegedUserSetEmpty(); +} + +TEST_F(TrafficControllerTest, TestGrantWrongPermission) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids); + expectPrivilegedUserSetEmpty(); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE); +} + +TEST_F(TrafficControllerTest, TestGrantDuplicatePermissionSlientlyFail) { + std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3}; + + mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids); + expectMapEmpty(mFakeUidPermissionMap); + + std::vector<uid_t> uidToAdd = {TEST_UID}; + mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, uidToAdd); + + expectPrivilegedUserSetEmpty(); + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids); + expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE); + + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids); + expectPrivilegedUserSet(appUids); + + mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, uidToAdd); + expectPrivilegedUserSet(appUids); + + mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids); + expectPrivilegedUserSetEmpty(); +} + +constexpr uint32_t SOCK_CLOSE_WAIT_US = 30 * 1000; +constexpr uint32_t ENOBUFS_POLL_WAIT_US = 10 * 1000; + +using android::base::Error; +using android::base::Result; +using android::bpf::BpfMap; + +// This test set up a SkDestroyListener that is running parallel with the production +// SkDestroyListener. The test will create thousands of sockets and tag them on the +// production cookieUidTagMap and close them in a short time. When the number of +// sockets get closed exceeds the buffer size, it will start to return ENOBUFF +// error. The error will be ignored by the production SkDestroyListener and the +// test will clean up the tags in tearDown if there is any remains. + +// TODO: Instead of test the ENOBUFF error, we can test the production +// SkDestroyListener to see if it failed to delete a tagged socket when ENOBUFF +// triggered. +class NetlinkListenerTest : public testing::Test { + protected: + NetlinkListenerTest() {} + BpfMap<uint64_t, UidTagValue> mCookieTagMap; + + void SetUp() { + mCookieTagMap.reset(android::bpf::mapRetrieveRW(COOKIE_TAG_MAP_PATH)); + ASSERT_TRUE(mCookieTagMap.isValid()); + } + + void TearDown() { + const auto deleteTestCookieEntries = [](const uint64_t& key, const UidTagValue& value, + BpfMap<uint64_t, UidTagValue>& map) { + if ((value.uid == TEST_UID) && (value.tag == TEST_TAG)) { + Result<void> res = map.deleteValue(key); + if (res.ok() || (res.error().code() == ENOENT)) { + return Result<void>(); + } + ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key, + strerror(res.error().code())); + } + // Move forward to next cookie in the map. + return Result<void>(); + }; + EXPECT_RESULT_OK(mCookieTagMap.iterateWithValue(deleteTestCookieEntries)); + } + + Result<void> checkNoGarbageTagsExist() { + const auto checkGarbageTags = [](const uint64_t&, const UidTagValue& value, + const BpfMap<uint64_t, UidTagValue>&) -> Result<void> { + if ((TEST_UID == value.uid) && (TEST_TAG == value.tag)) { + return Error(EUCLEAN) << "Closed socket is not untagged"; + } + return {}; + }; + return mCookieTagMap.iterateWithValue(checkGarbageTags); + } + + bool checkMassiveSocketDestroy(int totalNumber, bool expectError) { + std::unique_ptr<android::netdutils::NetlinkListenerInterface> skDestroyListener; + auto result = android::net::TrafficController::makeSkDestroyListener(); + if (!isOk(result)) { + ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str()); + } else { + skDestroyListener = std::move(result.value()); + } + int rxErrorCount = 0; + // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function. + const auto rxErrorHandler = [&rxErrorCount](const int, const int) { rxErrorCount++; }; + skDestroyListener->registerSkErrorHandler(rxErrorHandler); + int fds[totalNumber]; + for (int i = 0; i < totalNumber; i++) { + fds[i] = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + // The likely reason for a failure is running out of available file descriptors. + EXPECT_LE(0, fds[i]) << i << " of " << totalNumber; + if (fds[i] < 0) { + // EXPECT_LE already failed above, so test case is a failure, but we don't + // want potentially tens of thousands of extra failures creating and then + // closing all these fds cluttering up the logs. + totalNumber = i; + break; + }; + libnetd_updatable_tagSocket(fds[i], TEST_TAG, TEST_UID, 1000); + } + + // TODO: Use a separate thread that has its own fd table so we can + // close sockets even faster simply by terminating that thread. + for (int i = 0; i < totalNumber; i++) { + EXPECT_EQ(0, close(fds[i])); + } + // wait a bit for netlink listener to handle all the messages. + usleep(SOCK_CLOSE_WAIT_US); + if (expectError) { + // If ENOBUFS triggered, check it only called into the handler once, ie. + // that the netlink handler is not spinning. + int currentErrorCount = rxErrorCount; + // 0 error count is acceptable because the system has chances to close all sockets + // normally. + EXPECT_LE(0, rxErrorCount); + if (!rxErrorCount) return true; + + usleep(ENOBUFS_POLL_WAIT_US); + EXPECT_EQ(currentErrorCount, rxErrorCount); + } else { + EXPECT_RESULT_OK(checkNoGarbageTagsExist()); + EXPECT_EQ(0, rxErrorCount); + } + return false; + } +}; + +TEST_F(NetlinkListenerTest, TestAllSocketUntagged) { + checkMassiveSocketDestroy(10, false); + checkMassiveSocketDestroy(100, false); +} + +// Disabled because flaky on blueline-userdebug; this test relies on the main thread +// winning a race against the NetlinkListener::run() thread. There's no way to ensure +// things will be scheduled the same way across all architectures and test environments. +TEST_F(NetlinkListenerTest, DISABLED_TestSkDestroyError) { + bool needRetry = false; + int retryCount = 0; + do { + needRetry = checkMassiveSocketDestroy(32500, true); + if (needRetry) retryCount++; + } while (needRetry && retryCount < 3); + // Should review test if it can always close all sockets correctly. + EXPECT_GT(3, retryCount); +} + + +} // namespace net +} // namespace android
diff --git a/service/native/include/Common.h b/service/native/include/Common.h new file mode 100644 index 0000000..dc44845 --- /dev/null +++ b/service/native/include/Common.h
@@ -0,0 +1,40 @@ +/* + * 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 +// TODO: deduplicate with the constants in NetdConstants.h. +#include <aidl/android/net/INetd.h> + +using aidl::android::net::INetd; + +enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY }; + +// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed +// DENYLIST means the firewall allows all by default, uids must be explicitly DENYed + +enum FirewallType { ALLOWLIST = INetd::FIREWALL_ALLOWLIST, DENYLIST = INetd::FIREWALL_DENYLIST }; + +// LINT.IfChange(firewall_chain) +enum ChildChain { + NONE = 0, + DOZABLE = 1, + STANDBY = 2, + POWERSAVE = 3, + RESTRICTED = 4, + LOW_POWER_STANDBY = 5, + INVALID_CHAIN +}; +// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h new file mode 100644 index 0000000..e741dd6 --- /dev/null +++ b/service/native/include/TrafficController.h
@@ -0,0 +1,204 @@ +/* + * 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 <set> +#include <Common.h> + +#include "android-base/thread_annotations.h" +#include "bpf/BpfMap.h" +#include "bpf_shared.h" +#include "netdutils/DumpWriter.h" +#include "netdutils/NetlinkListener.h" +#include "netdutils/StatusOr.h" + +namespace android { +namespace net { + +using netdutils::StatusOr; + +class TrafficController { + public: + static constexpr char DUMP_KEYWORD[] = "trafficcontroller"; + + /* + * Initialize the whole controller + */ + netdutils::Status start(); + + int setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) EXCLUDES(mMutex); + + /* + * When deleting a tag data, the qtaguid module will grab the spinlock of each + * related rb_tree one by one and delete the tag information, counterSet + * information, iface stats information and uid stats information one by one. + * The new eBPF implementation is done similiarly by removing the entry on + * each map one by one. And deleting processes are also protected by the + * spinlock of the map. So no additional lock is required. + */ + int deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) EXCLUDES(mMutex); + + /* + * Swap the stats map config from current active stats map to the idle one. + */ + netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex); + + /* + * Add the interface name and index pair into the eBPF map. + */ + int addInterface(const char* name, uint32_t ifaceIndex); + + int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type); + + int removeUidOwnerRule(const uid_t uid); + + int replaceUidOwnerMap(const std::string& name, bool isAllowlist, + const std::vector<int32_t>& uids); + + enum IptOp { IptOpInsert, IptOpDelete }; + + netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule, + FirewallType type) EXCLUDES(mMutex); + + void dump(netdutils::DumpWriter& dw, bool verbose) EXCLUDES(mMutex); + + netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids) + EXCLUDES(mMutex); + + netdutils::Status addUidInterfaceRules(const int ifIndex, const std::vector<int32_t>& uids) + EXCLUDES(mMutex); + netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex); + + netdutils::Status updateUidOwnerMap(const uint32_t uid, + UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex); + + int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex); + + static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>> + makeSkDestroyListener(); + + void setPermissionForUids(int permission, const std::vector<uid_t>& uids) EXCLUDES(mMutex); + + FirewallType getFirewallType(ChildChain); + + static const char* LOCAL_DOZABLE; + static const char* LOCAL_STANDBY; + static const char* LOCAL_POWERSAVE; + static const char* LOCAL_RESTRICTED; + static const char* LOCAL_LOW_POWER_STANDBY; + + private: + /* + * mCookieTagMap: Store the corresponding tag and uid for a specific socket. + * DO NOT hold any locks when modifying this map, otherwise when the untag + * operation is waiting for a lock hold by other process and there are more + * sockets being closed than can fit in the socket buffer of the netlink socket + * that receives them, then the kernel will drop some of these sockets and we + * won't delete their tags. + * Map Key: uint64_t socket cookie + * Map Value: UidTagValue, contains a uint32 uid and a uint32 tag. + */ + bpf::BpfMap<uint64_t, UidTagValue> mCookieTagMap GUARDED_BY(mMutex); + + /* + * mUidCounterSetMap: Store the counterSet of a specific uid. + * Map Key: uint32 uid. + * Map Value: uint32 counterSet specifies if the traffic is a background + * or foreground traffic. + */ + bpf::BpfMap<uint32_t, uint8_t> mUidCounterSetMap GUARDED_BY(mMutex); + + /* + * mAppUidStatsMap: Store the total traffic stats for a uid regardless of + * tag, counterSet and iface. The stats is used by TrafficStats.getUidStats + * API to return persistent stats for a specific uid since device boot. + */ + bpf::BpfMap<uint32_t, StatsValue> mAppUidStatsMap; + + /* + * mStatsMapA/mStatsMapB: Store the traffic statistics for a specific + * combination of uid, tag, iface and counterSet. These two maps contain + * both tagged and untagged traffic. + * Map Key: StatsKey contains the uid, tag, counterSet and ifaceIndex + * information. + * Map Value: Stats, contains packet count and byte count of each + * transport protocol on egress and ingress direction. + */ + bpf::BpfMap<StatsKey, StatsValue> mStatsMapA GUARDED_BY(mMutex); + + bpf::BpfMap<StatsKey, StatsValue> mStatsMapB GUARDED_BY(mMutex); + + /* + * mIfaceIndexNameMap: Store the index name pair of each interface show up + * on the device since boot. The interface index is used by the eBPF program + * to correctly match the iface name when receiving a packet. + */ + bpf::BpfMap<uint32_t, IfaceValue> mIfaceIndexNameMap; + + /* + * mIfaceStataMap: Store per iface traffic stats gathered from xt_bpf + * filter. + */ + bpf::BpfMap<uint32_t, StatsValue> mIfaceStatsMap; + + /* + * mConfigurationMap: Store the current network policy about uid filtering + * and the current stats map in use. There are two configuration entries in + * the map right now: + * - Entry with UID_RULES_CONFIGURATION_KEY: + * Store the configuration for the current uid rules. It indicates the device + * is in doze/powersave/standby/restricted/low power standby mode. + * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY: + * Stores the current live stats map that kernel program is writing to. + * Userspace can do scraping and cleaning job on the other one depending on the + * current configs. + */ + bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex); + + /* + * mUidOwnerMap: Store uids that are used for bandwidth control uid match. + */ + bpf::BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex); + + /* + * mUidOwnerMap: Store uids that are used for INTERNET permission check. + */ + bpf::BpfMap<uint32_t, uint8_t> mUidPermissionMap GUARDED_BY(mMutex); + + std::unique_ptr<netdutils::NetlinkListenerInterface> mSkDestroyListener; + + netdutils::Status removeRule(uint32_t uid, UidOwnerMatchType match) REQUIRES(mMutex); + + netdutils::Status addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif = 0) + REQUIRES(mMutex); + + std::mutex mMutex; + + netdutils::Status initMaps() EXCLUDES(mMutex); + + // Keep track of uids that have permission UPDATE_DEVICE_STATS so we don't + // need to call back to system server for permission check. + std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex); + + bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex); + + // For testing + friend class TrafficControllerTest; +}; + +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp new file mode 100644 index 0000000..17ee996 --- /dev/null +++ b/service/native/libs/libclat/Android.bp
@@ -0,0 +1,66 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_static { + name: "libclat", + defaults: ["netd_defaults"], + header_libs: [ + "bpf_connectivity_headers", + "libbase_headers", + ], + srcs: [ + "TcUtils.cpp", // TODO: move to frameworks/libs/net + "bpfhelper.cpp", + "clatutils.cpp", + ], + stl: "libc++_static", + static_libs: [ + "libip_checksum", + "libnetdutils", // for netdutils/UidConstants.h in bpf_shared.h + ], + shared_libs: ["liblog"], + export_include_dirs: ["include"], + min_sdk_version: "30", + apex_available: ["com.android.tethering"], +} + +cc_test { + name: "libclat_test", + defaults: ["netd_defaults"], + test_suites: ["device-tests"], + header_libs: [ + "bpf_connectivity_headers", + ], + srcs: [ + "TcUtilsTest.cpp", + "clatutils_test.cpp", + ], + static_libs: [ + "libbase", + "libclat", + "libip_checksum", + "libnetd_test_tun_interface", + "libnetdutils", // for netdutils/UidConstants.h in bpf_shared.h + "libtcutils", + ], + shared_libs: [ + "liblog", + "libnetutils", + ], + require_root: true, +}
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp new file mode 100644 index 0000000..cdfb763 --- /dev/null +++ b/service/native/libs/libclat/TcUtils.cpp
@@ -0,0 +1,390 @@ +/* + * Copyright (C) 2019 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 "TcUtils" + +#include "libclat/TcUtils.h" + +#include <arpa/inet.h> +#include <linux/if.h> +#include <linux/if_arp.h> +#include <linux/netlink.h> +#include <linux/pkt_cls.h> +#include <linux/pkt_sched.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#include <log/log.h> + +#include "android-base/unique_fd.h" + +namespace android { +namespace net { + +using std::max; + +// Sync from system/netd/server/NetlinkCommands.h +const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; +const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; + +static int doSIOCGIF(const std::string& interface, int opt) { + base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)); + + if (ufd < 0) { + const int err = errno; + ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)"); + return -err; + }; + + struct ifreq ifr = {}; + // We use strncpy() instead of strlcpy() since kernel has to be able + // to handle non-zero terminated junk passed in by userspace anyway, + // and this way too long interface names (more than IFNAMSIZ-1 = 15 + // characters plus terminating NULL) will not get truncated to 15 + // characters and zero-terminated and thus potentially erroneously + // match a truncated interface if one were to exist. + strncpy(ifr.ifr_name, interface.c_str(), sizeof(ifr.ifr_name)); + + if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno; + + if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family; + if (opt == SIOCGIFMTU) return ifr.ifr_mtu; + return -EINVAL; +} + +int hardwareAddressType(const std::string& interface) { + return doSIOCGIF(interface, SIOCGIFHWADDR); +} + +int deviceMTU(const std::string& interface) { + return doSIOCGIF(interface, SIOCGIFMTU); +} + +base::Result<bool> isEthernet(const std::string& interface) { + int rv = hardwareAddressType(interface); + if (rv < 0) { + errno = -rv; + return ErrnoErrorf("Get hardware address type of interface {} failed", interface); + } + + switch (rv) { + case ARPHRD_ETHER: + return true; + case ARPHRD_NONE: + case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519 + case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet + return false; + default: + errno = EAFNOSUPPORT; // Address family not supported + return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface); + } +} + +// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol) +// and //system/netd/server/SockDiag.cpp:checkError(fd) +static int sendAndProcessNetlinkResponse(const void* req, int len) { + base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)); + if (fd == -1) { + const int err = errno; + ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)"); + return -err; + } + + static constexpr int on = 1; + int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on)); + if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on); + + // this is needed to get sane strace netlink parsing, it allocates the pid + rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR)); + if (rv) { + const int err = errno; + ALOGE("bind(fd, {AF_NETLINK, 0, 0})"); + return -err; + } + + // we do not want to receive messages from anyone besides the kernel + rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR)); + if (rv) { + const int err = errno; + ALOGE("connect(fd, {AF_NETLINK, 0, 0})"); + return -err; + } + + rv = send(fd, req, len, 0); + if (rv == -1) return -errno; + if (rv != len) return -EMSGSIZE; + + struct { + nlmsghdr h; + nlmsgerr e; + char buf[256]; + } resp = {}; + + rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC); + + if (rv == -1) { + const int err = errno; + ALOGE("recv() failed"); + return -err; + } + + if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { + ALOGE("recv() returned short packet: %d", rv); + return -EMSGSIZE; + } + + if (resp.h.nlmsg_len != (unsigned)rv) { + ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv); + return -EBADMSG; + } + + if (resp.h.nlmsg_type != NLMSG_ERROR) { + ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); + return -EBADMSG; + } + + return resp.e.error; // returns 0 on success +} + +// ADD: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE +// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE +// DEL: nlMsgType=RTM_DELQDISC nlMsgFlags=0 +int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) { + // This is the name of the qdisc we are attaching. + // Some hoop jumping to make this compile time constant with known size, + // so that the structure declaration is well defined at compile time. +#define CLSACT "clsact" + // sizeof() includes the terminating NULL + static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT); + + const struct { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)]; + } kind; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = nlMsgType, + .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags), + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0), + .tcm_parent = TC_H_CLSACT, + }, + .kind = + { + .attr = + { + .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT, + .nla_type = TCA_KIND, + }, + .str = CLSACT, + }, + }; +#undef CLSACT + + return sendAndProcessNetlinkResponse(&req, sizeof(req)); +} + +// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... +// direct-action +int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) { + // This is the name of the filter we're attaching (ie. this is the 'bpf' + // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF. + // + // We go through some hoops in order to make this compile time constants + // so that we can define the struct further down the function with the + // field for this sized correctly already during the build. +#define BPF "bpf" + // sizeof() includes the terminating NULL + static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF); + + // This is to replicate program name suffix used by 'tc' Linux cli + // when it attaches programs. +#define FSOBJ_SUFFIX ":[*fsobj]" + + // This macro expands (from header files) to: + // prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj] + // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces. + // (also compatible with anything that has 0 size L2 header) + static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX; + + // This macro expands (from header files) to: + // prog_clatd_schedcls_ingress6_clat_ether:[*fsobj] + // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces. + // (also compatible with anything that has standard ethernet header) + static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX; + + // This macro expands (from header files) to: + // prog_clatd_schedcls_egress4_clat_rawip:[*fsobj] + // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces. + // (also compatible with anything that has 0 size L2 header) + static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX; + + // This macro expands (from header files) to: + // prog_clatd_schedcls_egress4_clat_ether:[*fsobj] + // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces. + // (also compatible with anything that has standard ethernet header) + static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX; + +#undef FSOBJ_SUFFIX + + // The actual name we'll use is determined at run time via 'ethernet' and 'ingress' + // booleans. We need to compile time allocate enough space in the struct + // hence this macro magic to make sure we have enough space for either + // possibility. In practice some of these are actually the same size. + static constexpr size_t ASCIIZ_MAXLEN_NAME = max({ + sizeof(name_clat_rx_rawip), + sizeof(name_clat_rx_ether), + sizeof(name_clat_tx_rawip), + sizeof(name_clat_tx_ether), + }); + + // These are not compile time constants: 'name' is used in strncpy below + const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip; + const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip; + const char* const name = ingress ? name_clat_rx : name_clat_tx; + + struct { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + __u32 u32; + } fd; + struct { + nlattr attr; + char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)]; + } name; + struct { + nlattr attr; + __u32 u32; + } flags; + } options; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_NEWTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((PRIO_CLAT << 16) | htons(proto)), + }, + .kind = + { + .attr = + { + .nla_len = sizeof(req.kind), + .nla_type = TCA_KIND, + }, + .str = BPF, + }, + .options = + { + .attr = + { + .nla_len = sizeof(req.options), + .nla_type = NLA_F_NESTED | TCA_OPTIONS, + }, + .fd = + { + .attr = + { + .nla_len = sizeof(req.options.fd), + .nla_type = TCA_BPF_FD, + }, + .u32 = static_cast<__u32>(bpfFd), + }, + .name = + { + .attr = + { + .nla_len = sizeof(req.options.name), + .nla_type = TCA_BPF_NAME, + }, + // Visible via 'tc filter show', but + // is overwritten by strncpy below + .str = "placeholder", + }, + .flags = + { + .attr = + { + .nla_len = sizeof(req.options.flags), + .nla_type = TCA_BPF_FLAGS, + }, + .u32 = TCA_BPF_FLAG_ACT_DIRECT, + }, + }, + }; +#undef BPF + + strncpy(req.options.name.str, name, sizeof(req.options.name.str)); + + return sendAndProcessNetlinkResponse(&req, sizeof(req)); +} + +// tc filter del dev .. in/egress prio 4 protocol .. +int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) { + const struct { + nlmsghdr n; + tcmsg t; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_DELTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = (static_cast<uint32_t>(prio) << 16) | + static_cast<uint32_t>(htons(proto)), + }, + }; + + return sendAndProcessNetlinkResponse(&req, sizeof(req)); +} + +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp new file mode 100644 index 0000000..08f3042 --- /dev/null +++ b/service/native/libs/libclat/TcUtilsTest.cpp
@@ -0,0 +1,212 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * TcUtilsTest.cpp - unit tests for TcUtils.cpp + */ + +#include <gtest/gtest.h> + +#include "libclat/TcUtils.h" + +#include <linux/if_arp.h> +#include <stdlib.h> +#include <sys/wait.h> + +#include "bpf/BpfUtils.h" +#include "bpf_shared.h" + +namespace android { +namespace net { + +class TcUtilsTest : public ::testing::Test { + public: + void SetUp() {} +}; + +TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) { + ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if")); +} + +TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) { + ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo")); +} + +// If wireless 'wlan0' interface exists it should be Ethernet. +TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) { + int type = hardwareAddressType("wlan0"); + if (type == -ENODEV) return; + + ASSERT_EQ(ARPHRD_ETHER, type); +} + +// If cellular 'rmnet_data0' interface exists it should +// *probably* not be Ethernet and instead be RawIp. +TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) { + int type = hardwareAddressType("rmnet_data0"); + if (type == -ENODEV) return; + + ASSERT_NE(ARPHRD_ETHER, type); + + // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices. + if (type == 530) return; + + ASSERT_EQ(ARPHRD_RAWIP, type); +} + +TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) { + auto res = isEthernet("not_existing_if"); + ASSERT_FALSE(res.ok()); + ASSERT_EQ(ENODEV, res.error().code()); +} + +TEST_F(TcUtilsTest, IsEthernetOfLoopback) { + auto res = isEthernet("lo"); + ASSERT_FALSE(res.ok()); + ASSERT_EQ(EAFNOSUPPORT, res.error().code()); +} + +// If wireless 'wlan0' interface exists it should be Ethernet. +// See also HardwareAddressTypeOfWireless. +TEST_F(TcUtilsTest, IsEthernetOfWireless) { + auto res = isEthernet("wlan0"); + if (!res.ok() && res.error().code() == ENODEV) return; + + ASSERT_RESULT_OK(res); + ASSERT_TRUE(res.value()); +} + +// If cellular 'rmnet_data0' interface exists it should +// *probably* not be Ethernet and instead be RawIp. +// See also HardwareAddressTypeOfCellular. +TEST_F(TcUtilsTest, IsEthernetOfCellular) { + auto res = isEthernet("rmnet_data0"); + if (!res.ok() && res.error().code() == ENODEV) return; + + ASSERT_RESULT_OK(res); + ASSERT_FALSE(res.value()); +} + +TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) { + ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if")); +} + +TEST_F(TcUtilsTest, DeviceMTUofLoopback) { + ASSERT_EQ(65536, deviceMTU("lo")); +} + +TEST_F(TcUtilsTest, GetClatEgress4MapFd) { + int fd = getClatEgress4MapFd(); + ASSERT_GE(fd, 3); // 0,1,2 - stdin/out/err, thus fd >= 3 + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) { + int fd = getClatEgress4ProgFd(RAWIP); + ASSERT_GE(fd, 3); + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) { + int fd = getClatEgress4ProgFd(ETHER); + ASSERT_GE(fd, 3); + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +TEST_F(TcUtilsTest, GetClatIngress6MapFd) { + int fd = getClatIngress6MapFd(); + ASSERT_GE(fd, 3); // 0,1,2 - stdin/out/err, thus fd >= 3 + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) { + int fd = getClatIngress6ProgFd(RAWIP); + ASSERT_GE(fd, 3); + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) { + int fd = getClatIngress6ProgFd(ETHER); + ASSERT_GE(fd, 3); + EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD)); + close(fd); +} + +// See Linux kernel source in include/net/flow.h +#define LOOPBACK_IFINDEX 1 + +TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) { + // This attaches and detaches a configuration-less and thus no-op clsact + // qdisc to loopback interface (and it takes fractions of a second) + EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX)); +} + +static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) { + // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation... + const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL; + + int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet); + ASSERT_GE(clatBpfFd, 3); + + // This attaches and detaches a clsact plus ebpf program to loopback + // interface, but it should not affect traffic by virtue of us not + // actually populating the ebpf control map. + // Furthermore: it only takes fractions of a second. + EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX)); + EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX)); + EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX)); + if (ingress) { + EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet)); + EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX)); + } else { + EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet)); + EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX)); + } + EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX)); + EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX)); + EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX)); + + close(clatBpfFd); +} + +TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) { + checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP); +} + +TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) { + checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER); +} + +TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) { + checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP); +} + +TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) { + checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER); +} + +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp new file mode 100644 index 0000000..6e230d0 --- /dev/null +++ b/service/native/libs/libclat/bpfhelper.cpp
@@ -0,0 +1,231 @@ +/* + * 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. + * + * main.c - main function + */ +#define LOG_TAG "bpfhelper" + +#include "libclat/bpfhelper.h" + +#include <android-base/unique_fd.h> +#include <log/log.h> + +#include "bpf/BpfMap.h" +#include "libclat/TcUtils.h" + +#define DEVICEPREFIX "v4-" + +using android::base::unique_fd; +using android::net::RAWIP; +using android::net::getClatEgress4MapFd; +using android::net::getClatIngress6MapFd; +using android::net::getClatEgress4ProgFd; +using android::net::getClatIngress6ProgFd; +using android::net::tcQdiscAddDevClsact; +using android::net::tcFilterAddDevEgressClatIpv4; +using android::net::tcFilterAddDevIngressClatIpv6; +using android::net::tcFilterDelDevEgressClatIpv4; +using android::net::tcFilterDelDevIngressClatIpv6; +using android::bpf::BpfMap; + +BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map; +BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map; + +namespace android { +namespace net { +namespace clat { + +// TODO: have a clearMap function to remove all stubs while system server crash. +// For long term, move bpf access into java and map initialization should live +// ClatCoordinator constructor. +int initMaps(void) { + int rv = getClatEgress4MapFd(); + if (rv < 0) { + ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv)); + return -rv; + } + mClatEgress4Map.reset(rv); + + rv = getClatIngress6MapFd(); + if (rv < 0) { + ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv)); + mClatEgress4Map.reset(-1); + return -rv; + } + mClatIngress6Map.reset(rv); + + return 0; +} + +void maybeStartBpf(const ClatdTracker& tracker) { + auto isEthernet = android::net::isEthernet(tracker.iface); + if (!isEthernet.ok()) { + ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex, + isEthernet.error().message().c_str()); + return; + } + + // This program will be attached to the v4-* interface which is a TUN and thus always rawip. + int rv = getClatEgress4ProgFd(RAWIP); + if (rv < 0) { + ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv)); + return; + } + unique_fd txRawIpProgFd(rv); + + rv = getClatIngress6ProgFd(isEthernet.value()); + if (rv < 0) { + ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv)); + return; + } + unique_fd rxProgFd(rv); + + ClatEgress4Key txKey = { + .iif = tracker.v4ifIndex, + .local4 = tracker.v4, + }; + ClatEgress4Value txValue = { + .oif = tracker.ifIndex, + .local6 = tracker.v6, + .pfx96 = tracker.pfx96, + .oifIsEthernet = isEthernet.value(), + }; + + auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY); + if (!ret.ok()) { + ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code())); + return; + } + + ClatIngress6Key rxKey = { + .iif = tracker.ifIndex, + .pfx96 = tracker.pfx96, + .local6 = tracker.v6, + }; + ClatIngress6Value rxValue = { + // TODO: move all the clat code to eBPF and remove the tun interface entirely. + .oif = tracker.v4ifIndex, + .local4 = tracker.v4, + }; + + ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY); + if (!ret.ok()) { + ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code())); + ret = mClatEgress4Map.deleteValue(txKey); + if (!ret.ok()) + ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code())); + return; + } + + // We do tc setup *after* populating the maps, so scanning through them + // can always be used to tell us what needs cleanup. + + // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork. + // But clat is started before the v4- interface is added to the network. The clat startup have + // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf. + // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController. + rv = tcQdiscAddDevClsact(tracker.v4ifIndex); + if (rv) { + ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface, + strerror(-rv)); + ret = mClatEgress4Map.deleteValue(txKey); + if (!ret.ok()) + ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code())); + ret = mClatIngress6Map.deleteValue(rxKey); + if (!ret.ok()) + ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code())); + return; + } + + rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP); + if (rv) { + ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex, + tracker.v4iface, strerror(-rv)); + + // The v4- interface clsact is not deleted for unwinding error because once it is created + // with interface addition, the lifetime is till interface deletion. Moreover, the clsact + // has no clat filter now. It should not break anything. + + ret = mClatEgress4Map.deleteValue(txKey); + if (!ret.ok()) + ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code())); + ret = mClatIngress6Map.deleteValue(rxKey); + if (!ret.ok()) + ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code())); + return; + } + + rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value()); + if (rv) { + ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex, + tracker.iface, isEthernet.value(), strerror(-rv)); + rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex); + if (rv) { + ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex, + tracker.v4iface, strerror(-rv)); + } + + // The v4- interface clsact is not deleted. See the reason in the error unwinding code of + // the egress filter attaching of v4- tun interface. + + ret = mClatEgress4Map.deleteValue(txKey); + if (!ret.ok()) + ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code())); + ret = mClatIngress6Map.deleteValue(rxKey); + if (!ret.ok()) + ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code())); + return; + } + + // success +} + +void maybeStopBpf(const ClatdTracker& tracker) { + int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex); + if (rv < 0) { + ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface, + strerror(-rv)); + } + + rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex); + if (rv < 0) { + ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex, + tracker.v4iface, strerror(-rv)); + } + + // We cleanup the maps last, so scanning through them can be used to + // determine what still needs cleanup. + + ClatEgress4Key txKey = { + .iif = tracker.v4ifIndex, + .local4 = tracker.v4, + }; + + auto ret = mClatEgress4Map.deleteValue(txKey); + if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code())); + + ClatIngress6Key rxKey = { + .iif = tracker.ifIndex, + .pfx96 = tracker.pfx96, + .local6 = tracker.v6, + }; + + ret = mClatIngress6Map.deleteValue(rxKey); + if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code())); +} + +} // namespace clat +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/clatutils.cpp b/service/native/libs/libclat/clatutils.cpp new file mode 100644 index 0000000..4a125ba --- /dev/null +++ b/service/native/libs/libclat/clatutils.cpp
@@ -0,0 +1,268 @@ +// 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 "clatutils" + +#include "libclat/clatutils.h" + +#include <errno.h> +#include <linux/filter.h> +#include <linux/if_packet.h> +#include <linux/if_tun.h> +#include <log/log.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +extern "C" { +#include "checksum.h" +} + +// Sync from external/android-clat/clatd.h +#define MAXMTU 65536 +#define PACKETLEN (MAXMTU + sizeof(struct tun_pi)) + +// Sync from system/netd/include/netid_client.h. +#define MARK_UNSET 0u + +namespace android { +namespace net { +namespace clat { + +bool isIpv4AddressFree(in_addr_t addr) { + int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s == -1) { + return 0; + } + + // Attempt to connect to the address. If the connection succeeds and getsockname returns the + // same then the address is already assigned to the system and we can't use it. + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_port = htons(53), + .sin_addr = {addr}, + }; + socklen_t len = sizeof(sin); + bool inuse = connect(s, (struct sockaddr*)&sin, sizeof(sin)) == 0 && + getsockname(s, (struct sockaddr*)&sin, &len) == 0 && (size_t)len >= sizeof(sin) && + sin.sin_addr.s_addr == addr; + + close(s); + return !inuse; +} + +// Picks a free IPv4 address, starting from ip and trying all addresses in the prefix in order. +// ip - the IP address from the configuration file +// prefixlen - the length of the prefix from which addresses may be selected. +// returns: the IPv4 address, or INADDR_NONE if no addresses were available +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen) { + return selectIpv4AddressInternal(ip, prefixlen, isIpv4AddressFree); +} + +// Only allow testing to use this function directly. Otherwise call selectIpv4Address(ip, pfxlen) +// which has applied valid isIpv4AddressFree function pointer. +in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, + isIpv4AddrFreeFn isIpv4AddressFreeFunc) { + // Impossible! Only test allows to apply fn. + if (isIpv4AddressFreeFunc == nullptr) { + return INADDR_NONE; + } + + // Don't accept prefixes that are too large because we scan addresses one by one. + if (prefixlen < 16 || prefixlen > 32) { + return INADDR_NONE; + } + + // All these are in host byte order. + in_addr_t mask = 0xffffffff >> (32 - prefixlen) << (32 - prefixlen); + in_addr_t ipv4 = ntohl(ip.s_addr); + in_addr_t first_ipv4 = ipv4; + in_addr_t prefix = ipv4 & mask; + + // Pick the first IPv4 address in the pool, wrapping around if necessary. + // So, for example, 192.0.0.4 -> 192.0.0.5 -> 192.0.0.6 -> 192.0.0.7 -> 192.0.0.0. + do { + if (isIpv4AddressFreeFunc(htonl(ipv4))) { + return htonl(ipv4); + } + ipv4 = prefix | ((ipv4 + 1) & ~mask); + } while (ipv4 != first_ipv4); + + return INADDR_NONE; +} + +// Alters the bits in the IPv6 address to make them checksum neutral with v4 and nat64Prefix. +void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix) { + // Fill last 8 bytes of IPv6 address with random bits. + arc4random_buf(&v6->s6_addr[8], 8); + + // Make the IID checksum-neutral. That is, make it so that: + // checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6) + // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4): + // checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix) + // Do this by adjusting the two bytes in the middle of the IID. + + uint16_t middlebytes = (v6->s6_addr[11] << 8) + v6->s6_addr[12]; + + uint32_t c1 = ip_checksum_add(0, &v4, sizeof(v4)); + uint32_t c2 = ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) + + ip_checksum_add(0, v6, sizeof(*v6)); + + uint16_t delta = ip_checksum_adjust(middlebytes, c1, c2); + v6->s6_addr[11] = delta >> 8; + v6->s6_addr[12] = delta & 0xff; +} + +// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix. +int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix, + in6_addr* v6) { + int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s == -1) return -errno; + + if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface) + 1) == -1) { + close(s); + return -errno; + } + + sockaddr_in6 sin6 = {.sin6_family = AF_INET6, .sin6_addr = nat64Prefix}; + if (connect(s, reinterpret_cast<struct sockaddr*>(&sin6), sizeof(sin6)) == -1) { + close(s); + return -errno; + } + + socklen_t len = sizeof(sin6); + if (getsockname(s, reinterpret_cast<struct sockaddr*>(&sin6), &len) == -1) { + close(s); + return -errno; + } + + *v6 = sin6.sin6_addr; + + if (IN6_IS_ADDR_UNSPECIFIED(v6) || IN6_IS_ADDR_LOOPBACK(v6) || IN6_IS_ADDR_LINKLOCAL(v6) || + IN6_IS_ADDR_SITELOCAL(v6) || IN6_IS_ADDR_ULA(v6)) { + close(s); + return -ENETUNREACH; + } + + makeChecksumNeutral(v6, v4, nat64Prefix); + close(s); + + return 0; +} + +int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark) { + // Create an IPv6 UDP socket. + int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (s < 0) { + int ret = errno; + ALOGE("socket(AF_INET6, SOCK_DGRAM, 0) failed: %s", strerror(errno)); + return -ret; + } + + // Socket's mark affects routing decisions (network selection) + if ((mark != MARK_UNSET) && setsockopt(s, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) { + int ret = errno; + ALOGE("setsockopt(SOL_SOCKET, SO_MARK) failed: %s", strerror(errno)); + close(s); + return -ret; + } + + // Try to connect udp socket to plat_subnet(96 bits):plat_suffix(32 bits) + struct sockaddr_in6 dst = { + .sin6_family = AF_INET6, + .sin6_addr = *plat_subnet, + }; + dst.sin6_addr.s6_addr32[3] = plat_suffix; + if (connect(s, (struct sockaddr*)&dst, sizeof(dst))) { + int ret = errno; + ALOGE("connect() failed: %s", strerror(errno)); + close(s); + return -ret; + } + + // Fetch the socket's IPv6 mtu - this is effectively fetching mtu from routing table + int mtu; + socklen_t sz_mtu = sizeof(mtu); + if (getsockopt(s, SOL_IPV6, IPV6_MTU, &mtu, &sz_mtu)) { + int ret = errno; + ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) failed: %s", strerror(errno)); + close(s); + return -ret; + } + if (sz_mtu != sizeof(mtu)) { + ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) returned unexpected size: %d", sz_mtu); + close(s); + return -EFAULT; + } + close(s); + + return mtu; +} + +/* function: configure_packet_socket + * Binds the packet socket and attaches the receive filter to it. + * sock - the socket to configure + * addr - the IP address to filter + * ifindex - index of interface to add the filter to + * returns: 0 on success, -errno on failure + */ +int configure_packet_socket(int sock, in6_addr* addr, int ifindex) { + uint32_t* ipv6 = addr->s6_addr32; + + // clang-format off + struct sock_filter filter_code[] = { + // Load the first four bytes of the IPv6 destination address (starts 24 bytes in). + // Compare it against the first four bytes of our IPv6 address, in host byte order (BPF loads + // are always in host byte order). If it matches, continue with next instruction (JMP 0). If it + // doesn't match, jump ahead to statement that returns 0 (ignore packet). Repeat for the other + // three words of the IPv6 address, and if they all match, return PACKETLEN (accept packet). + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 24), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[0]), 0, 7), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 28), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[1]), 0, 5), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 32), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[2]), 0, 3), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 36), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[3]), 0, 1), + BPF_STMT(BPF_RET | BPF_K, PACKETLEN), + BPF_STMT(BPF_RET | BPF_K, 0), + }; + // clang-format on + struct sock_fprog filter = {sizeof(filter_code) / sizeof(filter_code[0]), filter_code}; + + if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter))) { + int res = errno; + ALOGE("attach packet filter failed: %s", strerror(errno)); + return -res; + } + + struct sockaddr_ll sll = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_IPV6), + .sll_ifindex = ifindex, + .sll_pkttype = + PACKET_OTHERHOST, // The 464xlat IPv6 address is not assigned to the kernel. + }; + if (bind(sock, (struct sockaddr*)&sll, sizeof(sll))) { + int res = errno; + ALOGE("binding packet socket: %s", strerror(errno)); + return -res; + } + + return 0; +} + +} // namespace clat +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp new file mode 100644 index 0000000..4153e19 --- /dev/null +++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -0,0 +1,187 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "libclat/clatutils.h" + +#include <android-base/stringprintf.h> +#include <arpa/inet.h> +#include <gtest/gtest.h> +#include <linux/if_packet.h> +#include <linux/if_tun.h> +#include "tun_interface.h" + +extern "C" { +#include "checksum.h" +} + +// Default translation parameters. +static const char kIPv4LocalAddr[] = "192.0.0.4"; + +namespace android { +namespace net { +namespace clat { + +using android::net::TunInterface; +using base::StringPrintf; + +class ClatUtils : public ::testing::Test {}; + +// Mock functions for isIpv4AddressFree. +bool neverFree(in_addr_t /* addr */) { + return 0; +} +bool alwaysFree(in_addr_t /* addr */) { + return 1; +} +bool only2Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) == 2; +} +bool over6Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) >= 6; +} +bool only10Free(in_addr_t addr) { + return (ntohl(addr) & 0xff) == 10; +} + +// Apply mocked isIpv4AddressFree function for selectIpv4Address test. +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen, + isIpv4AddrFreeFn fn /* mocked function */) { + // Call internal function to replace isIpv4AddressFreeFn for testing. + return selectIpv4AddressInternal(ip, prefixlen, fn); +} + +TEST_F(ClatUtils, SelectIpv4Address) { + struct in_addr addr; + + inet_pton(AF_INET, kIPv4LocalAddr, &addr); + + // If no addresses are free, return INADDR_NONE. + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, neverFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 16, neverFree)); + + // If the configured address is free, pick that. But a prefix that's too big is invalid. + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 29, alwaysFree)); + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 20, alwaysFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 15, alwaysFree)); + + // A prefix length of 32 works, but anything above it is invalid. + EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 32, alwaysFree)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 33, alwaysFree)); + + // If another address is free, pick it. + EXPECT_EQ(inet_addr("192.0.0.6"), selectIpv4Address(addr, 29, over6Free)); + + // Check that we wrap around to addresses that are lower than the first address. + EXPECT_EQ(inet_addr("192.0.0.2"), selectIpv4Address(addr, 29, only2Free)); + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 30, only2Free)); + + // If a free address exists outside the prefix, we don't pick it. + EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, only10Free)); + EXPECT_EQ(inet_addr("192.0.0.10"), selectIpv4Address(addr, 24, only10Free)); + + // Now try using the real function which sees if IP addresses are free using bind(). + // Assume that the machine running the test has the address 127.0.0.1, but not 8.8.8.8. + addr.s_addr = inet_addr("8.8.8.8"); + EXPECT_EQ(inet_addr("8.8.8.8"), selectIpv4Address(addr, 29)); + + addr.s_addr = inet_addr("127.0.0.1"); + EXPECT_EQ(inet_addr("127.0.0.2"), selectIpv4Address(addr, 29)); +} + +TEST_F(ClatUtils, MakeChecksumNeutral) { + // We can't test generateIPv6Address here since it requires manipulating routing, which we can't + // do without talking to the real netd on the system. + uint32_t rand = arc4random_uniform(0xffffffff); + uint16_t rand1 = rand & 0xffff; + uint16_t rand2 = (rand >> 16) & 0xffff; + std::string v6PrefixStr = StringPrintf("2001:db8:%x:%x", rand1, rand2); + std::string v6InterfaceAddrStr = StringPrintf("%s::%x:%x", v6PrefixStr.c_str(), rand2, rand1); + std::string nat64PrefixStr = StringPrintf("2001:db8:%x:%x::", rand2, rand1); + + in_addr v4 = {inet_addr(kIPv4LocalAddr)}; + in6_addr v6InterfaceAddr; + ASSERT_TRUE(inet_pton(AF_INET6, v6InterfaceAddrStr.c_str(), &v6InterfaceAddr)); + in6_addr nat64Prefix; + ASSERT_TRUE(inet_pton(AF_INET6, nat64PrefixStr.c_str(), &nat64Prefix)); + + // Generate a boatload of random IIDs. + int onebits = 0; + uint64_t prev_iid = 0; + for (int i = 0; i < 100000; i++) { + in6_addr v6 = v6InterfaceAddr; + makeChecksumNeutral(&v6, v4, nat64Prefix); + + // Check the generated IP address is in the same prefix as the interface IPv6 address. + EXPECT_EQ(0, memcmp(&v6, &v6InterfaceAddr, 8)); + + // Check that consecutive IIDs are not the same. + uint64_t iid = *(uint64_t*)(&v6.s6_addr[8]); + ASSERT_TRUE(iid != prev_iid) + << "Two consecutive random IIDs are the same: " << std::showbase << std::hex << iid + << "\n"; + prev_iid = iid; + + // Check that the IID is checksum-neutral with the NAT64 prefix and the + // local prefix. + uint16_t c1 = ip_checksum_finish(ip_checksum_add(0, &v4, sizeof(v4))); + uint16_t c2 = ip_checksum_finish(ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) + + ip_checksum_add(0, &v6, sizeof(v6))); + + if (c1 != c2) { + char v6Str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &v6, v6Str, sizeof(v6Str)); + FAIL() << "Bad IID: " << v6Str << " not checksum-neutral with " << kIPv4LocalAddr + << " and " << nat64PrefixStr.c_str() << std::showbase << std::hex + << "\n IPv4 checksum: " << c1 << "\n IPv6 checksum: " << c2 << "\n"; + } + + // Check that IIDs are roughly random and use all the bits by counting the + // total number of bits set to 1 in a random sample of 100000 generated IIDs. + onebits += __builtin_popcountll(*(uint64_t*)&iid); + } + EXPECT_LE(3190000, onebits); + EXPECT_GE(3210000, onebits); +} + +TEST_F(ClatUtils, DetectMtu) { + // ::1 with bottom 32 bits set to 1 is still ::1 which routes via lo with mtu of 64KiB + ASSERT_EQ(detect_mtu(&in6addr_loopback, htonl(1), 0 /*MARK_UNSET*/), 65536); +} + +TEST_F(ClatUtils, ConfigurePacketSocket) { + // Create an interface for configure_packet_socket to attach socket filter to. + TunInterface v6Iface; + ASSERT_EQ(0, v6Iface.init()); + + int s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6)); + EXPECT_LE(0, s); + struct in6_addr addr6; + EXPECT_EQ(1, inet_pton(AF_INET6, "2001:db8::f00", &addr6)); + EXPECT_EQ(0, configure_packet_socket(s, &addr6, v6Iface.ifindex())); + + // Check that the packet socket is bound to the interface. We can't check the socket filter + // because there is no way to fetch it from the kernel. + sockaddr_ll sll; + socklen_t len = sizeof(sll); + ASSERT_EQ(0, getsockname(s, reinterpret_cast<sockaddr*>(&sll), &len)); + EXPECT_EQ(htons(ETH_P_IPV6), sll.sll_protocol); + EXPECT_EQ(sll.sll_ifindex, v6Iface.ifindex()); + + close(s); + v6Iface.destroy(); +} + +} // namespace clat +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h new file mode 100644 index 0000000..212838e --- /dev/null +++ b/service/native/libs/libclat/include/libclat/TcUtils.h
@@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <android-base/result.h> +#include <errno.h> +#include <linux/if_ether.h> +#include <linux/if_link.h> +#include <linux/rtnetlink.h> + +#include <string> + +#include "bpf/BpfUtils.h" +#include "bpf_shared.h" + +namespace android { +namespace net { + +// For better code clarity - do not change values - used for booleans like +// with_ethernet_header or isEthernet. +constexpr bool RAWIP = false; +constexpr bool ETHER = true; + +// For better code clarity when used for 'bool ingress' parameter. +constexpr bool EGRESS = false; +constexpr bool INGRESS = true; + +// The priority of clat hook - must be after tethering. +constexpr uint16_t PRIO_CLAT = 4; + +// this returns an ARPHRD_* constant or a -errno +int hardwareAddressType(const std::string& interface); + +// return MTU or -errno +int deviceMTU(const std::string& interface); + +base::Result<bool> isEthernet(const std::string& interface); + +inline int getClatEgress4MapFd(void) { + const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH); + return (fd == -1) ? -errno : fd; +} + +inline int getClatEgress4ProgFd(bool with_ethernet_header) { + const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH + : CLAT_EGRESS4_PROG_RAWIP_PATH); + return (fd == -1) ? -errno : fd; +} + +inline int getClatIngress6MapFd(void) { + const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH); + return (fd == -1) ? -errno : fd; +} + +inline int getClatIngress6ProgFd(bool with_ethernet_header) { + const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH + : CLAT_INGRESS6_PROG_RAWIP_PATH); + return (fd == -1) ? -errno : fd; +} + +int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags); + +inline int tcQdiscAddDevClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE); +} + +inline int tcQdiscReplaceDevClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE); +} + +inline int tcQdiscDelDevClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0); +} + +// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... +// direct-action +int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet); + +// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action +inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) { + return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet); +} + +// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action +inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) { + return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet); +} + +// tc filter del dev .. in/egress prio .. protocol .. +int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto); + +// tc filter del dev .. ingress prio 4 protocol ipv6 +inline int tcFilterDelDevIngressClatIpv6(int ifIndex) { + return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6); +} + +// tc filter del dev .. egress prio 4 protocol ip +inline int tcFilterDelDevEgressClatIpv4(int ifIndex) { + return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP); +} + +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h new file mode 100644 index 0000000..c0328c0 --- /dev/null +++ b/service/native/libs/libclat/include/libclat/bpfhelper.h
@@ -0,0 +1,40 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include <arpa/inet.h> +#include <linux/if.h> + +namespace android { +namespace net { +namespace clat { + +struct ClatdTracker { + unsigned ifIndex; + char iface[IFNAMSIZ]; + unsigned v4ifIndex; + char v4iface[IFNAMSIZ]; + in_addr v4; + in6_addr v6; + in6_addr pfx96; +}; + +int initMaps(void); +void maybeStartBpf(const ClatdTracker& tracker); +void maybeStopBpf(const ClatdTracker& tracker); + +} // namespace clat +} // namespace net +} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/clatutils.h b/service/native/libs/libclat/include/libclat/clatutils.h new file mode 100644 index 0000000..812c86e --- /dev/null +++ b/service/native/libs/libclat/include/libclat/clatutils.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 <netinet/in.h> +#include <netinet/in6.h> + +namespace android { +namespace net { +namespace clat { + +bool isIpv4AddressFree(in_addr_t addr); +in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen); +void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix); +int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix, + in6_addr* v6); +int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark); +int configure_packet_socket(int sock, in6_addr* addr, int ifindex); + +// For testing +typedef bool (*isIpv4AddrFreeFn)(in_addr_t); +in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, isIpv4AddrFreeFn fn); + +} // namespace clat +} // namespace net +} // namespace android
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java new file mode 100644 index 0000000..7a3bab3 --- /dev/null +++ b/service/src/com/android/server/BpfNetMaps.java
@@ -0,0 +1,316 @@ +/* + * 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; + +import android.net.INetd; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.system.Os; +import android.util.Log; + +import com.android.modules.utils.build.SdkLevel; + +/** + * BpfNetMaps is responsible for providing traffic controller relevant functionality. + * + * {@hide} + */ +public class BpfNetMaps { + private static final String TAG = "BpfNetMaps"; + private final INetd mNetd; + // Use legacy netd for releases before T. + private static final boolean USE_NETD = !SdkLevel.isAtLeastT(); + private static boolean sInitialized = false; + + /** + * Initializes the class if it is not already initialized. This method will open maps but not + * cause any other effects. This method may be called multiple times on any thread. + */ + private static synchronized void ensureInitialized() { + if (sInitialized) return; + if (!USE_NETD) { + System.loadLibrary("service-connectivity"); + native_init(); + } + sInitialized = true; + } + + public BpfNetMaps(INetd netd) { + ensureInitialized(); + mNetd = netd; + } + + private void maybeThrow(final int err, final String msg) { + if (err != 0) { + throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err)); + } + } + + /** + * Add naughty app bandwidth rule for specific app + * + * @param uid uid of target app + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void addNaughtyApp(final int uid) throws RemoteException { + if (USE_NETD) { + mNetd.bandwidthAddNaughtyApp(uid); + return; + } + final int err = native_addNaughtyApp(uid); + maybeThrow(err, "Unable to add naughty app"); + } + + /** + * Remove naughty app bandwidth rule for specific app + * + * @param uid uid of target app + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void removeNaughtyApp(final int uid) throws RemoteException { + if (USE_NETD) { + mNetd.bandwidthRemoveNaughtyApp(uid); + return; + } + final int err = native_removeNaughtyApp(uid); + maybeThrow(err, "Unable to remove naughty app"); + } + + /** + * Add nice app bandwidth rule for specific app + * + * @param uid uid of target app + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void addNiceApp(final int uid) throws RemoteException { + if (USE_NETD) { + mNetd.bandwidthAddNiceApp(uid); + return; + } + final int err = native_addNiceApp(uid); + maybeThrow(err, "Unable to add nice app"); + } + + /** + * Remove nice app bandwidth rule for specific app + * + * @param uid uid of target app + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void removeNiceApp(final int uid) throws RemoteException { + if (USE_NETD) { + mNetd.bandwidthRemoveNiceApp(uid); + return; + } + final int err = native_removeNiceApp(uid); + maybeThrow(err, "Unable to remove nice app"); + } + + /** + * Set target firewall child chain + * + * @param childChain target chain to enable + * @param enable whether to enable or disable child chain. + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void setChildChain(final int childChain, final boolean enable) throws RemoteException { + if (USE_NETD) { + mNetd.firewallEnableChildChain(childChain, enable); + return; + } + final int err = native_setChildChain(childChain, enable); + maybeThrow(err, "Unable to set child chain"); + } + + /** + * Replaces the contents of the specified UID-based firewall chain. + * + * The chain may be an allowlist chain or a denylist chain. A denylist chain contains DROP + * rules for the specified UIDs and a RETURN rule at the end. An allowlist chain contains RETURN + * rules for the system UID range (0 to {@code UID_APP} - 1), RETURN rules for the specified + * UIDs, and a DROP rule at the end. The chain will be created if it does not exist. + * + * @param chainName The name of the chain to replace. + * @param isAllowlist Whether this is an allowlist or denylist chain. + * @param uids The list of UIDs to allow/deny. + * @return 0 if the chain was successfully replaced, errno otherwise. + * @throws RemoteException when netd has crashed. + */ + public int replaceUidChain(final String chainName, final boolean isAllowlist, + final int[] uids) throws RemoteException { + if (USE_NETD) { + mNetd.firewallReplaceUidChain(chainName, isAllowlist, uids); + return 0; + } + final int err = native_replaceUidChain(chainName, isAllowlist, uids); + if (err != 0) { + Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err)); + } + return -err; + } + + /** + * Set firewall rule for uid + * + * @param childChain target chain + * @param uid uid to allow/deny + * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void setUidRule(final int childChain, final int uid, final int firewallRule) + throws RemoteException { + if (USE_NETD) { + mNetd.firewallSetUidRule(childChain, uid, firewallRule); + return; + } + final int err = native_setUidRule(childChain, uid, firewallRule); + maybeThrow(err, "Unable to set uid rule"); + } + + /** + * Add ingress interface filtering rules to a list of UIDs + * + * For a given uid, once a filtering rule is added, the kernel will only allow packets from the + * allowed interface and loopback to be sent to the list of UIDs. + * + * Calling this method on one or more UIDs with an existing filtering rule but a different + * interface name will result in the filtering rule being updated to allow the new interface + * instead. Otherwise calling this method will not affect existing rules set on other UIDs. + * + * @param ifName the name of the interface on which the filtering rules will allow packets to + * be received. + * @param uids an array of UIDs which the filtering rules will be set + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException { + if (USE_NETD) { + mNetd.firewallAddUidInterfaceRules(ifName, uids); + return; + } + final int err = native_addUidInterfaceRules(ifName, uids); + maybeThrow(err, "Unable to add uid interface rules"); + } + + /** + * Remove ingress interface filtering rules from a list of UIDs + * + * Clear the ingress interface filtering rules from the list of UIDs which were previously set + * by addUidInterfaceRules(). Ignore any uid which does not have filtering rule. + * + * @param uids an array of UIDs from which the filtering rules will be removed + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void removeUidInterfaceRules(final int[] uids) throws RemoteException { + if (USE_NETD) { + mNetd.firewallRemoveUidInterfaceRules(uids); + return; + } + final int err = native_removeUidInterfaceRules(uids); + maybeThrow(err, "Unable to remove uid interface rules"); + } + + /** + * Request netd to change the current active network stats map. + * + * @throws RemoteException when netd has crashed. + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void swapActiveStatsMap() throws RemoteException { + if (USE_NETD) { + mNetd.trafficSwapActiveStatsMap(); + return; + } + final int err = native_swapActiveStatsMap(); + maybeThrow(err, "Unable to swap active stats map"); + } + + /** + * Assigns android.permission.INTERNET and/or android.permission.UPDATE_DEVICE_STATS to the uids + * specified. Or remove all permissions from the uids. + * + * @param permissions The permission to grant, it could be either PERMISSION_INTERNET and/or + * PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then + * revoke all permissions for the uids. + * @param uids uid of users to grant permission + * @throws RemoteException when netd has crashed. + */ + public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException { + if (USE_NETD) { + mNetd.trafficSetNetPermForUids(permissions, uids); + return; + } + native_setPermissionForUids(permissions, uids); + } + + /** + * Set counter set for uid + * + * @param counterSet either SET_DEFAULT or SET_FOREGROUND + * @param uid uid to foreground/background + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void setCounterSet(final int counterSet, final int uid) { + final int err = native_setCounterSet(counterSet, uid); + maybeThrow(err, "setCounterSet failed"); + } + + /** + * Reset Uid stats + * + * @param tag default 0 + * @param uid given uid to be clear + * @throws ServiceSpecificException in case of failure, with an error code indicating the + * cause of the failure. + */ + public void deleteTagData(final int tag, final int uid) { + final int err = native_deleteTagData(tag, uid); + maybeThrow(err, "deleteTagData failed"); + } + + private static native void native_init(); + private native int native_addNaughtyApp(int uid); + private native int native_removeNaughtyApp(int uid); + private native int native_addNiceApp(int uid); + private native int native_removeNiceApp(int uid); + private native int native_setChildChain(int childChain, boolean enable); + private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids); + private native int native_setUidRule(int childChain, int uid, int firewallRule); + private native int native_addUidInterfaceRules(String ifName, int[] uids); + private native int native_removeUidInterfaceRules(int[] uids); + private native int native_swapActiveStatsMap(); + private native void native_setPermissionForUids(int permissions, int[] uids); + private native int native_setCounterSet(int counterSet, int uid); + private native int native_deleteTagData(int tag, int uid); +}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 578fabe..d833bc2 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java
@@ -73,6 +73,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; @@ -124,6 +126,7 @@ import android.net.ConnectivitySettingsManager; import android.net.DataStallReportParcelable; import android.net.DnsResolverServiceManager; +import android.net.DscpPolicy; import android.net.ICaptivePortal; import android.net.IConnectivityDiagnosticsCallback; import android.net.IConnectivityManager; @@ -218,6 +221,7 @@ import android.os.UserManager; import android.provider.Settings; import android.sysprop.NetworkProperties; +import android.system.ErrnoException; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -245,9 +249,11 @@ import com.android.net.module.util.PermissionUtils; import com.android.net.module.util.netlink.InetDiagMessage; import com.android.server.connectivity.AutodestructReference; +import com.android.server.connectivity.CarrierPrivilegeAuthenticator; import com.android.server.connectivity.ConnectivityFlags; import com.android.server.connectivity.DnsManager; import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate; +import com.android.server.connectivity.DscpPolicyTracker; import com.android.server.connectivity.FullScore; import com.android.server.connectivity.KeepaliveTracker; import com.android.server.connectivity.LingerMonitor; @@ -262,6 +268,7 @@ import com.android.server.connectivity.ProfileNetworkPreferenceList; import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; +import com.android.server.connectivity.UidRangeUtils; import libcore.io.IoUtils; @@ -386,9 +393,11 @@ protected IDnsResolver mDnsResolver; @VisibleForTesting protected INetd mNetd; + private DscpPolicyTracker mDscpPolicyTracker = null; private NetworkStatsManager mStatsManager; private NetworkPolicyManager mPolicyManager; private final NetdCallback mNetdCallback; + private final BpfNetMaps mBpfNetMaps; /** * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple @@ -431,8 +440,6 @@ * Requests that don't code for a per-app preference use PREFERENCE_ORDER_INVALID. * The default request uses PREFERENCE_ORDER_DEFAULT. */ - // Bound for the lowest valid preference order. - static final int PREFERENCE_ORDER_LOWEST = 999; // Used when sending to netd to code for "no order". static final int PREFERENCE_ORDER_NONE = 0; // Order for requests that don't code for a per-app preference. As it is @@ -440,11 +447,6 @@ // PREFERENCE_ORDER_NONE when sending to netd. @VisibleForTesting static final int PREFERENCE_ORDER_INVALID = Integer.MAX_VALUE; - // Order for the default internet request. Since this must always have the - // lowest priority, its value is larger than the largest acceptable value. As - // it is out of the valid range, the corresponding order should be - // PREFERENCE_ORDER_NONE when sending to netd. - static final int PREFERENCE_ORDER_DEFAULT = 1000; // As a security feature, VPNs have the top priority. static final int PREFERENCE_ORDER_VPN = 0; // Netd supports only 0 for VPN. // Order of per-app OEM preference. See {@link #setOemNetworkPreference}. @@ -459,6 +461,13 @@ // See {@link ConnectivitySettingsManager#setMobileDataPreferredUids} @VisibleForTesting static final int PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED = 30; + // Preference order that signifies the network shouldn't be set as a default network for + // the UIDs, only give them access to it. TODO : replace this with a boolean + // in NativeUidRangeConfig + @VisibleForTesting + static final int PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT = 999; + // Bound for the lowest valid preference order. + static final int PREFERENCE_ORDER_LOWEST = 999; /** * used internally to clear a wakelock when transitioning @@ -586,6 +595,13 @@ // Handle private DNS validation status updates. private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38; + /** + * used to remove a network request, either a listener or a real request and call unavailable + * arg1 = UID of caller + * obj = NetworkRequest + */ + private static final int EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE = 39; + /** * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has * been tested. @@ -753,6 +769,7 @@ private Set<String> mWolSupportedInterfaces; private final TelephonyManager mTelephonyManager; + private final CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator; private final AppOpsManager mAppOpsManager; private final LocationPermissionChecker mLocationPermissionChecker; @@ -1322,12 +1339,33 @@ } /** + * @see CarrierPrivilegeAuthenticator + */ + public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator( + @NonNull final Context context, @NonNull final TelephonyManager tm) { + if (SdkLevel.isAtLeastT()) { + return new CarrierPrivilegeAuthenticator(context, tm); + } else { + return null; + } + } + + /** * @see DeviceConfigUtils#isFeatureEnabled */ public boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled) { return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name, TETHERING_MODULE_NAME, defaultEnabled); } + + /** + * Get the BpfNetMaps implementation to use in ConnectivityService. + * @param netd + * @return BpfNetMaps implementation. + */ + public BpfNetMaps getBpfNetMaps(INetd netd) { + return new BpfNetMaps(netd); + } } public ConnectivityService(Context context) { @@ -1396,9 +1434,12 @@ mProxyTracker = mDeps.makeProxyTracker(mContext, mHandler); mNetd = netd; + mBpfNetMaps = mDeps.getBpfNetMaps(netd); mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext); + mCarrierPrivilegeAuthenticator = + mDeps.makeCarrierPrivilegeAuthenticator(mContext, mTelephonyManager); // To ensure uid state is synchronized with Network Policy, register for // NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService @@ -1423,7 +1464,7 @@ mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - mPermissionMonitor = new PermissionMonitor(mContext, mNetd); + mPermissionMonitor = new PermissionMonitor(mContext, mNetd, mBpfNetMaps); mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); // Listen for user add/removes to inform PermissionMonitor. @@ -1486,19 +1527,33 @@ new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null, new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, mLingerDelayMs, mQosCallbackTracker, mDeps); + + try { + // DscpPolicyTracker cannot run on S because on S the tethering module can only load + // BPF programs/maps into /sys/fs/tethering/bpf, which the system server cannot access. + // Even if it could, running on S would at least require mocking out the BPF map, + // otherwise the unit tests will fail on pre-T devices where the seccomp filter blocks + // the bpf syscall. http://aosp/1907693 + if (SdkLevel.isAtLeastT()) { + mDscpPolicyTracker = new DscpPolicyTracker(); + } + } catch (ErrnoException e) { + loge("Unable to create DscpPolicyTracker"); + } } private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) { - return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid)); + return createDefaultNetworkCapabilitiesForUidRangeSet(Collections.singleton( + new UidRange(uid, uid))); } - private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange( - @NonNull final UidRange uids) { + private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRangeSet( + @NonNull final Set<UidRange> uidRangeSet) { final NetworkCapabilities netCap = new NetworkCapabilities(); netCap.addCapability(NET_CAPABILITY_INTERNET); netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); netCap.removeCapability(NET_CAPABILITY_NOT_VPN); - netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids))); + netCap.setUids(UidRange.toIntRanges(uidRangeSet)); return netCap; } @@ -2063,6 +2118,7 @@ newNc.setAdministratorUids(new int[0]); if (!checkAnyPermissionOf( callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) { + newNc.setAccessUids(new ArraySet<>()); newNc.setSubscriptionIds(Collections.emptySet()); } @@ -3312,18 +3368,8 @@ switch (msg.what) { case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: { - NetworkCapabilities networkCapabilities = (NetworkCapabilities) arg.second; - if (networkCapabilities.hasConnectivityManagedCapability()) { - Log.wtf(TAG, "BUG: " + nai + " has CS-managed capability."); - } - if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { - // Make sure the original object is not mutated. NetworkAgent normally - // makes a copy of the capabilities when sending the message through - // the Messenger, but if this ever changes, not making a defensive copy - // here will give attack vectors to clients using this code path. - networkCapabilities = new NetworkCapabilities(networkCapabilities); - networkCapabilities.restrictCapabilitiesForTestNetwork(nai.creatorUid); - } + final NetworkCapabilities networkCapabilities = new NetworkCapabilities( + (NetworkCapabilities) arg.second); processCapabilitiesFromAgent(nai, networkCapabilities); updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities); break; @@ -3402,6 +3448,25 @@ nai.setLingerDuration((int) arg.second); break; } + case NetworkAgent.EVENT_ADD_DSCP_POLICY: { + DscpPolicy policy = (DscpPolicy) arg.second; + if (mDscpPolicyTracker != null) { + mDscpPolicyTracker.addDscpPolicy(nai, policy); + } + break; + } + case NetworkAgent.EVENT_REMOVE_DSCP_POLICY: { + if (mDscpPolicyTracker != null) { + mDscpPolicyTracker.removeDscpPolicy(nai, (int) arg.second); + } + break; + } + case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: { + if (mDscpPolicyTracker != null) { + mDscpPolicyTracker.removeAllDscpPolicies(nai); + } + break; + } } } @@ -4042,7 +4107,7 @@ config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE, (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass), - getVpnType(nai), /*excludeLocalRoutes=*/ false); + getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn); } else { config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL, getNetworkPermission(nai.networkCapabilities), /*secure=*/ false, @@ -4100,6 +4165,15 @@ } } + private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid, + @NonNull final NetworkCapabilities caps) { + if (SdkLevel.isAtLeastT() && mCarrierPrivilegeAuthenticator != null) { + return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + callingUid, caps); + } + return false; + } + private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) { final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj); // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests. @@ -4123,6 +4197,7 @@ private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) { ensureRunningOnConnectivityServiceThread(); + NetworkRequest requestToBeReleased = null; for (final NetworkRequestInfo nri : nris) { mNetworkRequestInfoLogs.log("REGISTER " + nri); checkNrisConsistency(nri); @@ -4137,7 +4212,15 @@ } } } + if (req.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) { + if (!hasCarrierPrivilegeForNetworkCaps(nri.mUid, req.networkCapabilities) + && !checkConnectivityRestrictedNetworksPermission( + nri.mPid, nri.mUid)) { + requestToBeReleased = req; + } + } } + // If this NRI has a satisfier already, it is replacing an older request that // has been removed. Track it. final NetworkRequest activeRequest = nri.getActiveRequest(); @@ -4147,6 +4230,11 @@ } } + if (requestToBeReleased != null) { + releaseNetworkRequestAndCallOnUnavailable(requestToBeReleased); + return; + } + if (mFlags.noRematchAllRequestsOnRegister()) { rematchNetworksAndRequests(nris); } else { @@ -4986,6 +5074,11 @@ /* callOnUnavailable */ false); break; } + case EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE: { + handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1, + /* callOnUnavailable */ true); + break; + } case EVENT_SET_ACCEPT_UNVALIDATED: { Network network = (Network) msg.obj; handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2)); @@ -5674,7 +5767,7 @@ mPermissionMonitor.onUserRemoved(user); // If there was a network preference for this user, remove it. handleSetProfileNetworkPreference( - List.of(new ProfileNetworkPreferenceList.Preference(user, null)), + List.of(new ProfileNetworkPreferenceList.Preference(user, null, true)), null /* listener */); if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) { handleSetOemNetworkPreference(mOemNetworkPreferences, null); @@ -6055,13 +6148,6 @@ } } - private void ensureRequestableCapabilities(NetworkCapabilities networkCapabilities) { - final String badCapability = networkCapabilities.describeFirstNonRequestableCapability(); - if (badCapability != null) { - throw new IllegalArgumentException("Cannot request network with " + badCapability); - } - } - // This checks that the passed capabilities either do not request a // specific SSID/SignalStrength, or the calling app has permission to do so. private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc, @@ -6119,7 +6205,7 @@ nai.onSignalStrengthThresholdsUpdated(thresholdsArray); } - private void ensureValidNetworkSpecifier(NetworkCapabilities nc) { + private static void ensureValidNetworkSpecifier(NetworkCapabilities nc) { if (nc == null) { return; } @@ -6132,11 +6218,22 @@ } } - private void ensureValid(NetworkCapabilities nc) { + private static void ensureListenableCapabilities(@NonNull final NetworkCapabilities nc) { ensureValidNetworkSpecifier(nc); if (nc.isPrivateDnsBroken()) { throw new IllegalArgumentException("Can't request broken private DNS"); } + if (nc.hasAccessUids()) { + throw new IllegalArgumentException("Can't request access UIDs"); + } + } + + private void ensureRequestableCapabilities(@NonNull final NetworkCapabilities nc) { + ensureListenableCapabilities(nc); + final String badCapability = nc.describeFirstNonRequestableCapability(); + if (badCapability != null) { + throw new IllegalArgumentException("Cannot request network with " + badCapability); + } } // TODO: Set the mini sdk to 31 and remove @TargetApi annotation when b/205923322 is addressed. @@ -6231,7 +6328,6 @@ if (timeoutMs < 0) { throw new IllegalArgumentException("Bad timeout specified"); } - ensureValid(networkCapabilities); final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType, nextNetworkRequestId(), reqType); @@ -6290,12 +6386,24 @@ private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities, String callingPackageName, String callingAttributionTag) { if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) { - enforceConnectivityRestrictedNetworksPermission(); + if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) { + enforceConnectivityRestrictedNetworksPermission(); + } } else { enforceChangePermission(callingPackageName, callingAttributionTag); } } + private boolean checkConnectivityRestrictedNetworksPermission(int callerPid, int callerUid) { + if (checkAnyPermissionOf(callerPid, callerUid, + android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS) + || checkAnyPermissionOf(callerPid, callerUid, + android.Manifest.permission.CONNECTIVITY_INTERNAL)) { + return true; + } + return false; + } + @Override public boolean requestBandwidthUpdate(Network network) { enforceAccessPermission(); @@ -6359,7 +6467,6 @@ ensureRequestableCapabilities(networkCapabilities); ensureSufficientPermissionsForRequest(networkCapabilities, Binder.getCallingPid(), callingUid, callingPackageName); - ensureValidNetworkSpecifier(networkCapabilities); restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities, callingUid, callingPackageName); @@ -6428,7 +6535,7 @@ // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE // can't request networks. restrictBackgroundRequestForCaller(nc); - ensureValid(nc); + ensureListenableCapabilities(nc); NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(), NetworkRequest.Type.LISTEN); @@ -6450,7 +6557,7 @@ if (!hasWifiNetworkListenPermission(networkCapabilities)) { enforceAccessPermission(); } - ensureValid(networkCapabilities); + ensureListenableCapabilities(networkCapabilities); ensureSufficientPermissionsForRequest(networkCapabilities, Binder.getCallingPid(), callingUid, callingPackageName); final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities); @@ -6478,6 +6585,13 @@ EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest)); } + private void releaseNetworkRequestAndCallOnUnavailable(NetworkRequest networkRequest) { + ensureNetworkRequestHasType(networkRequest); + mHandler.sendMessage(mHandler.obtainMessage( + EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE, mDeps.getCallingUid(), 0, + networkRequest)); + } + private void handleRegisterNetworkProvider(NetworkProviderInfo npi) { if (mNetworkProviderInfos.containsKey(npi.messenger)) { // Avoid creating duplicates. even if an app makes a direct AIDL call. @@ -6875,28 +6989,18 @@ LinkProperties linkProperties, NetworkCapabilities networkCapabilities, NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId, int uid) { - if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { - // Strictly, sanitizing here is unnecessary as the capabilities will be sanitized in - // the call to mixInCapabilities below anyway, but sanitizing here means the NAI never - // sees capabilities that may be malicious, which might prevent mistakes in the future. - networkCapabilities = new NetworkCapabilities(networkCapabilities); - networkCapabilities.restrictCapabilitiesForTestNetwork(uid); - } - LinkProperties lp = new LinkProperties(linkProperties); - - final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities); + // At this point the capabilities/properties are untrusted and unverified, e.g. checks that + // the capabilities' access UID comply with security limitations. They will be sanitized + // as the NAI registration finishes, in handleRegisterNetworkAgent(). This is + // because some of the checks must happen on the handler thread. final NetworkAgentInfo nai = new NetworkAgentInfo(na, - new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc, + new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), + linkProperties, networkCapabilities, currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs, mQosCallbackTracker, mDeps); - // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says. - processCapabilitiesFromAgent(nai, nc); - nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc)); - processLinkPropertiesFromAgent(nai, nai.linkProperties); - final String extraInfo = networkInfo.getExtraInfo(); final String name = TextUtils.isEmpty(extraInfo) ? nai.networkCapabilities.getSsid() : extraInfo; @@ -6911,8 +7015,20 @@ } private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) { + if (VDBG) log("Network Monitor created for " + nai); + // nai.nc and nai.lp are the same object that was passed by the network agent if the agent + // lives in the same process as this code (e.g. wifi), so make sure this code doesn't + // mutate their object + final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities); + final LinkProperties lp = new LinkProperties(nai.linkProperties); + // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says. + processCapabilitiesFromAgent(nai, nc); + nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc)); + processLinkPropertiesFromAgent(nai, lp); + nai.linkProperties = lp; + nai.onNetworkMonitorCreated(networkMonitor); - if (VDBG) log("Got NetworkAgent Messenger"); + mNetworkAgentInfos.add(nai); synchronized (mNetworkForNetId) { mNetworkForNetId.put(nai.network.getNetId(), nai); @@ -6923,10 +7039,11 @@ } catch (RemoteException e) { e.rethrowAsRuntimeException(); } + nai.notifyRegistered(); NetworkInfo networkInfo = nai.networkInfo; updateNetworkInfo(nai, networkInfo); - updateUids(nai, null, nai.networkCapabilities); + updateVpnUids(nai, null, nai.networkCapabilities); } private class NetworkOfferInfo implements IBinder.DeathRecipient { @@ -7370,9 +7487,11 @@ * Stores into |nai| any data coming from the agent that might also be written to the network's * NetworkCapabilities by ConnectivityService itself. This ensures that the data provided by the * agent is not lost when updateCapabilities is called. - * This method should never alter the agent's NetworkCapabilities, only store data in |nai|. */ private void processCapabilitiesFromAgent(NetworkAgentInfo nai, NetworkCapabilities nc) { + if (nc.hasConnectivityManagedCapability()) { + Log.wtf(TAG, "BUG: " + nai + " has CS-managed capability."); + } // Note: resetting the owner UID before storing the agent capabilities in NAI means that if // the agent attempts to change the owner UID, then nai.declaredCapabilities will not // actually be the same as the capabilities sent by the agent. Still, it is safer to reset @@ -7383,6 +7502,8 @@ nc.setOwnerUid(nai.networkCapabilities.getOwnerUid()); } nai.declaredCapabilities = new NetworkCapabilities(nc); + NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(nc, nai.creatorUid, + mCarrierPrivilegeAuthenticator); } /** Modifies |newNc| based on the capabilities of |underlyingNetworks| and |agentCaps|. */ @@ -7558,7 +7679,8 @@ updateNetworkPermissions(nai, newNc); final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc); - updateUids(nai, prevNc, newNc); + updateVpnUids(nai, prevNc, newNc); + updateAccessUids(nai, prevNc, newNc); nai.updateScoreForNetworkAgentUpdate(); if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) { @@ -7647,6 +7769,17 @@ return stableRanges; } + private static UidRangeParcel[] intsToUidRangeStableParcels( + final @NonNull ArraySet<Integer> uids) { + final UidRangeParcel[] stableRanges = new UidRangeParcel[uids.size()]; + int index = 0; + for (int uid : uids) { + stableRanges[index] = new UidRangeParcel(uid, uid); + index++; + } + return stableRanges; + } + private static UidRangeParcel[] toUidRangeStableParcels(UidRange[] ranges) { final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.length]; for (int i = 0; i < ranges.length; i++) { @@ -7717,8 +7850,8 @@ } } - private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc, - NetworkCapabilities newNc) { + private void updateVpnUids(@NonNull NetworkAgentInfo nai, @Nullable NetworkCapabilities prevNc, + @Nullable NetworkCapabilities newNc) { Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUidRanges(); Set<UidRange> newRanges = null == newNc ? null : newNc.getUidRanges(); if (null == prevRanges) prevRanges = new ArraySet<>(); @@ -7773,7 +7906,50 @@ } } catch (Exception e) { // Never crash! - loge("Exception in updateUids: ", e); + loge("Exception in updateVpnUids: ", e); + } + } + + private void updateAccessUids(@NonNull NetworkAgentInfo nai, + @Nullable NetworkCapabilities prevNc, @Nullable NetworkCapabilities newNc) { + // In almost all cases both NC code for empty access UIDs. return as fast as possible. + final boolean prevEmpty = null == prevNc || prevNc.getAccessUidsNoCopy().isEmpty(); + final boolean newEmpty = null == newNc || newNc.getAccessUidsNoCopy().isEmpty(); + if (prevEmpty && newEmpty) return; + + final ArraySet<Integer> prevUids = + null == prevNc ? new ArraySet<>() : prevNc.getAccessUidsNoCopy(); + final ArraySet<Integer> newUids = + null == newNc ? new ArraySet<>() : newNc.getAccessUidsNoCopy(); + + if (prevUids.equals(newUids)) return; + + // This implementation is very simple and vastly faster for sets of Integers than + // CompareOrUpdateResult, which is tuned for sets that need to be compared based on + // a key computed from the value and has storage for that. + final ArraySet<Integer> toRemove = new ArraySet<>(prevUids); + final ArraySet<Integer> toAdd = new ArraySet<>(newUids); + toRemove.removeAll(newUids); + toAdd.removeAll(prevUids); + + try { + if (!toAdd.isEmpty()) { + mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig( + nai.network.netId, + intsToUidRangeStableParcels(toAdd), + PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT)); + } + if (!toRemove.isEmpty()) { + mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig( + nai.network.netId, + intsToUidRangeStableParcels(toRemove), + PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT)); + } + } catch (ServiceSpecificException e) { + // Has the interface disappeared since the network was built ? + Log.i(TAG, "Can't set access UIDs for network " + nai.network, e); + } catch (RemoteException e) { + // Netd died. This usually causes a runtime restart anyway. } } @@ -8686,6 +8862,7 @@ } networkAgent.created = true; networkAgent.onNetworkCreated(); + updateAccessUids(networkAgent, null, networkAgent.networkCapabilities); } if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) { @@ -8739,7 +8916,7 @@ } else if (state == NetworkInfo.State.DISCONNECTED) { networkAgent.disconnect(); if (networkAgent.isVPN()) { - updateUids(networkAgent, networkAgent.networkCapabilities, null); + updateVpnUids(networkAgent, networkAgent.networkCapabilities, null); } disconnectAndDestroyNetwork(networkAgent); if (networkAgent.isVPN()) { @@ -9743,7 +9920,7 @@ android.Manifest.permission.NETWORK_STACK); final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network); if (!nc.hasTransport(TRANSPORT_TEST)) { - throw new SecurityException("Data Stall simluation is only possible for test networks"); + throw new SecurityException("Data Stall simulation is only possible for test networks"); } final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); @@ -10139,29 +10316,87 @@ final List<ProfileNetworkPreferenceList.Preference> preferenceList = new ArrayList<ProfileNetworkPreferenceList.Preference>(); + boolean allowFallback = true; for (final ProfileNetworkPreference preference : preferences) { final NetworkCapabilities nc; switch (preference.getPreference()) { case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT: nc = null; + if (preference.getPreferenceEnterpriseId() != 0) { + throw new IllegalArgumentException( + "Invalid enterprise identifier in setProfileNetworkPreferences"); + } break; + case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK: + allowFallback = false; + // continue to process the enterprise preference. case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE: - final UidRange uids = UidRange.createForUser(profile); - nc = createDefaultNetworkCapabilitiesForUidRange(uids); + if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) { + throw new IllegalArgumentException( + "Invalid enterprise identifier in setProfileNetworkPreferences"); + } + final Set<UidRange> uidRangeSet = + getUidListToBeAppliedForNetworkPreference(profile, preference); + if (!isRangeAlreadyInPreferenceList(preferenceList, uidRangeSet)) { + nc = createDefaultNetworkCapabilitiesForUidRangeSet(uidRangeSet); + } else { + throw new IllegalArgumentException( + "Overlapping uid range in setProfileNetworkPreferences"); + } nc.addCapability(NET_CAPABILITY_ENTERPRISE); + nc.addEnterpriseId( + preference.getPreferenceEnterpriseId()); nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); break; default: throw new IllegalArgumentException( "Invalid preference in setProfileNetworkPreferences"); } - preferenceList.add( - new ProfileNetworkPreferenceList.Preference(profile, nc)); + preferenceList.add(new ProfileNetworkPreferenceList.Preference( + profile, nc, allowFallback)); } mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE, new Pair<>(preferenceList, listener))); } + private Set<UidRange> getUidListToBeAppliedForNetworkPreference( + @NonNull final UserHandle profile, + @NonNull final ProfileNetworkPreference profileNetworkPreference) { + final UidRange profileUids = UidRange.createForUser(profile); + Set<UidRange> uidRangeSet = UidRangeUtils.convertListToUidRange( + profileNetworkPreference.getIncludedUids()); + if (uidRangeSet.size() > 0) { + if (!UidRangeUtils.isRangeSetInUidRange(profileUids, uidRangeSet)) { + throw new IllegalArgumentException( + "Allow uid range is outside the uid range of profile."); + } + } else { + ArraySet<UidRange> disallowUidRangeSet = UidRangeUtils.convertListToUidRange( + profileNetworkPreference.getExcludedUids()); + if (disallowUidRangeSet.size() > 0) { + if (!UidRangeUtils.isRangeSetInUidRange(profileUids, disallowUidRangeSet)) { + throw new IllegalArgumentException( + "disallow uid range is outside the uid range of profile."); + } + uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(profileUids, + disallowUidRangeSet); + } else { + uidRangeSet = new ArraySet<UidRange>(); + uidRangeSet.add(profileUids); + } + } + return uidRangeSet; + } + + private boolean isEnterpriseIdentifierValid( + @NetworkCapabilities.EnterpriseId int identifier) { + if ((identifier >= NET_ENTERPRISE_ID_1) + && (identifier <= NET_ENTERPRISE_ID_5)) { + return true; + } + return false; + } + private void validateNetworkCapabilitiesOfProfileNetworkPreference( @Nullable final NetworkCapabilities nc) { if (null == nc) return; // Null caps are always allowed. It means to remove the setting. @@ -10172,17 +10407,22 @@ @NonNull final ProfileNetworkPreferenceList prefs) { final ArraySet<NetworkRequestInfo> result = new ArraySet<>(); for (final ProfileNetworkPreferenceList.Preference pref : prefs.preferences) { - // The NRI for a user should be comprised of two layers: - // - The request for the capabilities - // - The request for the default network, for fallback. Create an image of it to - // have the correct UIDs in it (also a request can only be part of one NRI, because - // of lookups in 1:1 associations like mNetworkRequests). - // Note that denying a fallback can be implemented simply by not adding the second - // request. + // The NRI for a user should contain the request for capabilities. + // If fallback to default network is needed then NRI should include + // the request for the default network. Create an image of it to + // have the correct UIDs in it (also a request can only be part of one NRI, because + // of lookups in 1:1 associations like mNetworkRequests). final ArrayList<NetworkRequest> nrs = new ArrayList<>(); nrs.add(createNetworkRequest(NetworkRequest.Type.REQUEST, pref.capabilities)); - nrs.add(createDefaultInternetRequestForTransport( - TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT)); + if (pref.allowFallback) { + nrs.add(createDefaultInternetRequestForTransport( + TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT)); + } + if (VDBG) { + loge("pref.capabilities.getUids():" + UidRange.fromIntRanges( + pref.capabilities.getUids())); + } + setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids())); final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs, PREFERENCE_ORDER_PROFILE); @@ -10191,6 +10431,25 @@ return result; } + /** + * Compare if the given UID range sets have the same UIDs. + * + */ + private boolean isRangeAlreadyInPreferenceList( + @NonNull List<ProfileNetworkPreferenceList.Preference> preferenceList, + @NonNull Set<UidRange> uidRangeSet) { + if (uidRangeSet.size() == 0 || preferenceList.size() == 0) { + return false; + } + for (ProfileNetworkPreferenceList.Preference pref : preferenceList) { + if (UidRangeUtils.doesRangeSetOverlap( + UidRange.fromIntRanges(pref.capabilities.getUids()), uidRangeSet)) { + return true; + } + } + return false; + } + private void handleSetProfileNetworkPreference( @NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList, @Nullable final IOnCompleteListener listener) { @@ -10587,9 +10846,9 @@ try { if (add) { - mNetd.bandwidthAddNiceApp(uid); + mBpfNetMaps.addNiceApp(uid); } else { - mNetd.bandwidthRemoveNiceApp(uid); + mBpfNetMaps.removeNiceApp(uid); } } catch (RemoteException | ServiceSpecificException e) { throw new IllegalStateException(e); @@ -10602,12 +10861,76 @@ try { if (add) { - mNetd.bandwidthAddNaughtyApp(uid); + mBpfNetMaps.addNaughtyApp(uid); } else { - mNetd.bandwidthRemoveNaughtyApp(uid); + mBpfNetMaps.removeNaughtyApp(uid); } } catch (RemoteException | ServiceSpecificException e) { throw new IllegalStateException(e); } } + + @Override + public void updateFirewallRule(final int chain, final int uid, final boolean allow) { + enforceNetworkStackOrSettingsPermission(); + + try { + mBpfNetMaps.setUidRule(chain, uid, + allow ? INetd.FIREWALL_RULE_ALLOW : INetd.FIREWALL_RULE_DENY); + } catch (RemoteException | ServiceSpecificException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void setFirewallChainEnabled(final int chain, final boolean enable) { + enforceNetworkStackOrSettingsPermission(); + + try { + mBpfNetMaps.setChildChain(chain, enable); + } catch (RemoteException | ServiceSpecificException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void replaceFirewallChain(final int chain, final int[] uids) { + enforceNetworkStackOrSettingsPermission(); + + try { + switch (chain) { + case ConnectivityManager.FIREWALL_CHAIN_DOZABLE: + mBpfNetMaps.replaceUidChain("fw_dozable", true /* isAllowList */, uids); + break; + case ConnectivityManager.FIREWALL_CHAIN_STANDBY: + mBpfNetMaps.replaceUidChain("fw_standby", false /* isAllowList */, uids); + break; + case ConnectivityManager.FIREWALL_CHAIN_POWERSAVE: + mBpfNetMaps.replaceUidChain("fw_powersave", true /* isAllowList */, uids); + break; + case ConnectivityManager.FIREWALL_CHAIN_RESTRICTED: + mBpfNetMaps.replaceUidChain("fw_restricted", true /* isAllowList */, uids); + break; + case ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY: + mBpfNetMaps.replaceUidChain("fw_low_power_standby", true /* isAllowList */, + uids); + break; + default: + throw new IllegalArgumentException("replaceFirewallChain with invalid chain: " + + chain); + } + } catch (RemoteException | ServiceSpecificException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void swapActiveStatsMap() { + enforceNetworkStackOrSettingsPermission(); + try { + mBpfNetMaps.swapActiveStatsMap(); + } catch (RemoteException | ServiceSpecificException e) { + throw new IllegalStateException(e); + } + } }
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java new file mode 100644 index 0000000..b761762 --- /dev/null +++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -0,0 +1,325 @@ +/* + * 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.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.NetworkCapabilities; +import android.net.NetworkSpecifier; +import android.net.TelephonyNetworkSpecifier; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.networkstack.apishim.TelephonyManagerShimImpl; +import com.android.networkstack.apishim.common.TelephonyManagerShim; +import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +/** + * Tracks the uid of the carrier privileged app that provides the carrier config. + * Authenticates if the caller has same uid as + * carrier privileged app that provides the carrier config + * @hide + */ +public class CarrierPrivilegeAuthenticator extends BroadcastReceiver { + private static final String TAG = CarrierPrivilegeAuthenticator.class.getSimpleName(); + private static final boolean DBG = true; + + // The context is for the current user (system server) + private final Context mContext; + private final TelephonyManagerShim mTelephonyManagerShim; + private final TelephonyManager mTelephonyManager; + @GuardedBy("mLock") + private int[] mCarrierServiceUid; + @GuardedBy("mLock") + private int mModemCount = 0; + private final Object mLock = new Object(); + private final HandlerThread mThread; + private final Handler mHandler; + @NonNull + private final List<CarrierPrivilegesListenerShim> mCarrierPrivilegesChangedListeners = + new ArrayList<>(); + + public CarrierPrivilegeAuthenticator(@NonNull final Context c, + @NonNull final TelephonyManager t, + @NonNull final TelephonyManagerShimImpl telephonyManagerShim) { + mContext = c; + mTelephonyManager = t; + mTelephonyManagerShim = telephonyManagerShim; + mThread = new HandlerThread(TAG); + mThread.start(); + mHandler = new Handler(mThread.getLooper()) {}; + synchronized (mLock) { + mModemCount = mTelephonyManager.getActiveModemCount(); + registerForCarrierChanges(); + updateCarrierServiceUid(); + } + } + + public CarrierPrivilegeAuthenticator(@NonNull final Context c, + @NonNull final TelephonyManager t) { + mContext = c; + mTelephonyManager = t; + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) { + mTelephonyManagerShim = new TelephonyManagerShimImpl(mTelephonyManager); + } else { + mTelephonyManagerShim = null; + } + mThread = new HandlerThread(TAG); + mThread.start(); + mHandler = new Handler(mThread.getLooper()) {}; + synchronized (mLock) { + mModemCount = mTelephonyManager.getActiveModemCount(); + registerForCarrierChanges(); + updateCarrierServiceUid(); + } + } + + /** + * An adapter {@link Executor} that posts all executed tasks onto the given + * {@link Handler}. + * + * TODO : migrate to the version in frameworks/libs/net when it's ready + * + * @hide + */ + public class HandlerExecutor implements Executor { + private final Handler mHandler; + public HandlerExecutor(@NonNull Handler handler) { + mHandler = handler; + } + @Override + public void execute(Runnable command) { + if (!mHandler.post(command)) { + throw new RejectedExecutionException(mHandler + " is shutting down"); + } + } + } + + /** + * Broadcast receiver for ACTION_MULTI_SIM_CONFIG_CHANGED + * + * <p>The broadcast receiver is registered with mHandler + */ + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED: + handleActionMultiSimConfigChanged(context, intent); + break; + default: + Log.d(TAG, "Unknown intent received with action: " + intent.getAction()); + } + } + + private void handleActionMultiSimConfigChanged(Context context, Intent intent) { + unregisterCarrierPrivilegesListeners(); + synchronized (mLock) { + mModemCount = mTelephonyManager.getActiveModemCount(); + } + registerCarrierPrivilegesListeners(); + updateCarrierServiceUid(); + } + + private void registerForCarrierChanges() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + mContext.registerReceiver(this, filter, null, mHandler, RECEIVER_NOT_EXPORTED /* flags */); + registerCarrierPrivilegesListeners(); + } + + private void registerCarrierPrivilegesListeners() { + final HandlerExecutor executor = new HandlerExecutor(mHandler); + int modemCount; + synchronized (mLock) { + modemCount = mModemCount; + } + try { + for (int i = 0; i < modemCount; i++) { + CarrierPrivilegesListenerShim carrierPrivilegesListener = + new CarrierPrivilegesListenerShim() { + @Override + public void onCarrierPrivilegesChanged( + @NonNull List<String> privilegedPackageNames, + @NonNull int[] privilegedUids) { + // Re-trigger the synchronous check (which is also very cheap due + // to caching in CarrierPrivilegesTracker). This allows consistency + // with the onSubscriptionsChangedListener and broadcasts. + updateCarrierServiceUid(); + } + }; + addCarrierPrivilegesListener(i, executor, carrierPrivilegesListener); + mCarrierPrivilegesChangedListeners.add(carrierPrivilegesListener); + } + } catch (IllegalArgumentException e) { + Log.e(TAG, "Encountered exception registering carrier privileges listeners", e); + } + } + + private void addCarrierPrivilegesListener(int logicalSlotIndex, Executor executor, + CarrierPrivilegesListenerShim listener) { + if (mTelephonyManagerShim == null) { + return; + } + try { + mTelephonyManagerShim.addCarrierPrivilegesListener( + logicalSlotIndex, executor, listener); + } catch (UnsupportedApiLevelException unsupportedApiLevelException) { + Log.e(TAG, "addCarrierPrivilegesListener API is not available"); + } + } + + private void removeCarrierPrivilegesListener(CarrierPrivilegesListenerShim listener) { + if (mTelephonyManagerShim == null) { + return; + } + try { + mTelephonyManagerShim.removeCarrierPrivilegesListener(listener); + } catch (UnsupportedApiLevelException unsupportedApiLevelException) { + Log.e(TAG, "removeCarrierPrivilegesListener API is not available"); + } + } + + private String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex) { + if (mTelephonyManagerShim == null) { + return null; + } + try { + return mTelephonyManagerShim.getCarrierServicePackageNameForLogicalSlot( + logicalSlotIndex); + } catch (UnsupportedApiLevelException unsupportedApiLevelException) { + Log.e(TAG, "getCarrierServicePackageNameForLogicalSlot API is not available"); + } + return null; + } + + private void unregisterCarrierPrivilegesListeners() { + for (CarrierPrivilegesListenerShim carrierPrivilegesListener : + mCarrierPrivilegesChangedListeners) { + removeCarrierPrivilegesListener(carrierPrivilegesListener); + } + mCarrierPrivilegesChangedListeners.clear(); + } + + /** + * Check if a UID is the carrier service app of the subscription ID in the provided capabilities + * + * This returns whether the passed UID is the carrier service package for the subscription ID + * stored in the telephony network specifier in the passed network capabilities. + * If the capabilities don't code for a cellular network, or if they don't have the + * subscription ID in their specifier, this returns false. + * + * This method can be used to check that a network request for {@link NET_CAPABILITY_CBS} is + * allowed for the UID of a caller, which must hold carrier privilege and provide the carrier + * config. + * It can also be used to check that a factory is entitled to grant access to a given network + * to a given UID on grounds that it is the carrier service package. + * + * @param callingUid uid of the app claimed to be the carrier service package. + * @param networkCapabilities the network capabilities for which carrier privilege is checked. + * @return true if uid provides the relevant carrier config else false. + */ + public boolean hasCarrierPrivilegeForNetworkCapabilities(int callingUid, + @NonNull NetworkCapabilities networkCapabilities) { + if (callingUid == Process.INVALID_UID) return false; + if (!networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)) return false; + final int subId = getSubIdFromNetworkSpecifier(networkCapabilities.getNetworkSpecifier()); + if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false; + return callingUid == getCarrierServiceUidForSubId(subId); + } + + @VisibleForTesting + void updateCarrierServiceUid() { + synchronized (mLock) { + mCarrierServiceUid = new int[mModemCount]; + for (int i = 0; i < mModemCount; i++) { + mCarrierServiceUid[i] = getCarrierServicePackageUidForSlot(i); + } + } + } + + @VisibleForTesting + int getCarrierServiceUidForSubId(int subId) { + final int slotId = getSlotIndex(subId); + synchronized (mLock) { + if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mModemCount) { + return mCarrierServiceUid[slotId]; + } + } + return Process.INVALID_UID; + } + + @VisibleForTesting + protected int getSlotIndex(int subId) { + return SubscriptionManager.getSlotIndex(subId); + } + + @VisibleForTesting + int getSubIdFromNetworkSpecifier(NetworkSpecifier specifier) { + if (specifier instanceof TelephonyNetworkSpecifier) { + return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); + } + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + @VisibleForTesting + int getUidForPackage(String pkgName) { + if (pkgName == null) { + return Process.INVALID_UID; + } + try { + PackageManager pm = mContext.getPackageManager(); + if (pm != null) { + ApplicationInfo applicationInfo = pm.getApplicationInfo(pkgName, 0); + if (applicationInfo != null) { + return applicationInfo.uid; + } + } + } catch (PackageManager.NameNotFoundException exception) { + // Didn't find package. Try other users + Log.i(TAG, "Unable to find uid for package " + pkgName); + } + return Process.INVALID_UID; + } + + @VisibleForTesting + int getCarrierServicePackageUidForSlot(int slotId) { + return getUidForPackage(getCarrierServicePackageNameForLogicalSlot(slotId)); + } +}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java new file mode 100644 index 0000000..c57983b --- /dev/null +++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -0,0 +1,406 @@ +/* + * 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 android.net.INetd.PERMISSION_SYSTEM; + +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetd; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.InterfaceParams; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +/** + * This coordinator is responsible for providing clat relevant functionality. + * + * {@hide} + */ +public class ClatCoordinator { + private static final String TAG = ClatCoordinator.class.getSimpleName(); + + // Sync from external/android-clat/clatd.c + // 40 bytes IPv6 header - 20 bytes IPv4 header + 8 bytes fragment header. + @VisibleForTesting + static final int MTU_DELTA = 28; + @VisibleForTesting + static final int CLAT_MAX_MTU = 65536; + + // This must match the interface prefix in clatd.c. + private static final String CLAT_PREFIX = "v4-"; + + // For historical reasons, start with 192.0.0.4, and after that, use all subsequent addresses + // in 192.0.0.0/29 (RFC 7335). + @VisibleForTesting + static final String INIT_V4ADDR_STRING = "192.0.0.4"; + @VisibleForTesting + static final int INIT_V4ADDR_PREFIX_LEN = 29; + private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8"); + + private static final int INVALID_IFINDEX = 0; + private static final int INVALID_PID = 0; + + @NonNull + private final INetd mNetd; + @NonNull + private final Dependencies mDeps; + @Nullable + private String mIface = null; + @Nullable + private String mNat64Prefix = null; + @Nullable + private String mXlatLocalAddress4 = null; + @Nullable + private String mXlatLocalAddress6 = null; + private int mPid = INVALID_PID; + + @VisibleForTesting + abstract static class Dependencies { + /** + * Get netd. + */ + @NonNull + public abstract INetd getNetd(); + + /** + * @see ParcelFileDescriptor#adoptFd(int). + */ + @NonNull + public ParcelFileDescriptor adoptFd(int fd) { + return ParcelFileDescriptor.adoptFd(fd); + } + + /** + * Get interface index for a given interface. + */ + public int getInterfaceIndex(String ifName) { + final InterfaceParams params = InterfaceParams.getByName(ifName); + return params != null ? params.index : INVALID_IFINDEX; + } + + /** + * Create tun interface for a given interface name. + */ + public int createTunInterface(@NonNull String tuniface) throws IOException { + return native_createTunInterface(tuniface); + } + + /** + * Pick an IPv4 address for clat. + */ + @NonNull + public String selectIpv4Address(@NonNull String v4addr, int prefixlen) + throws IOException { + return native_selectIpv4Address(v4addr, prefixlen); + } + + /** + * Generate a checksum-neutral IID. + */ + @NonNull + public String generateIpv6Address(@NonNull String iface, @NonNull String v4, + @NonNull String prefix64) throws IOException { + return native_generateIpv6Address(iface, v4, prefix64); + } + + /** + * Detect MTU. + */ + public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark) + throws IOException { + return native_detectMtu(platSubnet, platSuffix, mark); + } + + /** + * Open packet socket. + */ + public int openPacketSocket() throws IOException { + return native_openPacketSocket(); + } + + /** + * Open IPv6 raw socket and set SO_MARK. + */ + public int openRawSocket6(int mark) throws IOException { + return native_openRawSocket6(mark); + } + + /** + * Add anycast setsockopt. + */ + public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex) + throws IOException { + native_addAnycastSetsockopt(sock, v6, ifindex); + } + + /** + * Configure packet socket. + */ + public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex) + throws IOException { + native_configurePacketSocket(sock, v6, ifindex); + } + + /** + * Start clatd. + */ + 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_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6); + } + + /** + * Stop clatd. + */ + public void stopClatd(String iface, String pfx96, String v4, String v6, int pid) + throws IOException { + native_stopClatd(iface, pfx96, v4, v6, pid); + } + } + + @VisibleForTesting + static int getFwmark(int netId) { + // See union Fwmark in system/netd/include/Fwmark.h + return (netId & 0xffff) + | 0x1 << 16 // protectedFromVpn: true + | 0x1 << 17 // explicitlySelected: true + | (PERMISSION_SYSTEM & 0x3) << 18; + } + + @VisibleForTesting + static int adjustMtu(int mtu) { + // clamp to minimum ipv6 mtu - this probably cannot ever trigger + if (mtu < IPV6_MIN_MTU) mtu = IPV6_MIN_MTU; + // clamp to buffer size + if (mtu > CLAT_MAX_MTU) mtu = CLAT_MAX_MTU; + // decrease by ipv6(40) + ipv6 fragmentation header(8) vs ipv4(20) overhead of 28 bytes + mtu -= MTU_DELTA; + + return mtu; + } + + public ClatCoordinator(@NonNull Dependencies deps) { + mDeps = deps; + mNetd = mDeps.getNetd(); + } + + /** + * Start clatd for a given interface and NAT64 prefix. + */ + 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 is already running on " + mIface + " (pid " + mPid + ")"); + } + if (nat64Prefix.getPrefixLength() != 96) { + throw new IOException("Prefix must be 96 bits long: " + nat64Prefix); + } + + // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 .. + final String v4; + try { + v4 = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN); + } catch (IOException e) { + throw new IOException("no IPv4 addresses were available for clat: " + e); + } + + // [2] Generate a checksum-neutral IID. + final String pfx96 = nat64Prefix.getAddress().getHostAddress(); + final String v6; + try { + v6 = mDeps.generateIpv6Address(iface, v4, pfx96); + } catch (IOException e) { + throw new IOException("no IPv6 addresses were available for clat: " + e); + } + + // [3] Open, configure and bring up the tun interface. + // Create the v4-... tun interface. + final String tunIface = CLAT_PREFIX + iface; + final ParcelFileDescriptor tunFd; + try { + tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface)); + } catch (IOException e) { + throw new IOException("Create tun interface " + tunIface + " failed: " + e); + } + + // disable IPv6 on it - failing to do so is not a critical error + try { + mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */); + } catch (RemoteException | ServiceSpecificException e) { + tunFd.close(); + Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e); + } + + // Detect ipv4 mtu. + final Integer fwmark = getFwmark(netId); + final int detectedMtu = mDeps.detectMtu(pfx96, + ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark); + final int mtu = adjustMtu(detectedMtu); + Log.i(TAG, "ipv4 mtu is " + mtu); + + // TODO: add setIptablesDropRule + + // Config tun interface mtu, address and bring up. + 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(); + ifConfig.ifName = tunIface; + ifConfig.ipv4Addr = v4; + ifConfig.prefixLength = 32; + ifConfig.hwAddr = ""; + ifConfig.flags = new String[] {IF_STATE_UP}; + 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); + } + + // [4] Open and configure local 464xlat read/write sockets. + // Opens a packet socket to receive IPv6 packets in clatd. + 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 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); + } + + // Opens a raw socket with a given fwmark to send IPv6 packets in clatd. + final ParcelFileDescriptor writeSock6; + try { + // Use a JNI call to get native file descriptor instead of Os.socket(). See above + // 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); + } + + // Start translating packets to the new prefix. + try { + mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex); + } catch (IOException e) { + tunFd.close(); + readSock6.close(); + writeSock6.close(); + throw new IOException("add anycast sockopt failed: " + e); + } + + // Update our packet socket filter to reflect the new 464xlat IP address. + 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] Start clatd. + try { + 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 clatd on " + iface + ": " + e); + } finally { + tunFd.close(); + readSock6.close(); + writeSock6.close(); + } + + return v6; + } + + /** + * Stop clatd + */ + public void clatStop() throws IOException { + 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"); + + mIface = null; + mNat64Prefix = null; + mXlatLocalAddress4 = null; + mXlatLocalAddress6 = null; + mPid = INVALID_PID; + } + + private static native String native_selectIpv4Address(String v4addr, int prefixlen) + throws IOException; + private static native String native_generateIpv6Address(String iface, String v4, + String prefix64) throws IOException; + private static native int native_createTunInterface(String tuniface) throws IOException; + private static native int native_detectMtu(String platSubnet, int platSuffix, int mark) + throws IOException; + private static native int native_openPacketSocket() throws IOException; + private static native int native_openRawSocket6(int mark) throws IOException; + private static native void native_addAnycastSetsockopt(FileDescriptor sock, String v6, + int ifindex) throws IOException; + private static native void native_configurePacketSocket(FileDescriptor sock, String v6, + int ifindex) throws IOException; + 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_stopClatd(String iface, String pfx96, String v4, String v6, + int pid) throws IOException; +}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java new file mode 100644 index 0000000..43cfc8f --- /dev/null +++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -0,0 +1,271 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.DscpPolicy.STATUS_DELETED; +import static android.net.DscpPolicy.STATUS_INSUFFICIENT_PROCESSING_RESOURCES; +import static android.net.DscpPolicy.STATUS_POLICY_NOT_FOUND; +import static android.net.DscpPolicy.STATUS_SUCCESS; +import static android.system.OsConstants.ETH_P_ALL; + +import android.annotation.NonNull; +import android.net.DscpPolicy; +import android.os.RemoteException; +import android.system.ErrnoException; +import android.util.Log; +import android.util.SparseIntArray; + +import com.android.net.module.util.BpfMap; +import com.android.net.module.util.Struct; +import com.android.net.module.util.TcUtils; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.NetworkInterface; +import java.util.HashSet; +import java.util.Set; + +/** + * DscpPolicyTracker has a single entry point from ConnectivityService handler. + * This guarantees that all code runs on the same thread and no locking is needed. + */ +public class DscpPolicyTracker { + // After tethering and clat priorities. + static final short PRIO_DSCP = 5; + + private static final String TAG = DscpPolicyTracker.class.getSimpleName(); + private static final String PROG_PATH = + "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp"; + // Name is "map + *.o + map_name + map". Can probably shorten this + private static final String IPV4_POLICY_MAP_PATH = makeMapPath( + "dscp_policy_ipv4_dscp_policies"); + private static final String IPV6_POLICY_MAP_PATH = makeMapPath( + "dscp_policy_ipv6_dscp_policies"); + private static final int MAX_POLICIES = 16; + + private static String makeMapPath(String which) { + return "/sys/fs/bpf/map_" + which + "_map"; + } + + private Set<String> mAttachedIfaces; + + private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies; + private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies; + private final SparseIntArray mPolicyIdToBpfMapIndex; + + public DscpPolicyTracker() throws ErrnoException { + mAttachedIfaces = new HashSet<String>(); + + mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES); + mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH, + BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class); + mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH, + BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class); + } + + private int getFirstFreeIndex() { + for (int i = 0; i < MAX_POLICIES; i++) { + if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i; + } + return MAX_POLICIES; + } + + private void sendStatus(NetworkAgentInfo nai, int policyId, int status) { + try { + nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status); + } catch (RemoteException e) { + Log.d(TAG, "Failed update policy status: ", e); + } + } + + private boolean matchesIpv4(DscpPolicy policy) { + return ((policy.getDestinationAddress() == null + || policy.getDestinationAddress() instanceof Inet4Address) + && (policy.getSourceAddress() == null + || policy.getSourceAddress() instanceof Inet4Address)); + } + + private boolean matchesIpv6(DscpPolicy policy) { + return ((policy.getDestinationAddress() == null + || policy.getDestinationAddress() instanceof Inet6Address) + && (policy.getSourceAddress() == null + || policy.getSourceAddress() instanceof Inet6Address)); + } + + private int addDscpPolicyInternal(DscpPolicy policy) { + // If there is no existing policy with a matching ID, and we are already at + // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES. + final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1); + if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) { + return STATUS_INSUFFICIENT_PROCESSING_RESOURCES; + } + + // Currently all classifiers are supported, if any are removed return + // STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED, + // and for any other generic error STATUS_REQUEST_DECLINED + + int addIndex = 0; + // If a policy with a matching ID exists, replace it, otherwise use the next free + // index for the policy. + if (existingIndex != -1) { + addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId()); + } else { + addIndex = getFirstFreeIndex(); + } + + try { + mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex); + + // Add v4 policy to mBpfDscpIpv4Policies if source and destination address + // are both null or if they are both instances of Inet6Address. + if (matchesIpv4(policy)) { + mBpfDscpIpv4Policies.insertOrReplaceEntry( + new Struct.U32(addIndex), + new DscpPolicyValue(policy.getSourceAddress(), + policy.getDestinationAddress(), + policy.getSourcePort(), policy.getDestinationPortRange(), + (short) policy.getProtocol(), (short) policy.getDscpValue())); + } + + // Add v6 policy to mBpfDscpIpv6Policies if source and destination address + // are both null or if they are both instances of Inet6Address. + if (matchesIpv6(policy)) { + mBpfDscpIpv6Policies.insertOrReplaceEntry( + new Struct.U32(addIndex), + new DscpPolicyValue(policy.getSourceAddress(), + policy.getDestinationAddress(), + policy.getSourcePort(), policy.getDestinationPortRange(), + (short) policy.getProtocol(), (short) policy.getDscpValue())); + } + } catch (ErrnoException e) { + Log.e(TAG, "Failed to insert policy into map: ", e); + return STATUS_INSUFFICIENT_PROCESSING_RESOURCES; + } + + return STATUS_SUCCESS; + } + + /** + * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface + * if not already attached. Response will be sent back to nai with status. + * + * STATUS_SUCCESS - if policy was added successfully + * STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set + */ + public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) { + if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) { + if (!attachProgram(nai.linkProperties.getInterfaceName())) { + Log.e(TAG, "Unable to attach program"); + sendStatus(nai, policy.getPolicyId(), STATUS_INSUFFICIENT_PROCESSING_RESOURCES); + return; + } + } + + int status = addDscpPolicyInternal(policy); + sendStatus(nai, policy.getPolicyId(), status); + } + + private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) { + int status = STATUS_POLICY_NOT_FOUND; + try { + mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE); + mBpfDscpIpv6Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE); + status = STATUS_DELETED; + } catch (ErrnoException e) { + Log.e(TAG, "Failed to delete policy from map: ", e); + } + + sendStatus(nai, policyId, status); + } + + /** + * Remove specified DSCP policy and detach program if no other policies are active. + */ + public void removeDscpPolicy(NetworkAgentInfo nai, int policyId) { + if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) { + // Nothing to remove since program is not attached. Send update back for policy id. + sendStatus(nai, policyId, STATUS_POLICY_NOT_FOUND); + return; + } + + if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) { + removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId)); + mPolicyIdToBpfMapIndex.delete(policyId); + } + + // TODO: detach should only occur if no more policies are present on the nai's iface. + if (mPolicyIdToBpfMapIndex.size() == 0) { + detachProgram(nai.linkProperties.getInterfaceName()); + } + } + + /** + * Remove all DSCP policies and detach program. + */ + // TODO: Remove all should only remove policies from corresponding nai iface. + public void removeAllDscpPolicies(NetworkAgentInfo nai) { + if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) { + // Nothing to remove since program is not attached. Send update for policy + // id 0. The status update must contain a policy ID, and 0 is an invalid id. + sendStatus(nai, 0, STATUS_SUCCESS); + return; + } + + for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) { + removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i), + mPolicyIdToBpfMapIndex.valueAt(i)); + } + mPolicyIdToBpfMapIndex.clear(); + + // Can detach program since no policies are active. + detachProgram(nai.linkProperties.getInterfaceName()); + } + + /** + * Attach BPF program + */ + private boolean attachProgram(@NonNull String iface) { + // TODO: attach needs to be per iface not program. + + try { + NetworkInterface netIface = NetworkInterface.getByName(iface); + TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL, + PROG_PATH); + } catch (IOException e) { + Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e); + return false; + } + mAttachedIfaces.add(iface); + return true; + } + + /** + * Detach BPF program + */ + public void detachProgram(@NonNull String iface) { + try { + NetworkInterface netIface = NetworkInterface.getByName(iface); + if (netIface != null) { + TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL); + } + } catch (IOException e) { + Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e); + } + mAttachedIfaces.remove(iface); + } +}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java new file mode 100644 index 0000000..cb40306 --- /dev/null +++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -0,0 +1,180 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.util.Log; +import android.util.Range; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** Value type for DSCP setting and rewriting to DSCP policy BPF maps. */ +public class DscpPolicyValue extends Struct { + private static final String TAG = DscpPolicyValue.class.getSimpleName(); + + // TODO: add the interface index. + @Field(order = 0, type = Type.ByteArray, arraysize = 16) + public final byte[] src46; + + @Field(order = 1, type = Type.ByteArray, arraysize = 16) + public final byte[] dst46; + + @Field(order = 2, type = Type.UBE16) + public final int srcPort; + + @Field(order = 3, type = Type.UBE16) + public final int dstPortStart; + + @Field(order = 4, type = Type.UBE16) + public final int dstPortEnd; + + @Field(order = 5, type = Type.U8) + public final short proto; + + @Field(order = 6, type = Type.U8) + public final short dscp; + + @Field(order = 7, type = Type.U8, padding = 3) + public final short mask; + + private static final int SRC_IP_MASK = 0x1; + private static final int DST_IP_MASK = 0x02; + private static final int SRC_PORT_MASK = 0x4; + private static final int DST_PORT_MASK = 0x8; + private static final int PROTO_MASK = 0x10; + + private boolean ipEmpty(final byte[] ip) { + for (int i = 0; i < ip.length; i++) { + if (ip[i] != 0) return false; + } + return true; + } + + private byte[] toIpv4MappedAddressBytes(InetAddress ia) { + final byte[] addr6 = new byte[16]; + if (ia != null) { + final byte[] addr4 = ia.getAddress(); + addr6[10] = (byte) 0xff; + addr6[11] = (byte) 0xff; + addr6[12] = addr4[0]; + addr6[13] = addr4[1]; + addr6[14] = addr4[2]; + addr6[15] = addr4[3]; + } + return addr6; + } + + private byte[] toAddressField(InetAddress addr) { + if (addr == null) { + return EMPTY_ADDRESS_FIELD; + } else if (addr instanceof Inet4Address) { + return toIpv4MappedAddressBytes(addr); + } else { + return addr.getAddress(); + } + } + + private static final byte[] EMPTY_ADDRESS_FIELD = + InetAddress.parseNumericAddress("::").getAddress(); + + private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort, + final int dstPortStart, final short proto, final short dscp) { + short mask = 0; + if (src46 != EMPTY_ADDRESS_FIELD) { + mask |= SRC_IP_MASK; + } + if (dst46 != EMPTY_ADDRESS_FIELD) { + mask |= DST_IP_MASK; + } + if (srcPort != -1) { + mask |= SRC_PORT_MASK; + } + if (dstPortStart != -1 && dstPortEnd != -1) { + mask |= DST_PORT_MASK; + } + if (proto != -1) { + mask |= PROTO_MASK; + } + return mask; + } + + // This constructor is necessary for BpfMap#getValue since all values must be + // in the constructor. + public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort, + final int dstPortStart, final int dstPortEnd, final short proto, + final short dscp) { + this.src46 = toAddressField(src46); + this.dst46 = toAddressField(dst46); + + // These params need to be stored as 0 because uints are used in BpfMap. + // If they are -1 BpfMap write will throw errors. + this.srcPort = srcPort != -1 ? srcPort : 0; + this.dstPortStart = dstPortStart != -1 ? dstPortStart : 0; + this.dstPortEnd = dstPortEnd != -1 ? dstPortEnd : 0; + this.proto = proto != -1 ? proto : 0; + + this.dscp = dscp; + // Use member variables for IP since byte[] is needed and api variables for everything else + // so -1 is passed into mask if parameter is not present. + this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp); + } + + public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort, + final Range<Integer> dstPort, final short proto, + final short dscp) { + this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1, + dstPort != null ? dstPort.getUpper() : -1, proto, dscp); + } + + public static final DscpPolicyValue NONE = new DscpPolicyValue( + null /* src46 */, null /* dst46 */, -1 /* srcPort */, + -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */, + (short) 0 /* dscp */); + + @Override + public String toString() { + String srcIpString = "empty"; + String dstIpString = "empty"; + + // Separate try/catch for IP's so it's easier to debug. + try { + srcIpString = InetAddress.getByAddress(src46).getHostAddress(); + } catch (UnknownHostException e) { + Log.e(TAG, "Invalid SRC IP address", e); + } + + try { + dstIpString = InetAddress.getByAddress(src46).getHostAddress(); + } catch (UnknownHostException e) { + Log.e(TAG, "Invalid DST IP address", e); + } + + try { + return String.format( + "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d," + + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart, + dstPortEnd, proto, dscp); + } catch (IllegalArgumentException e) { + return String.format("String format error: " + e); + } + } +}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java index b7f3ed9..e917b3f 100644 --- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,13 +17,16 @@ package com.android.server.connectivity; import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.transportNamesOf; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.net.CaptivePortalData; +import android.net.DscpPolicy; import android.net.IDnsResolver; import android.net.INetd; import android.net.INetworkAgent; @@ -51,11 +54,13 @@ import android.os.SystemClock; import android.telephony.data.EpsBearerQosSessionAttributes; import android.telephony.data.NrQosSessionAttributes; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.SparseArray; import com.android.internal.util.WakeupMessage; +import com.android.modules.utils.build.SdkLevel; import com.android.server.ConnectivityService; import java.io.PrintWriter; @@ -700,6 +705,24 @@ mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED, new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget(); } + + @Override + public void sendAddDscpPolicy(final DscpPolicy policy) { + mHandler.obtainMessage(NetworkAgent.EVENT_ADD_DSCP_POLICY, + new Pair<>(NetworkAgentInfo.this, policy)).sendToTarget(); + } + + @Override + public void sendRemoveDscpPolicy(final int policyId) { + mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_DSCP_POLICY, + new Pair<>(NetworkAgentInfo.this, policyId)).sendToTarget(); + } + + @Override + public void sendRemoveAllDscpPolicies() { + mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES, + new Pair<>(NetworkAgentInfo.this, null)).sendToTarget(); + } } /** @@ -1169,6 +1192,54 @@ return mConnectivityReport; } + /** + * Make sure the NC from network agents don't contain stuff they shouldn't. + * + * @param nc the capabilities to sanitize + * @param creatorUid the UID of the process creating this network agent + * @param authenticator the carrier privilege authenticator to check for telephony constraints + */ + public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc, + final int creatorUid, @NonNull final CarrierPrivilegeAuthenticator authenticator) { + if (nc.hasTransport(TRANSPORT_TEST)) { + nc.restrictCapabilitiesForTestNetwork(creatorUid); + } + if (!areAccessUidsAcceptableFromNetworkAgent(nc, authenticator)) { + nc.setAccessUids(new ArraySet<>()); + } + } + + private static boolean areAccessUidsAcceptableFromNetworkAgent( + @NonNull final NetworkCapabilities nc, + @Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) { + // NCs without access UIDs are fine. + if (!nc.hasAccessUids()) return true; + // S and below must never accept access UIDs, even if an agent sends them, because netd + // didn't support the required feature in S. + if (!SdkLevel.isAtLeastT()) return false; + + // On a non-restricted network, access UIDs make no sense + if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) return false; + + // If this network has TRANSPORT_TEST, then the caller can do whatever they want to + // access UIDs + if (nc.hasTransport(TRANSPORT_TEST)) return true; + + // Factories that make cell networks can allow the UID for the carrier service package. + // This can only work in T where there is support for CarrierPrivilegeAuthenticator + if (null != carrierPrivilegeAuthenticator + && nc.hasSingleTransport(TRANSPORT_CELLULAR) + && (1 == nc.getAccessUidsNoCopy().size()) + && (carrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + nc.getAccessUidsNoCopy().valueAt(0), nc))) { + return true; + } + + // TODO : accept Railway callers + + return false; + } + // TODO: Print shorter members first and only print the boolean variable which value is true // to improve readability. public String toString() {
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java index 439db89..ac46054 100755 --- a/service/src/com/android/server/connectivity/PermissionMonitor.java +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -58,7 +58,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.system.OsConstants; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -68,6 +67,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.net.module.util.CollectionUtils; +import com.android.server.BpfNetMaps; import java.util.ArrayList; import java.util.HashMap; @@ -93,6 +93,7 @@ private final INetd mNetd; private final Dependencies mDeps; private final Context mContext; + private final BpfNetMaps mBpfNetMaps; @GuardedBy("this") private final Set<UserHandle> mUsers = new HashSet<>(); @@ -184,12 +185,14 @@ } } - public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd) { - this(context, netd, new Dependencies()); + public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd, + @NonNull final BpfNetMaps bpfNetMaps) { + this(context, netd, bpfNetMaps, new Dependencies()); } @VisibleForTesting PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd, + @NonNull final BpfNetMaps bpfNetMaps, @NonNull final Dependencies deps) { mPackageManager = context.getPackageManager(); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); @@ -197,6 +200,7 @@ mNetd = netd; mDeps = deps; mContext = context; + mBpfNetMaps = bpfNetMaps; } private int getPackageNetdNetworkPermission(@NonNull final PackageInfo app) { @@ -803,17 +807,11 @@ } try { if (add) { - mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids)); + mBpfNetMaps.addUidInterfaceRules(iface, toIntArray(uids)); } else { - mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids)); + mBpfNetMaps.removeUidInterfaceRules(toIntArray(uids)); } - } catch (ServiceSpecificException e) { - // Silently ignore exception when device does not support eBPF, otherwise just log - // the exception and do not crash - if (e.errorCode != OsConstants.EOPNOTSUPP) { - loge("Exception when updating permissions: ", e); - } - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { loge("Exception when updating permissions: ", e); } } @@ -878,26 +876,27 @@ try { // TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids() if (allPermissionAppIds.size() != 0) { - mNetd.trafficSetNetPermForUids( + mBpfNetMaps.setNetPermForUids( PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS, toIntArray(allPermissionAppIds)); } if (internetPermissionAppIds.size() != 0) { - mNetd.trafficSetNetPermForUids(PERMISSION_INTERNET, + mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, toIntArray(internetPermissionAppIds)); } if (updateStatsPermissionAppIds.size() != 0) { - mNetd.trafficSetNetPermForUids(PERMISSION_UPDATE_DEVICE_STATS, + mBpfNetMaps.setNetPermForUids(PERMISSION_UPDATE_DEVICE_STATS, toIntArray(updateStatsPermissionAppIds)); } if (noPermissionAppIds.size() != 0) { - mNetd.trafficSetNetPermForUids(PERMISSION_NONE, toIntArray(noPermissionAppIds)); + mBpfNetMaps.setNetPermForUids(PERMISSION_NONE, + toIntArray(noPermissionAppIds)); } if (uninstalledAppIds.size() != 0) { - mNetd.trafficSetNetPermForUids(PERMISSION_UNINSTALLED, + mBpfNetMaps.setNetPermForUids(PERMISSION_UNINSTALLED, toIntArray(uninstalledAppIds)); } - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(TAG, "Pass appId list of special permission failed." + e); } }
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java index b345ede..71f342d 100644 --- a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java +++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -38,16 +38,22 @@ @NonNull public final UserHandle user; // Capabilities are only null when sending an object to remove the setting for a user @Nullable public final NetworkCapabilities capabilities; + public final boolean allowFallback; public Preference(@NonNull final UserHandle user, - @Nullable final NetworkCapabilities capabilities) { + @Nullable final NetworkCapabilities capabilities, + final boolean allowFallback) { this.user = user; this.capabilities = null == capabilities ? null : new NetworkCapabilities(capabilities); + this.allowFallback = allowFallback; } /** toString */ public String toString() { - return "[ProfileNetworkPreference user=" + user + " caps=" + capabilities + "]"; + return "[ProfileNetworkPreference user=" + user + + " caps=" + capabilities + + " allowFallback=" + allowFallback + + "]"; } }
diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java new file mode 100644 index 0000000..7318296 --- /dev/null +++ b/service/src/com/android/server/connectivity/UidRangeUtils.java
@@ -0,0 +1,156 @@ +/* + * 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 android.annotation.NonNull; +import android.net.UidRange; +import android.util.ArraySet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Utility class for UidRange + * + * @hide + */ +public final class UidRangeUtils { + /** + * Check if given uid range set is within the uid range + * @param uids uid range in which uidRangeSet is checked to be in range. + * @param uidRangeSet uid range set to be be checked if it is in range of uids + * @return true uidRangeSet is in the range of uids + * @hide + */ + public static boolean isRangeSetInUidRange(@NonNull UidRange uids, + @NonNull Set<UidRange> uidRangeSet) { + Objects.requireNonNull(uids); + Objects.requireNonNull(uidRangeSet); + if (uidRangeSet.size() == 0) { + return true; + } + for (UidRange range : uidRangeSet) { + if (!uids.contains(range.start) || !uids.contains(range.stop)) { + return false; + } + } + return true; + } + + /** + * Remove given uid ranges set from a uid range + * @param uids uid range from which uidRangeSet will be removed + * @param uidRangeSet uid range set to be removed from uids. + * WARNING : This function requires the UidRanges in uidRangeSet to be disjoint + * WARNING : This function requires the arrayset to be iterated in increasing order of the + * ranges. Today this is provided by the iteration order stability of + * ArraySet, and the fact that the code creating this ArraySet always + * creates it in increasing order. + * Note : if any of the above is not satisfied this function throws IllegalArgumentException + * TODO : remove these limitations + * @hide + */ + public static ArraySet<UidRange> removeRangeSetFromUidRange(@NonNull UidRange uids, + @NonNull ArraySet<UidRange> uidRangeSet) { + Objects.requireNonNull(uids); + Objects.requireNonNull(uidRangeSet); + final ArraySet<UidRange> filteredRangeSet = new ArraySet<UidRange>(); + if (uidRangeSet.size() == 0) { + filteredRangeSet.add(uids); + return filteredRangeSet; + } + + int start = uids.start; + UidRange previousRange = null; + for (UidRange uidRange : uidRangeSet) { + if (previousRange != null) { + if (previousRange.stop > uidRange.start) { + throw new IllegalArgumentException("UID ranges are not increasing order"); + } + } + if (uidRange.start > start) { + filteredRangeSet.add(new UidRange(start, uidRange.start - 1)); + start = uidRange.stop + 1; + } else if (uidRange.start == start) { + start = uidRange.stop + 1; + } + previousRange = uidRange; + } + if (start < uids.stop) { + filteredRangeSet.add(new UidRange(start, uids.stop)); + } + return filteredRangeSet; + } + + /** + * Compare if the given UID range sets have overlapping uids + * @param uidRangeSet1 first uid range set to check for overlap + * @param uidRangeSet2 second uid range set to check for overlap + * @hide + */ + public static boolean doesRangeSetOverlap(@NonNull Set<UidRange> uidRangeSet1, + @NonNull Set<UidRange> uidRangeSet2) { + Objects.requireNonNull(uidRangeSet1); + Objects.requireNonNull(uidRangeSet2); + + if (uidRangeSet1.size() == 0 || uidRangeSet2.size() == 0) { + return false; + } + for (UidRange range1 : uidRangeSet1) { + for (UidRange range2 : uidRangeSet2) { + if (range1.contains(range2.start) || range1.contains(range2.stop) + || range2.contains(range1.start) || range2.contains(range1.stop)) { + return true; + } + } + } + return false; + } + + /** + * Convert a list of uid to set of UidRanges. + * @param uids list of uids + * @return set of UidRanges + * @hide + */ + public static ArraySet<UidRange> convertListToUidRange(@NonNull List<Integer> uids) { + Objects.requireNonNull(uids); + final ArraySet<UidRange> uidRangeSet = new ArraySet<UidRange>(); + if (uids.size() == 0) { + return uidRangeSet; + } + List<Integer> uidsNew = new ArrayList<>(uids); + Collections.sort(uidsNew); + int start = uidsNew.get(0); + int stop = start; + + for (Integer i : uidsNew) { + if (i <= stop + 1) { + stop = i; + } else { + uidRangeSet.add(new UidRange(start, stop)); + start = i; + stop = i; + } + } + uidRangeSet.add(new UidRange(start, stop)); + return uidRangeSet; + } +}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp index c533dab..acf04bf 100644 --- a/tests/common/Android.bp +++ b/tests/common/Android.bp
@@ -114,6 +114,7 @@ // meaning @hide APIs in framework-connectivity are resolved before @SystemApi // stubs in framework "framework-connectivity.impl", + "framework-connectivity-tiramisu.impl", "framework-tethering.impl", "framework", @@ -121,3 +122,25 @@ "framework-res", ], } + +// Defaults for tests that want to run in mainline-presubmit. +// Not widely used because many of our tests have AndroidTest.xml files and +// use the mainline-param config-descriptor metadata in AndroidTest.xml. + +// test_mainline_modules is an array of strings. Each element in the array is a list of modules +// separated by "+". The modules in this list must be in alphabetical order. +// See SuiteModuleLoader.java. +// TODO: why are the modules separated by + instead of being separate entries in the array? +mainline_presubmit_modules = [ + "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex", +] + +cc_defaults { + name: "connectivity-mainline-presubmit-cc-defaults", + test_mainline_modules: mainline_presubmit_modules, +} + +java_defaults { + name: "connectivity-mainline-presubmit-java-defaults", + test_mainline_modules: mainline_presubmit_modules, +}
diff --git a/tests/common/java/ParseExceptionTest.kt b/tests/common/java/ParseExceptionTest.kt index b702d61..ca01c76 100644 --- a/tests/common/java/ParseExceptionTest.kt +++ b/tests/common/java/ParseExceptionTest.kt
@@ -18,6 +18,7 @@ import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import junit.framework.Assert.assertEquals import junit.framework.Assert.assertNull @@ -27,6 +28,7 @@ @SmallTest @RunWith(AndroidJUnit4::class) +@ConnectivityModuleTest class ParseExceptionTest { @get:Rule val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.R)
diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt index 18a9331..f927380 100644 --- a/tests/common/java/android/net/CaptivePortalDataTest.kt +++ b/tests/common/java/android/net/CaptivePortalDataTest.kt
@@ -19,7 +19,6 @@ import android.os.Build import androidx.test.filters.SmallTest import com.android.modules.utils.build.SdkLevel -import com.android.testutils.assertParcelSane import com.android.testutils.assertParcelingIsLossless import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo @@ -71,9 +70,8 @@ @Test fun testParcelUnparcel() { - val fieldCount = if (SdkLevel.isAtLeastS()) 10 else 7 - assertParcelSane(data, fieldCount) - assertParcelSane(dataFromPasspoint, fieldCount) + assertParcelingIsLossless(data) + assertParcelingIsLossless(dataFromPasspoint) assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build()) assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build())
diff --git a/tests/common/java/android/net/ConnectivityDiagnosticsManagerTest.java b/tests/common/java/android/net/ConnectivityDiagnosticsManagerTest.java index 294ed10..03a9a80 100644 --- a/tests/common/java/android/net/ConnectivityDiagnosticsManagerTest.java +++ b/tests/common/java/android/net/ConnectivityDiagnosticsManagerTest.java
@@ -21,7 +21,7 @@ import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; import static android.net.ConnectivityDiagnosticsManager.DataStallReport; -import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -202,7 +202,7 @@ @Test public void testConnectivityReportParcelUnparcel() { - assertParcelSane(createSampleConnectivityReport(), 5); + assertParcelingIsLossless(createSampleConnectivityReport()); } private DataStallReport createSampleDataStallReport() { @@ -303,7 +303,7 @@ @Test public void testDataStallReportParcelUnparcel() { - assertParcelSane(createSampleDataStallReport(), 6); + assertParcelingIsLossless(createSampleDataStallReport()); } @Test
diff --git a/tests/common/java/android/net/DhcpInfoTest.java b/tests/common/java/android/net/DhcpInfoTest.java index ab4726b..b42e183 100644 --- a/tests/common/java/android/net/DhcpInfoTest.java +++ b/tests/common/java/android/net/DhcpInfoTest.java
@@ -17,7 +17,6 @@ package android.net; import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTL; -import static com.android.testutils.MiscAsserts.assertFieldCountEquals; import static com.android.testutils.ParcelUtils.parcelingRoundTrip; import static org.junit.Assert.assertEquals; @@ -101,7 +100,6 @@ // Cannot use assertParcelSane() here because this requires .equals() to work as // defined, but DhcpInfo has a different legacy behavior that we cannot change. final DhcpInfo dhcpInfo = createDhcpInfoObject(); - assertFieldCountEquals(7, DhcpInfo.class); final DhcpInfo dhcpInfoRoundTrip = parcelingRoundTrip(dhcpInfo); assertTrue(dhcpInfoEquals(null, null));
diff --git a/tests/common/java/android/net/IpPrefixTest.java b/tests/common/java/android/net/IpPrefixTest.java index f61c8c3..fef6416 100644 --- a/tests/common/java/android/net/IpPrefixTest.java +++ b/tests/common/java/android/net/IpPrefixTest.java
@@ -17,7 +17,6 @@ package android.net; import static com.android.testutils.MiscAsserts.assertEqualBothWays; -import static com.android.testutils.MiscAsserts.assertFieldCountEquals; import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; @@ -31,6 +30,8 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.ConnectivityModuleTest; + import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +40,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class IpPrefixTest { private static InetAddress address(String addr) { @@ -371,7 +373,5 @@ p = new IpPrefix("192.0.2.0/25"); assertParcelingIsLossless(p); assertTrue(p.isIPv4()); - - assertFieldCountEquals(2, IpPrefix.class); } }
diff --git a/tests/common/java/android/net/LinkAddressTest.java b/tests/common/java/android/net/LinkAddressTest.java index 2cf3cf9..6b04fee 100644 --- a/tests/common/java/android/net/LinkAddressTest.java +++ b/tests/common/java/android/net/LinkAddressTest.java
@@ -28,7 +28,6 @@ import static android.system.OsConstants.RT_SCOPE_UNIVERSE; import static com.android.testutils.MiscAsserts.assertEqualBothWays; -import static com.android.testutils.MiscAsserts.assertFieldCountEquals; import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; @@ -44,8 +43,8 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule; -import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import org.junit.Rule; @@ -63,6 +62,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class LinkAddressTest { @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); @@ -352,17 +352,6 @@ assertParcelingIsLossless(l); } - @Test @IgnoreAfter(Build.VERSION_CODES.Q) - public void testFieldCount_Q() { - assertFieldCountEquals(4, LinkAddress.class); - } - - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) - public void testFieldCount() { - // Make sure any new field is covered by the above parceling tests when changing this number - assertFieldCountEquals(6, LinkAddress.class); - } - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) public void testDeprecationTime() { try {
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java index 550953d..4d85a57 100644 --- a/tests/common/java/android/net/LinkPropertiesTest.java +++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,7 +20,6 @@ import static android.net.RouteInfo.RTN_UNICAST; import static android.net.RouteInfo.RTN_UNREACHABLE; -import static com.android.testutils.ParcelUtils.assertParcelSane; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static com.android.testutils.ParcelUtils.parcelingRoundTrip; @@ -41,6 +40,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; @@ -60,6 +60,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class LinkPropertiesTest { @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); @@ -1006,7 +1007,7 @@ @Test @IgnoreAfter(Build.VERSION_CODES.Q) public void testLinkPropertiesParcelable_Q() throws Exception { final LinkProperties source = makeLinkPropertiesForParceling(); - assertParcelSane(source, 14 /* fieldCount */); + assertParcelingIsLossless(source); } @Test @IgnoreUpTo(Build.VERSION_CODES.Q) @@ -1017,8 +1018,7 @@ source.setCaptivePortalApiUrl(CAPPORT_API_URL); source.setCaptivePortalData((CaptivePortalData) getCaptivePortalData()); source.setDhcpServerAddress((Inet4Address) GATEWAY1); - assertParcelSane(new LinkProperties(source, true /* parcelSensitiveFields */), - 18 /* fieldCount */); + assertParcelingIsLossless(new LinkProperties(source, true /* parcelSensitiveFields */)); // Verify that without using a sensitiveFieldsParcelingCopy, sensitive fields are cleared. final LinkProperties sanitized = new LinkProperties(source);
diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt index a5e44d5..4a4859d 100644 --- a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt +++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
@@ -22,14 +22,11 @@ import android.os.Build import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 - -import com.android.testutils.assertParcelSane +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo - -import java.lang.IllegalStateException - +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertFalse import org.junit.Rule import org.junit.Test @@ -38,6 +35,7 @@ @RunWith(AndroidJUnit4::class) @SmallTest +@ConnectivityModuleTest class MatchAllNetworkSpecifierTest { @Rule @JvmField val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() @@ -50,7 +48,7 @@ @Test fun testParcel() { - assertParcelSane(MatchAllNetworkSpecifier(), 0) + assertParcelingIsLossless(MatchAllNetworkSpecifier()) } @Test
diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt index 46f39dd..ad7a526 100644 --- a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt +++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
@@ -23,10 +23,9 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.testutils.assertEqualBothWays -import com.android.testutils.assertFieldCountEquals -import com.android.testutils.assertParcelSane import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.assertParcelingIsLossless import com.android.testutils.parcelingRoundTrip import java.net.InetAddress import org.junit.Assert.assertEquals @@ -93,7 +92,7 @@ @Test @IgnoreUpTo(Build.VERSION_CODES.Q) fun testParcel() { - assertParcelSane(nattKeepalivePacket(), 0) + assertParcelingIsLossless(nattKeepalivePacket()) } @Test @IgnoreUpTo(Build.VERSION_CODES.Q) @@ -103,8 +102,6 @@ assertNotEquals(nattKeepalivePacket(srcAddress = TEST_DST_ADDRV4), nattKeepalivePacket()) // Test src port only because dst port have to be NATT_PORT assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket()) - // Make sure the parceling test is updated if fields are added in the base class. - assertFieldCountEquals(5, KeepalivePacketData::class.java) } @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt index ed9995c..11d3ba0 100644 --- a/tests/common/java/android/net/NetworkAgentConfigTest.kt +++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -20,9 +20,11 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.modules.utils.build.SdkLevel.isAtLeastS +import com.android.modules.utils.build.SdkLevel.isAtLeastT +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -32,6 +34,7 @@ @RunWith(AndroidJUnit4::class) @SmallTest +@ConnectivityModuleTest class NetworkAgentConfigTest { @Rule @JvmField val ignoreRule = DevSdkIgnoreRule() @@ -47,21 +50,12 @@ if (isAtLeastS()) { setBypassableVpn(true) } + if (isAtLeastT()) { + setExcludeLocalRoutesVpn(true) + setVpnRequiresValidation(true) + } }.build() - // This test can be run as unit test against the latest system image, as CTS to verify - // an Android release that is as recent as the test, or as MTS to verify the - // Connectivity module. In the first two cases NetworkAgentConfig will be as recent - // as the test. In the last case, starting from S NetworkAgentConfig is updated as part - // of Connectivity, so it is also as recent as the test. For MTS on Q and R, - // NetworkAgentConfig is not updatable, so it may have a different number of fields. - if (isAtLeastS()) { - // When this test is run on S+, NetworkAgentConfig is as recent as the test, - // so this should be the most recent known number of fields. - assertParcelSane(config, 13) - } else { - // For R or below, the config will have 10 items - assertParcelSane(config, 10) - } + assertParcelingIsLossless(config) } @Test @IgnoreUpTo(Build.VERSION_CODES.Q) @@ -80,6 +74,10 @@ setProvisioningNotificationEnabled(false) setBypassableVpn(true) } + if (isAtLeastT()) { + setExcludeLocalRoutesVpn(true) + setVpnRequiresValidation(true) + } }.build() assertTrue(config.isExplicitlySelected()) @@ -88,6 +86,10 @@ assertFalse(config.isPartialConnectivityAcceptable()) assertTrue(config.isUnvalidatedConnectivityAcceptable()) assertEquals("TEST_NETWORK", config.getLegacyTypeName()) + if (isAtLeastT()) { + assertTrue(config.getExcludeLocalRouteVpn()) + assertTrue(config.getVpnRequiresValidation()) + } if (isAtLeastS()) { assertEquals(testExtraInfo, config.getLegacyExtraInfo()) assertFalse(config.isNat64DetectionEnabled())
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java index 2a4df7a..742044b 100644 --- a/tests/common/java/android/net/NetworkCapabilitiesTest.java +++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -23,11 +23,6 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS; import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; -import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1; -import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2; -import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3; -import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4; -import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5; import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; @@ -39,9 +34,16 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY; import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; @@ -57,9 +59,9 @@ import static com.android.modules.utils.build.SdkLevel.isAtLeastR; import static com.android.modules.utils.build.SdkLevel.isAtLeastS; import static com.android.modules.utils.build.SdkLevel.isAtLeastT; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static com.android.testutils.MiscAsserts.assertEmpty; import static com.android.testutils.MiscAsserts.assertThrows; -import static com.android.testutils.ParcelUtils.assertParcelSane; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertArrayEquals; @@ -306,6 +308,48 @@ } } + @Test @IgnoreUpTo(SC_V2) + public void testSetAccessUids() { + final NetworkCapabilities nc = new NetworkCapabilities(); + assertThrows(NullPointerException.class, () -> nc.setAccessUids(null)); + assertFalse(nc.hasAccessUids()); + assertFalse(nc.isAccessUid(0)); + assertFalse(nc.isAccessUid(1000)); + assertEquals(0, nc.getAccessUids().size()); + nc.setAccessUids(new ArraySet<>()); + assertFalse(nc.hasAccessUids()); + assertFalse(nc.isAccessUid(0)); + assertFalse(nc.isAccessUid(1000)); + assertEquals(0, nc.getAccessUids().size()); + + final ArraySet<Integer> uids = new ArraySet<>(); + uids.add(200); + uids.add(250); + uids.add(-1); + uids.add(Integer.MAX_VALUE); + nc.setAccessUids(uids); + assertNotEquals(nc, new NetworkCapabilities()); + assertTrue(nc.hasAccessUids()); + + final List<Integer> includedList = List.of(-2, 0, 199, 700, 901, 1000, Integer.MIN_VALUE); + final List<Integer> excludedList = List.of(-1, 200, 250, Integer.MAX_VALUE); + for (final int uid : includedList) { + assertFalse(nc.isAccessUid(uid)); + } + for (final int uid : excludedList) { + assertTrue(nc.isAccessUid(uid)); + } + + final Set<Integer> outUids = nc.getAccessUids(); + assertEquals(4, outUids.size()); + for (final int uid : includedList) { + assertFalse(outUids.contains(uid)); + } + for (final int uid : excludedList) { + assertTrue(outUids.contains(uid)); + } + } + @Test public void testParcelNetworkCapabilities() { final Set<Range<Integer>> uids = new ArraySet<>(); @@ -316,6 +360,10 @@ .addCapability(NET_CAPABILITY_EIMS) .addCapability(NET_CAPABILITY_NOT_METERED); if (isAtLeastS()) { + final ArraySet<Integer> accessUids = new ArraySet<>(); + accessUids.add(4); + accessUids.add(9); + netCap.setAccessUids(accessUids); netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)); netCap.setUids(uids); } @@ -344,21 +392,7 @@ } private void testParcelSane(NetworkCapabilities cap) { - // This test can be run as unit test against the latest system image, as CTS to verify - // an Android release that is as recent as the test, or as MTS to verify the - // Connectivity module. In the first two cases NetworkCapabilities will be as recent - // as the test. In the last case, starting from S NetworkCapabilities is updated as part - // of Connectivity, so it is also as recent as the test. For MTS on Q and R, - // NetworkCapabilities is not updatable, so it may have a different number of fields. - if (isAtLeastS()) { - // When this test is run on S+, NetworkCapabilities is as recent as the test, - // so this should be the most recent known number of fields. - assertParcelSane(cap, 18); - } else if (isAtLeastR()) { - assertParcelSane(cap, 15); - } else { - assertParcelSane(cap, 11); - } + assertParcelingIsLossless(cap); } private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfo() { @@ -429,6 +463,31 @@ assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities())); } + @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + public void testPrioritizeLatencyAndBandwidth() { + NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + @Test @IgnoreUpTo(Build.VERSION_CODES.R) public void testOemPrivate() { NetworkCapabilities nc = new NetworkCapabilities(); @@ -803,79 +862,80 @@ } catch (IllegalStateException expected) { } } - @Test @IgnoreUpTo(Build.VERSION_CODES.S) - public void testEnterpriseCapabilitySubLevel() { + @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + public void testEnterpriseId() { final NetworkCapabilities nc1 = new NetworkCapabilities.Builder() .addCapability(NET_CAPABILITY_ENTERPRISE) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) + .addEnterpriseId(NET_ENTERPRISE_ID_1) .build(); - assertEquals(1, nc1.getEnterpriseCapabilitySubLevels().length); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1, - nc1.getEnterpriseCapabilitySubLevels()[0]); + assertEquals(1, nc1.getEnterpriseIds().length); + assertEquals(NET_ENTERPRISE_ID_1, + nc1.getEnterpriseIds()[0]); final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() .addCapability(NET_CAPABILITY_ENTERPRISE) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) + .addEnterpriseId(NET_ENTERPRISE_ID_1) + .addEnterpriseId(NET_ENTERPRISE_ID_2) .build(); - assertEquals(2, nc2.getEnterpriseCapabilitySubLevels().length); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1, - nc2.getEnterpriseCapabilitySubLevels()[0]); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2, - nc2.getEnterpriseCapabilitySubLevels()[1]); + assertEquals(2, nc2.getEnterpriseIds().length); + assertEquals(NET_ENTERPRISE_ID_1, + nc2.getEnterpriseIds()[0]); + assertEquals(NET_ENTERPRISE_ID_2, + nc2.getEnterpriseIds()[1]); final NetworkCapabilities nc3 = new NetworkCapabilities.Builder() .addCapability(NET_CAPABILITY_ENTERPRISE) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5) + .addEnterpriseId(NET_ENTERPRISE_ID_1) + .addEnterpriseId(NET_ENTERPRISE_ID_2) + .addEnterpriseId(NET_ENTERPRISE_ID_3) + .addEnterpriseId(NET_ENTERPRISE_ID_4) + .addEnterpriseId(NET_ENTERPRISE_ID_5) .build(); - assertEquals(5, nc3.getEnterpriseCapabilitySubLevels().length); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1, - nc3.getEnterpriseCapabilitySubLevels()[0]); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2, - nc3.getEnterpriseCapabilitySubLevels()[1]); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3, - nc3.getEnterpriseCapabilitySubLevels()[2]); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4, - nc3.getEnterpriseCapabilitySubLevels()[3]); - assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5, - nc3.getEnterpriseCapabilitySubLevels()[4]); + assertEquals(5, nc3.getEnterpriseIds().length); + assertEquals(NET_ENTERPRISE_ID_1, + nc3.getEnterpriseIds()[0]); + assertEquals(NET_ENTERPRISE_ID_2, + nc3.getEnterpriseIds()[1]); + assertEquals(NET_ENTERPRISE_ID_3, + nc3.getEnterpriseIds()[2]); + assertEquals(NET_ENTERPRISE_ID_4, + nc3.getEnterpriseIds()[3]); + assertEquals(NET_ENTERPRISE_ID_5, + nc3.getEnterpriseIds()[4]); final Class<IllegalArgumentException> illegalArgumentExceptionClass = IllegalArgumentException.class; assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder() - .addEnterpriseCapabilitySubLevel(6) + .addEnterpriseId(6) .build()); assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder() - .removeEnterpriseCapabilitySubLevel(6) + .removeEnterpriseId(6) .build()); final Class<IllegalStateException> illegalStateException = IllegalStateException.class; assertThrows(illegalStateException, () -> new NetworkCapabilities.Builder() - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) + .addEnterpriseId(NET_ENTERPRISE_ID_1) .build()); final NetworkCapabilities nc4 = new NetworkCapabilities.Builder() .addCapability(NET_CAPABILITY_ENTERPRISE) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) - .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) + .addEnterpriseId(NET_ENTERPRISE_ID_1) + .addEnterpriseId(NET_ENTERPRISE_ID_2) + .removeEnterpriseId(NET_ENTERPRISE_ID_1) + .removeEnterpriseId(NET_ENTERPRISE_ID_2) .build(); - assertEquals(0, nc4.getEnterpriseCapabilitySubLevels().length); + assertEquals(1, nc4.getEnterpriseIds().length); + assertTrue(nc4.hasEnterpriseId(NET_ENTERPRISE_ID_1)); final NetworkCapabilities nc5 = new NetworkCapabilities.Builder() .addCapability(NET_CAPABILITY_CBS) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) - .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1) - .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2) + .addEnterpriseId(NET_ENTERPRISE_ID_1) + .addEnterpriseId(NET_ENTERPRISE_ID_2) + .removeEnterpriseId(NET_ENTERPRISE_ID_1) + .removeEnterpriseId(NET_ENTERPRISE_ID_2) .build(); assertTrue(nc4.satisfiedByNetworkCapabilities(nc1)); - assertFalse(nc1.satisfiedByNetworkCapabilities(nc4)); + assertTrue(nc1.satisfiedByNetworkCapabilities(nc4)); assertFalse(nc3.satisfiedByNetworkCapabilities(nc2)); assertTrue(nc2.satisfiedByNetworkCapabilities(nc3));
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt index ff5de1d..3ceacf8 100644 --- a/tests/common/java/android/net/NetworkProviderTest.kt +++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -32,6 +32,7 @@ import androidx.test.InstrumentationRegistry import com.android.net.module.util.ArrayTrackRecord import com.android.testutils.CompatUtil +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo @@ -62,6 +63,7 @@ @RunWith(DevSdkIgnoreRunner::class) @IgnoreUpTo(Build.VERSION_CODES.Q) +@ConnectivityModuleTest class NetworkProviderTest { @Rule @JvmField val mIgnoreRule = DevSdkIgnoreRule()
diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt index f3409f5..b960417 100644 --- a/tests/common/java/android/net/NetworkSpecifierTest.kt +++ b/tests/common/java/android/net/NetworkSpecifierTest.kt
@@ -17,18 +17,20 @@ import android.os.Build import androidx.test.filters.SmallTest +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo import com.android.testutils.DevSdkIgnoreRunner -import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals -import org.junit.Test -import org.junit.runner.RunWith +import kotlin.test.assertTrue @SmallTest @RunWith(DevSdkIgnoreRunner::class) @IgnoreUpTo(Build.VERSION_CODES.Q) +@ConnectivityModuleTest class NetworkSpecifierTest { private class TestNetworkSpecifier( val intData: Int = 123,
diff --git a/tests/common/java/android/net/NetworkStateSnapshotTest.kt b/tests/common/java/android/net/NetworkStateSnapshotTest.kt index 0ca4d95..0dad6a8 100644 --- a/tests/common/java/android/net/NetworkStateSnapshotTest.kt +++ b/tests/common/java/android/net/NetworkStateSnapshotTest.kt
@@ -22,9 +22,10 @@ import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.os.Build import androidx.test.filters.SmallTest +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRunner -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Test import org.junit.runner.RunWith import java.net.Inet4Address @@ -59,6 +60,7 @@ @SmallTest @RunWith(DevSdkIgnoreRunner::class) @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +@ConnectivityModuleTest class NetworkStateSnapshotTest { @Test @@ -67,7 +69,7 @@ LinkProperties(), null, TYPE_NONE) val snapshot = NetworkStateSnapshot( Network(TEST_NETID), TEST_CAPABILITIES, TEST_LINK_PROPERTIES, TEST_IMSI, TYPE_WIFI) - assertParcelSane(emptySnapshot, 5) - assertParcelSane(snapshot, 5) + assertParcelingIsLossless(emptySnapshot) + assertParcelingIsLossless(snapshot) } }
diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java index 7423c73..c102cb3 100644 --- a/tests/common/java/android/net/NetworkTest.java +++ b/tests/common/java/android/net/NetworkTest.java
@@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; @@ -46,6 +47,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class NetworkTest { final Network mNetwork = new Network(99);
diff --git a/tests/common/java/android/net/OemNetworkPreferencesTest.java b/tests/common/java/android/net/OemNetworkPreferencesTest.java index fd29a95..d96f80c 100644 --- a/tests/common/java/android/net/OemNetworkPreferencesTest.java +++ b/tests/common/java/android/net/OemNetworkPreferencesTest.java
@@ -17,7 +17,7 @@ package android.net; import static com.android.testutils.MiscAsserts.assertThrows; -import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; @@ -38,6 +39,7 @@ @IgnoreUpTo(Build.VERSION_CODES.R) @RunWith(DevSdkIgnoreRunner.class) @SmallTest +@ConnectivityModuleTest public class OemNetworkPreferencesTest { private static final int TEST_PREF = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED; @@ -101,7 +103,7 @@ final OemNetworkPreferences prefs = mBuilder.build(); - assertParcelSane(prefs, 1 /* fieldCount */); + assertParcelingIsLossless(prefs); } @Test
diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java index b69b045..5b28b84 100644 --- a/tests/common/java/android/net/RouteInfoTest.java +++ b/tests/common/java/android/net/RouteInfoTest.java
@@ -21,7 +21,6 @@ import static android.net.RouteInfo.RTN_UNREACHABLE; import static com.android.testutils.MiscAsserts.assertEqualBothWays; -import static com.android.testutils.MiscAsserts.assertFieldCountEquals; import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; @@ -38,8 +37,8 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule; -import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import org.junit.Rule; @@ -52,6 +51,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class RouteInfoTest { @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); @@ -383,17 +383,6 @@ assertParcelingIsLossless(r); } - @Test @IgnoreAfter(Build.VERSION_CODES.Q) - public void testFieldCount_Q() { - assertFieldCountEquals(6, RouteInfo.class); - } - - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) - public void testFieldCount() { - // Make sure any new field is covered by the above parceling tests when changing this number - assertFieldCountEquals(7, RouteInfo.class); - } - @Test @IgnoreUpTo(Build.VERSION_CODES.Q) public void testMtu() { RouteInfo r;
diff --git a/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt index 7a18bb0..063ea23 100644 --- a/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt +++ b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt
@@ -20,8 +20,7 @@ import android.os.Build import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRunner -import com.android.testutils.assertFieldCountEquals -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Test import org.junit.runner.RunWith import java.net.InetAddress @@ -68,15 +67,11 @@ assertNotEquals(makeData(tcpWndScale = 3), makeData()) assertNotEquals(makeData(ipTos = 0x14), makeData()) assertNotEquals(makeData(ipTtl = 11), makeData()) - - // Update above assertions if field is added - assertFieldCountEquals(5, KeepalivePacketData::class.java) - assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) } @Test fun testParcelUnparcel() { - assertParcelSane(makeData(), fieldCount = 6) { a, b -> + assertParcelingIsLossless(makeData()) { a, b -> // .equals() does not verify .packet a == b && a.packet contentEquals b.packet } @@ -98,9 +93,5 @@ assertTrue(str.contains(data.getTcpWindowScale().toString())) assertTrue(str.contains(data.getIpTos().toString())) assertTrue(str.contains(data.getIpTtl().toString())) - - // Update above assertions if field is added - assertFieldCountEquals(5, KeepalivePacketData::class.java) - assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) } } \ No newline at end of file
diff --git a/tests/common/java/android/net/UidRangeTest.java b/tests/common/java/android/net/UidRangeTest.java index a435119..d46fdc9 100644 --- a/tests/common/java/android/net/UidRangeTest.java +++ b/tests/common/java/android/net/UidRangeTest.java
@@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.ConnectivityModuleTest; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; @@ -46,6 +47,7 @@ @RunWith(AndroidJUnit4.class) @SmallTest +@ConnectivityModuleTest public class UidRangeTest { /*
diff --git a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt index f23ba26..a041c4e 100644 --- a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt +++ b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
@@ -18,9 +18,10 @@ import android.os.Build import androidx.test.filters.SmallTest +import com.android.testutils.ConnectivityModuleTest import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRunner -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertEquals @@ -32,6 +33,7 @@ @SmallTest @RunWith(DevSdkIgnoreRunner::class) @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +@ConnectivityModuleTest class UnderlyingNetworkInfoTest { @Test fun testParcelUnparcel() { @@ -39,12 +41,12 @@ assertEquals(TEST_OWNER_UID, testInfo.getOwnerUid()) assertEquals(TEST_IFACE, testInfo.getInterface()) assertEquals(TEST_IFACE_LIST, testInfo.getUnderlyingInterfaces()) - assertParcelSane(testInfo, 3) + assertParcelingIsLossless(testInfo) val emptyInfo = UnderlyingNetworkInfo(0, String(), listOf()) assertEquals(0, emptyInfo.getOwnerUid()) assertEquals(String(), emptyInfo.getInterface()) assertEquals(listOf(), emptyInfo.getUnderlyingInterfaces()) - assertParcelSane(emptyInfo, 3) + assertParcelingIsLossless(emptyInfo) } } \ No newline at end of file
diff --git a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java index 88996d9..fa4adcb 100644 --- a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java +++ b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
@@ -16,7 +16,7 @@ package android.net.apf; -import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -62,7 +62,7 @@ assertEquals(456, caps.maximumApfProgramSize); assertEquals(789, caps.apfPacketFormat); - assertParcelSane(caps, 3); + assertParcelingIsLossless(caps); } @Test
diff --git a/tests/common/java/android/net/metrics/ApfProgramEventTest.kt b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt index 0b7b740..1c175da 100644 --- a/tests/common/java/android/net/metrics/ApfProgramEventTest.kt +++ b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -48,7 +48,7 @@ assertEquals(5, apfProgramEvent.programLength) assertEquals(ApfProgramEvent.flagsFor(true, true), apfProgramEvent.flags) - assertParcelSane(apfProgramEvent, 6) + assertParcelingIsLossless(apfProgramEvent) } @Test
diff --git a/tests/common/java/android/net/metrics/ApfStatsTest.kt b/tests/common/java/android/net/metrics/ApfStatsTest.kt index 46a8c8e..610e674 100644 --- a/tests/common/java/android/net/metrics/ApfStatsTest.kt +++ b/tests/common/java/android/net/metrics/ApfStatsTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -52,6 +52,6 @@ assertEquals(8, apfStats.programUpdatesAllowingMulticast) assertEquals(9, apfStats.maxProgramSize) - assertParcelSane(apfStats, 10) + assertParcelingIsLossless(apfStats) } }
diff --git a/tests/common/java/android/net/metrics/DhcpClientEventTest.kt b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt index 8d7a9c4..4c70e11 100644 --- a/tests/common/java/android/net/metrics/DhcpClientEventTest.kt +++ b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -38,6 +38,6 @@ assertEquals(FAKE_MESSAGE, dhcpClientEvent.msg) assertEquals(Integer.MAX_VALUE, dhcpClientEvent.durationMs) - assertParcelSane(dhcpClientEvent, 2) + assertParcelingIsLossless(dhcpClientEvent) } }
diff --git a/tests/common/java/android/net/metrics/IpManagerEventTest.kt b/tests/common/java/android/net/metrics/IpManagerEventTest.kt index 64be508..bb21dca 100644 --- a/tests/common/java/android/net/metrics/IpManagerEventTest.kt +++ b/tests/common/java/android/net/metrics/IpManagerEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -33,7 +33,7 @@ assertEquals(it, ipManagerEvent.eventType) assertEquals(Long.MAX_VALUE, ipManagerEvent.durationMs) - assertParcelSane(ipManagerEvent, 2) + assertParcelingIsLossless(ipManagerEvent) } } }
diff --git a/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt index 55b5e49..3d21b81 100644 --- a/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt +++ b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -32,7 +32,7 @@ val ipReachabilityEvent = IpReachabilityEvent(it) assertEquals(it, ipReachabilityEvent.eventType) - assertParcelSane(ipReachabilityEvent, 1) + assertParcelingIsLossless(ipReachabilityEvent) } } }
diff --git a/tests/common/java/android/net/metrics/NetworkEventTest.kt b/tests/common/java/android/net/metrics/NetworkEventTest.kt index 41430b0..17b5e2d 100644 --- a/tests/common/java/android/net/metrics/NetworkEventTest.kt +++ b/tests/common/java/android/net/metrics/NetworkEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -37,7 +37,7 @@ assertEquals(it, networkEvent.eventType) assertEquals(Long.MAX_VALUE, networkEvent.durationMs) - assertParcelSane(networkEvent, 2) + assertParcelingIsLossless(networkEvent) } } }
diff --git a/tests/common/java/android/net/metrics/RaEventTest.kt b/tests/common/java/android/net/metrics/RaEventTest.kt index d9b7203..e9daa0f 100644 --- a/tests/common/java/android/net/metrics/RaEventTest.kt +++ b/tests/common/java/android/net/metrics/RaEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -67,6 +67,6 @@ assertEquals(5, raEvent.rdnssLifetime) assertEquals(6, raEvent.dnsslLifetime) - assertParcelSane(raEvent, 6) + assertParcelingIsLossless(raEvent) } }
diff --git a/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt index 51c0d41..7dfa7e1 100644 --- a/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt +++ b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt
@@ -18,7 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 -import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless import java.lang.reflect.Modifier import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -51,7 +51,7 @@ assertTrue(validationProbeEvent.probeType hasType FIRST_VALIDATION) assertEquals(ValidationProbeEvent.DNS_SUCCESS, validationProbeEvent.returnCode) - assertParcelSane(validationProbeEvent, 3) + assertParcelingIsLossless(validationProbeEvent) } @Test
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt index 7b22e45..c90b1aa 100644 --- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt +++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -31,7 +31,6 @@ import android.os.Build import androidx.test.filters.SmallTest import com.android.testutils.DevSdkIgnoreRule -import com.android.testutils.assertFieldCountEquals import com.android.testutils.assertNetworkStatsEquals import com.android.testutils.assertParcelingIsLossless import org.junit.Before @@ -176,7 +175,6 @@ assertParcelingIsLossless(testStatsEmpty) assertParcelingIsLossless(testStats1) assertParcelingIsLossless(testStats2) - assertFieldCountEquals(15, NetworkStats::class.java) } @Test
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS index 4264345..8dfa455 100644 --- a/tests/cts/OWNERS +++ b/tests/cts/OWNERS
@@ -1,4 +1,3 @@ # Bug component: 31808 set noparent [email protected] [email protected] \ No newline at end of file +file:platform/packages/modules/Connectivity:master:/OWNERS_core_networking_xts
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java index 916b566..5778b0d 100755 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -36,6 +36,7 @@ import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.testutils.Cleanup.testAndCleanup; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -71,7 +72,6 @@ import android.net.VpnTransportInfo; import android.net.cts.util.CtsNetUtils; import android.net.wifi.WifiManager; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; @@ -830,7 +830,7 @@ .getCaps().getUnderlyingNetworks()))); } - @Test @IgnoreUpTo(Build.VERSION_CODES.S) + @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public void testChangeUnderlyingNetworks() throws Exception { assumeTrue(supportedHardware()); assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp index 4c9bccf..01c8cd2 100644 --- a/tests/cts/hostside/app2/Android.bp +++ b/tests/cts/hostside/app2/Android.bp
@@ -22,7 +22,10 @@ name: "CtsHostsideNetworkTestsApp2", defaults: ["cts_support_defaults"], sdk_version: "test_current", - static_libs: ["CtsHostsideNetworkTestsAidl"], + static_libs: [ + "CtsHostsideNetworkTestsAidl", + "NetworkStackApiStableShims", + ], srcs: ["src/**/*.java"], // Tag this module as a cts test artifact test_suites: [
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java index 3b5e46f..f2a7b3f 100644 --- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -21,6 +21,7 @@ import static com.android.cts.net.hostside.app2.Common.ACTION_SNOOZE_WARNING; import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER; import static com.android.cts.net.hostside.app2.Common.TAG; +import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -40,6 +41,7 @@ import com.android.cts.net.hostside.IMyService; import com.android.cts.net.hostside.INetworkCallback; +import com.android.modules.utils.build.SdkLevel; /** * Service used to dynamically register a broadcast receiver. @@ -64,11 +66,14 @@ return; } final Context context = getApplicationContext(); + final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0; mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER); - context.registerReceiver(mReceiver, new IntentFilter(ACTION_RECEIVER_READY)); context.registerReceiver(mReceiver, - new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED)); - context.registerReceiver(mReceiver, new IntentFilter(ACTION_SNOOZE_WARNING)); + new IntentFilter(ACTION_RECEIVER_READY), flags); + context.registerReceiver(mReceiver, + new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED), flags); + context.registerReceiver(mReceiver, + new IntentFilter(ACTION_SNOOZE_WARNING), flags); Log.d(TAG, "receiver registered"); }
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp index 81c30b1..e979a3b 100644 --- a/tests/cts/net/Android.bp +++ b/tests/cts/net/Android.bp
@@ -49,6 +49,7 @@ "FrameworksNetCommonTests", "core-tests-support", "cts-net-utils", + "CtsNetTestsNonUpdatableLib", "ctstestrunner-axt", "junit", "junit-params", @@ -69,7 +70,7 @@ // devices. android_test { name: "CtsNetTestCases", - defaults: ["CtsNetTestCasesDefaults", "NetworkStackNextEnableDefaults"], + defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"], // TODO: CTS should not depend on the entirety of the networkstack code. static_libs: [ "NetworkStackApiCurrentLib",
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp index 1d1c18e..153ff51 100644 --- a/tests/cts/net/native/Android.bp +++ b/tests/cts/net/native/Android.bp
@@ -43,6 +43,7 @@ static_libs: [ "libbpf_android", "libgtest", + "libmodules-utils-build", ], // Tag this module as a cts test artifact
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp index 874bad4..97ecb9e 100644 --- a/tests/cts/net/native/src/BpfCompatTest.cpp +++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -21,6 +21,8 @@ #include <gtest/gtest.h> +#include "android-modules-utils/sdk_level.h" + #include "libbpf_android.h" using namespace android::bpf; @@ -33,11 +35,17 @@ EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0)); } -TEST(BpfTest, bpfStructSizeTest) { +TEST(BpfTest, bpfStructSizeTestPreT) { + if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device."; doBpfStructSizeTest("/system/etc/bpf/netd.o"); doBpfStructSizeTest("/system/etc/bpf/clatd.o"); } +TEST(BpfTest, bpfStructSizeTest) { + doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o"); + doBpfStructSizeTest("/system/etc/bpf/time_in_state.o"); +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java index 232114e..68fa38d 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -278,18 +278,23 @@ assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId, carrierConfigReceiver.waitForCarrierConfigChanged()); - assertTrue("Don't have Carrier Privileges after adding cert for this package", - mTelephonyManager.createForSubscriptionId(subId).hasCarrierPrivileges()); // Wait for CarrierPrivilegesTracker to receive the ACTION_CARRIER_CONFIG_CHANGED // broadcast. CPT then needs to update the corresponding DataConnection, which then // updates ConnectivityService. Unfortunately, this update to the NetworkCapabilities in // CS does not trigger NetworkCallback#onCapabilitiesChanged as changing the // administratorUids is not a publicly visible change. In lieu of a better signal to - // detministically wait for, use Thread#sleep here. + // deterministically wait for, use Thread#sleep here. // TODO(b/157949581): replace this Thread#sleep with a deterministic signal Thread.sleep(DELAY_FOR_ADMIN_UIDS_MILLIS); + // TODO(b/217559768): Receiving carrier config change and immediately checking carrier + // privileges is racy, as the CP status is updated after receiving the same signal. Move + // the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed + // by switching to CarrierPrivilegesListener. + assertTrue("Don't have Carrier Privileges after adding cert for this package", + mTelephonyManager.createForSubscriptionId(subId).hasCarrierPrivileges()); + final TestConnectivityDiagnosticsCallback connDiagsCallback = createAndRegisterConnectivityDiagnosticsCallback(CELLULAR_NETWORK_REQUEST);
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTiramisuTest.kt b/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTiramisuTest.kt new file mode 100644 index 0000000..049372f --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTiramisuTest.kt
@@ -0,0 +1,47 @@ +/* + * 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 android.net.cts + +import android.net.nsd.NsdManager +import androidx.test.platform.app.InstrumentationRegistry +import com.android.networkstack.apishim.ConnectivityFrameworkInitShimImpl +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.SC_V2 +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertNotNull + +private val cfiShim = ConnectivityFrameworkInitShimImpl.newInstance() + +@RunWith(DevSdkIgnoreRunner::class) +// ConnectivityFrameworkInitializerTiramisu was added in T +@IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available +class ConnectivityFrameworkInitializerTiramisuTest { + @Test + fun testServicesRegistered() { + val ctx = InstrumentationRegistry.getInstrumentation().context as android.content.Context + assertNotNull(ctx.getSystemService(NsdManager::class.java), + "NsdManager not registered") + } + + // registerServiceWrappers can only be called during initialization and should throw otherwise + @Test(expected = IllegalStateException::class) + fun testThrows() { + cfiShim.registerServiceWrappers() + } +}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java index 80c2db4..53e4ab7 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -168,6 +168,7 @@ import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRuleKt; +import com.android.testutils.DumpTestUtils; import com.android.testutils.RecorderCallback.CallbackEntry; import com.android.testutils.TestHttpServer; import com.android.testutils.TestNetworkTracker; @@ -3021,6 +3022,13 @@ } } + @Test + public void testDump() throws Exception { + final String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission( + Context.CONNECTIVITY_SERVICE, "--short"); + assertTrue(dumpOutput, dumpOutput.contains("Active default network")); + } + private void unregisterRegisteredCallbacks() { for (NetworkCallback callback: mRegisteredCallbacks) { mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt index 1a62560..555dd87 100644 --- a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt +++ b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
@@ -16,11 +16,11 @@ package android.net.cts -import android.os.Build import android.net.DhcpOption import androidx.test.filters.SmallTest import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.SC_V2 import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -28,7 +28,7 @@ import org.junit.Test @SmallTest -@IgnoreUpTo(Build.VERSION_CODES.S) +@IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available @RunWith(DevSdkIgnoreRunner::class) class DhcpOptionTest { private val DHCP_OPTION_TYPE: Byte = 2
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java index 4992795..c6fc38f 100644 --- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java +++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -25,6 +25,8 @@ import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; import static android.system.OsConstants.ETIMEDOUT; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -43,7 +45,6 @@ import android.net.NetworkRequest; import android.net.ParseException; import android.net.cts.util.CtsNetUtils; -import android.os.Build; import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; @@ -814,7 +815,7 @@ } /** Verifies that DnsResolver.DnsException can be subclassed and its constructor re-used. */ - @Test @IgnoreUpTo(Build.VERSION_CODES.S) + @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public void testDnsExceptionConstructor() throws InterruptedException { class TestDnsException extends DnsResolver.DnsException { TestDnsException(int code, @Nullable Throwable cause) {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt new file mode 100644 index 0000000..886b078 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -0,0 +1,523 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.cts + +import android.net.cts.util.CtsNetUtils.TestNetworkCallback + +import android.app.Instrumentation +import android.Manifest.permission.MANAGE_TEST_NETWORKS +import android.content.Context +import android.net.ConnectivityManager +import android.net.cts.util.CtsNetUtils +import android.net.DscpPolicy +import android.net.DscpPolicy.STATUS_DELETED +import android.net.DscpPolicy.STATUS_SUCCESS +import android.net.InetAddresses +import android.net.IpPrefix +import android.net.LinkAddress +import android.net.LinkProperties +import android.net.NetworkAgent +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkRequest +import android.net.TestNetworkInterface +import android.net.TestNetworkManager +import android.net.RouteInfo +import android.net.util.SocketUtils +import android.os.Build +import android.os.HandlerThread +import android.os.Looper +import android.platform.test.annotations.AppModeFull +import android.system.Os +import android.system.OsConstants +import android.system.OsConstants.AF_INET +import android.system.OsConstants.IPPROTO_IP +import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.SOCK_DGRAM +import android.system.OsConstants.SOCK_NONBLOCK +import android.util.Log +import android.util.Range +import androidx.test.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import com.android.modules.utils.build.SdkLevel +import com.android.testutils.CompatUtil +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.OffsetFilter +import com.android.testutils.assertParcelingIsLossless +import com.android.testutils.runAsShell +import com.android.testutils.SC_V2 +import com.android.testutils.TapPacketReader +import com.android.testutils.TestableNetworkAgent +import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated +import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.concurrent.thread +import kotlin.test.fail + +private const val MAX_PACKET_LENGTH = 1500 + +private val instrumentation: Instrumentation + get() = InstrumentationRegistry.getInstrumentation() + +private const val TAG = "DscpPolicyTest" +private const val PACKET_TIMEOUT_MS = 2_000L + +@AppModeFull(reason = "Instant apps cannot create test networks") +@RunWith(AndroidJUnit4::class) +class DscpPolicyTest { + @JvmField + @Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2) + + private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1") + private val TEST_TARGET_IPV4_ADDR = + InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address + + private val realContext = InstrumentationRegistry.getContext() + private val cm = realContext.getSystemService(ConnectivityManager::class.java) + + private val agentsToCleanUp = mutableListOf<NetworkAgent>() + private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>() + + private val handlerThread = HandlerThread(DscpPolicyTest::class.java.simpleName) + + private lateinit var iface: TestNetworkInterface + private lateinit var tunNetworkCallback: TestNetworkCallback + private lateinit var reader: TapPacketReader + + @Before + fun setUp() { + runAsShell(MANAGE_TEST_NETWORKS) { + val tnm = realContext.getSystemService(TestNetworkManager::class.java) + + iface = tnm.createTunInterface( Array(1){ LinkAddress(LOCAL_IPV4_ADDRESS, 32) } ) + assertNotNull(iface) + } + + handlerThread.start() + reader = TapPacketReader( + handlerThread.threadHandler, + iface.fileDescriptor.fileDescriptor, + MAX_PACKET_LENGTH) + reader.startAsyncForTest() + } + + @After + fun tearDown() { + agentsToCleanUp.forEach { it.unregister() } + callbacksToCleanUp.forEach { cm.unregisterNetworkCallback(it) } + + // reader.stop() cleans up tun fd + reader.handler.post { reader.stop() } + handlerThread.quitSafely() + } + + private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) { + cm.requestNetwork(request, callback) + callbacksToCleanUp.add(callback) + } + + private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest { + return NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_NOT_RESTRICTED) + .addTransportType(TRANSPORT_TEST) + .also { + if (specifier != null) { + it.setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier)) + } + } + .build() + } + + private fun createConnectedNetworkAgent( + context: Context = realContext, + specifier: String? = iface.getInterfaceName(), + ): Pair<TestableNetworkAgent, TestableNetworkCallback> { + val callback = TestableNetworkCallback() + // Ensure this NetworkAgent is never unneeded by filing a request with its specifier. + requestNetwork(makeTestNetworkRequest(specifier = specifier), callback) + + val nc = NetworkCapabilities().apply { + addTransportType(TRANSPORT_TEST) + removeCapability(NET_CAPABILITY_TRUSTED) + removeCapability(NET_CAPABILITY_INTERNET) + addCapability(NET_CAPABILITY_NOT_SUSPENDED) + addCapability(NET_CAPABILITY_NOT_ROAMING) + addCapability(NET_CAPABILITY_NOT_VPN) + addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + if (null != specifier) { + setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier)) + } + } + val lp = LinkProperties().apply { + addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32)) + addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null)) + setInterfaceName(iface.getInterfaceName()) + } + val config = NetworkAgentConfig.Builder().build() + val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config) + agentsToCleanUp.add(agent) + + // Connect the agent and verify initial status callbacks. + runAsShell(MANAGE_TEST_NETWORKS) { agent.register() } + agent.markConnected() + agent.expectCallback<OnNetworkCreated>() + agent.expectSignalStrengths(intArrayOf()) + agent.expectValidationBypassedStatus() + val network = agent.network ?: fail("Expected a non-null network") + return agent to callback + } + + fun ByteArray.toHex(): String = joinToString(separator = "") { + eachByte -> "%02x".format(eachByte) + } + + fun checkDscpValue( + agent : TestableNetworkAgent, + callback : TestableNetworkCallback, + dscpValue : Int = 0, + dstPort : Int = 0, + ) { + val testString = "test string" + val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8)) + var packetFound = false + + val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP) + agent.network.bindSocket(socket) + + val originalPacket = testPacket.readAsArray() + Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, + 0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort) + + Os.close(socket) + generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet -> + val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN) + val ip_ver = buffer.get() + val tos = buffer.get() + val length = buffer.getShort() + val id = buffer.getShort() + val offset = buffer.getShort() + val ttl = buffer.get() + val ipType = buffer.get() + val checksum = buffer.getShort() + + val ipAddr = ByteArray(4) + buffer.get(ipAddr) + val srcIp = Inet4Address.getByAddress(ipAddr); + buffer.get(ipAddr) + val dstIp = Inet4Address.getByAddress(ipAddr); + val packetSrcPort = buffer.getShort().toInt() + val packetDstPort = buffer.getShort().toInt() + + // TODO: Add source port comparison. + if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR && + packetDstPort == dstPort) { + assertEquals(dscpValue, (tos.toInt().shr(2))) + packetFound = true + } + } + assertTrue(packetFound) + } + + fun doRemovePolicyTest( + agent : TestableNetworkAgent, + callback : TestableNetworkCallback, + policyId : Int + ) { + val portNumber = 1111 * policyId + agent.sendRemoveDscpPolicy(policyId) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(policyId, it.policyId) + assertEquals(STATUS_DELETED, it.status) + checkDscpValue(agent, callback, dstPort = portNumber) + } + } + + @Test + fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1) + .setDestinationPortRange(Range(4444, 4444)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + } + + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444) + + agent.sendRemoveDscpPolicy(1) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_DELETED, it.status) + } + + val policy2 = DscpPolicy.Builder(1, 4) + .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS) + .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + } + + checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555) + + agent.sendRemoveDscpPolicy(1) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_DELETED, it.status) + } + } + + @Test + // Remove policies in the same order as addition. + fun testRemoveDscpPolicy_RemoveSameOrderAsAdd(): Unit = createConnectedNetworkAgent().let { + (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111) + } + + val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(2, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222) + } + + val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build() + agent.sendAddDscpPolicy(policy3) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(3, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333) + } + + /* Remove Policies and check CE is no longer set */ + doRemovePolicyTest(agent, callback, 1) + doRemovePolicyTest(agent, callback, 2) + doRemovePolicyTest(agent, callback, 3) + } + + @Test + fun testRemoveDscpPolicy_RemoveImmediatelyAfterAdd(): Unit = + createConnectedNetworkAgent().let{ (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111) + } + doRemovePolicyTest(agent, callback, 1) + + val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(2, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222) + } + doRemovePolicyTest(agent, callback, 2) + + val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build() + agent.sendAddDscpPolicy(policy3) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(3, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333) + } + doRemovePolicyTest(agent, callback, 3) + } + + @Test + // Remove policies in reverse order from addition. + fun testRemoveDscpPolicy_RemoveReverseOrder(): Unit = + createConnectedNetworkAgent().let { (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111) + } + + val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(2, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222) + } + + val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build() + agent.sendAddDscpPolicy(policy3) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(3, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333) + } + + /* Remove Policies and check CE is no longer set */ + doRemovePolicyTest(agent, callback, 3) + doRemovePolicyTest(agent, callback, 2) + doRemovePolicyTest(agent, callback, 1) + } + + @Test + fun testRemoveDscpPolicy_InvalidPolicy(): Unit = createConnectedNetworkAgent().let { + (agent, callback) -> + agent.sendRemoveDscpPolicy(3) + // Is there something to add in TestableNetworkCallback to NOT expect a callback? + // Or should we send STATUS_DELETED in any case or a different STATUS? + } + + @Test + fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1) + .setDestinationPortRange(Range(1111, 1111)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111) + } + + val policy2 = DscpPolicy.Builder(2, 1) + .setDestinationPortRange(Range(2222, 2222)).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(2, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222) + } + + val policy3 = DscpPolicy.Builder(3, 1) + .setDestinationPortRange(Range(3333, 3333)).build() + agent.sendAddDscpPolicy(policy3) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(3, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333) + } + + agent.sendRemoveAllDscpPolicies() + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_DELETED, it.status) + checkDscpValue(agent, callback, dstPort = 1111) + } + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(2, it.policyId) + assertEquals(STATUS_DELETED, it.status) + checkDscpValue(agent, callback, dstPort = 2222) + } + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(3, it.policyId) + assertEquals(STATUS_DELETED, it.status) + checkDscpValue(agent, callback, dstPort = 3333) + } + } + + @Test + fun testAddDuplicateDscpPolicy(): Unit = createConnectedNetworkAgent().let { + (agent, callback) -> + val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build() + agent.sendAddDscpPolicy(policy) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444) + } + + // TODO: send packet on socket and confirm that changing the DSCP policy + // updates the mark to the new value. + + val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build() + agent.sendAddDscpPolicy(policy2) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_SUCCESS, it.status) + + // Sending packet with old policy should fail + checkDscpValue(agent, callback, dstPort = 4444) + checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555) + } + + agent.sendRemoveDscpPolicy(1) + agent.expectCallback<OnDscpPolicyStatusUpdated>().let { + assertEquals(1, it.policyId) + assertEquals(STATUS_DELETED, it.status) + } + } + + @Test + fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let { + (agent, callback) -> + // Check that policy with partial parameters is lossless. + val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build() + assertParcelingIsLossless(policy); + + // Check that policy with all parameters is lossless. + val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)) + .setSourceAddress(LOCAL_IPV4_ADDRESS) + .setDestinationAddress(TEST_TARGET_IPV4_ADDR) + .setProtocol(IPPROTO_UDP).build() + assertParcelingIsLossless(policy2); + } +} + +private fun ByteBuffer.readAsArray(): ByteArray { + val out = ByteArray(remaining()) + get(out) + return out +} + +private fun <T> Context.assertHasService(manager: Class<T>): T { + return getSystemService(manager) ?: fail("Service $manager not found") +}
diff --git a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java index 56ab2a7..d221694 100644 --- a/tests/cts/net/src/android/net/cts/IpConfigurationTest.java +++ b/tests/cts/net/src/android/net/cts/IpConfigurationTest.java
@@ -16,7 +16,7 @@ package android.net.cts; -import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -25,12 +25,16 @@ import android.net.LinkAddress; import android.net.ProxyInfo; import android.net.StaticIpConfiguration; +import android.os.Build; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.DevSdkIgnoreRule; + import libcore.net.InetAddressUtils; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +54,9 @@ private StaticIpConfiguration mStaticIpConfig; private ProxyInfo mProxy; + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + @Before public void setUp() { dnsServers.add(DNS1); @@ -99,6 +106,18 @@ assertIpConfigurationEqual(ipConfig, new IpConfiguration(ipConfig)); } + @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testBuilder() { + final IpConfiguration c = new IpConfiguration.Builder() + .setStaticIpConfiguration(mStaticIpConfig) + .setHttpProxy(mProxy) + .build(); + + assertEquals(mStaticIpConfig, c.getStaticIpConfiguration()); + assertEquals(mProxy, c.getHttpProxy()); + } + private void checkEmpty(IpConfiguration config) { assertEquals(IpConfiguration.IpAssignment.UNASSIGNED, config.getIpAssignment().UNASSIGNED); @@ -118,6 +137,6 @@ @Test public void testParcel() { final IpConfiguration config = new IpConfiguration(); - assertParcelSane(config, 4); + assertParcelingIsLossless(config); } }
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java deleted file mode 100644 index 6e61705..0000000 --- a/tests/cts/net/src/android/net/cts/LocalSocketTest.java +++ /dev/null
@@ -1,470 +0,0 @@ -/* - * Copyright (C) 2008 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 android.net.cts; - -import junit.framework.TestCase; - -import android.net.Credentials; -import android.net.LocalServerSocket; -import android.net.LocalSocket; -import android.net.LocalSocketAddress; -import android.system.Os; -import android.system.OsConstants; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -public class LocalSocketTest extends TestCase { - private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest"; - - public void testLocalConnections() throws IOException { - String address = ADDRESS_PREFIX + "_testLocalConnections"; - // create client and server socket - LocalServerSocket localServerSocket = new LocalServerSocket(address); - LocalSocket clientSocket = new LocalSocket(); - - // establish connection between client and server - LocalSocketAddress locSockAddr = new LocalSocketAddress(address); - assertFalse(clientSocket.isConnected()); - clientSocket.connect(locSockAddr); - assertTrue(clientSocket.isConnected()); - - LocalSocket serverSocket = localServerSocket.accept(); - assertTrue(serverSocket.isConnected()); - assertTrue(serverSocket.isBound()); - try { - serverSocket.bind(localServerSocket.getLocalSocketAddress()); - fail("Cannot bind a LocalSocket from accept()"); - } catch (IOException expected) { - } - try { - serverSocket.connect(locSockAddr); - fail("Cannot connect a LocalSocket from accept()"); - } catch (IOException expected) { - } - - Credentials credent = clientSocket.getPeerCredentials(); - assertTrue(0 != credent.getPid()); - - // send data from client to server - OutputStream clientOutStream = clientSocket.getOutputStream(); - clientOutStream.write(12); - InputStream serverInStream = serverSocket.getInputStream(); - assertEquals(12, serverInStream.read()); - - //send data from server to client - OutputStream serverOutStream = serverSocket.getOutputStream(); - serverOutStream.write(3); - InputStream clientInStream = clientSocket.getInputStream(); - assertEquals(3, clientInStream.read()); - - // Test sending and receiving file descriptors - clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in}); - clientOutStream.write(32); - assertEquals(32, serverInStream.read()); - - FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors(); - assertEquals(1, out.length); - FileDescriptor fd = clientSocket.getFileDescriptor(); - assertTrue(fd.valid()); - - //shutdown input stream of client - clientSocket.shutdownInput(); - assertEquals(-1, clientInStream.read()); - - //shutdown output stream of client - clientSocket.shutdownOutput(); - try { - clientOutStream.write(10); - fail("testLocalSocket shouldn't come to here"); - } catch (IOException e) { - // expected - } - - //shutdown input stream of server - serverSocket.shutdownInput(); - assertEquals(-1, serverInStream.read()); - - //shutdown output stream of server - serverSocket.shutdownOutput(); - try { - serverOutStream.write(10); - fail("testLocalSocket shouldn't come to here"); - } catch (IOException e) { - // expected - } - - //close client socket - clientSocket.close(); - try { - clientInStream.read(); - fail("testLocalSocket shouldn't come to here"); - } catch (IOException e) { - // expected - } - - //close server socket - serverSocket.close(); - try { - serverInStream.read(); - fail("testLocalSocket shouldn't come to here"); - } catch (IOException e) { - // expected - } - } - - public void testAccessors() throws IOException { - String address = ADDRESS_PREFIX + "_testAccessors"; - LocalSocket socket = new LocalSocket(); - LocalSocketAddress addr = new LocalSocketAddress(address); - - assertFalse(socket.isBound()); - socket.bind(addr); - assertTrue(socket.isBound()); - assertEquals(addr, socket.getLocalSocketAddress()); - - String str = socket.toString(); - assertTrue(str.contains("impl:android.net.LocalSocketImpl")); - - socket.setReceiveBufferSize(1999); - assertEquals(1999 << 1, socket.getReceiveBufferSize()); - - socket.setSendBufferSize(3998); - assertEquals(3998 << 1, socket.getSendBufferSize()); - - assertEquals(0, socket.getSoTimeout()); - socket.setSoTimeout(1996); - assertTrue(socket.getSoTimeout() > 0); - - try { - socket.getRemoteSocketAddress(); - fail("testLocalSocketSecondary shouldn't come to here"); - } catch (UnsupportedOperationException e) { - // expected - } - - try { - socket.isClosed(); - fail("testLocalSocketSecondary shouldn't come to here"); - } catch (UnsupportedOperationException e) { - // expected - } - - try { - socket.isInputShutdown(); - fail("testLocalSocketSecondary shouldn't come to here"); - } catch (UnsupportedOperationException e) { - // expected - } - - try { - socket.isOutputShutdown(); - fail("testLocalSocketSecondary shouldn't come to here"); - } catch (UnsupportedOperationException e) { - // expected - } - - try { - socket.connect(addr, 2005); - fail("testLocalSocketSecondary shouldn't come to here"); - } catch (UnsupportedOperationException e) { - // expected - } - - socket.close(); - } - - // http://b/31205169 - public void testSetSoTimeout_readTimeout() throws Exception { - String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout"; - - try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { - final LocalSocket clientSocket = socketPair.clientSocket; - - // Set the timeout in millis. - int timeoutMillis = 1000; - clientSocket.setSoTimeout(timeoutMillis); - - // Avoid blocking the test run if timeout doesn't happen by using a separate thread. - Callable<Result> reader = () -> { - try { - clientSocket.getInputStream().read(); - return Result.noException("Did not block"); - } catch (IOException e) { - return Result.exception(e); - } - }; - // Allow the configured timeout, plus some slop. - int allowedTime = timeoutMillis + 2000; - Result result = runInSeparateThread(allowedTime, reader); - - // Check the message was a timeout, it's all we have to go on. - String expectedMessage = Os.strerror(OsConstants.EAGAIN); - result.assertThrewIOException(expectedMessage); - } - } - - // http://b/31205169 - public void testSetSoTimeout_writeTimeout() throws Exception { - String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout"; - - try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { - final LocalSocket clientSocket = socketPair.clientSocket; - - // Set the timeout in millis. - int timeoutMillis = 1000; - clientSocket.setSoTimeout(timeoutMillis); - - // Set a small buffer size so we know we can flood it. - clientSocket.setSendBufferSize(100); - final int bufferSize = clientSocket.getSendBufferSize(); - - // Avoid blocking the test run if timeout doesn't happen by using a separate thread. - Callable<Result> writer = () -> { - try { - byte[] toWrite = new byte[bufferSize * 2]; - clientSocket.getOutputStream().write(toWrite); - return Result.noException("Did not block"); - } catch (IOException e) { - return Result.exception(e); - } - }; - // Allow the configured timeout, plus some slop. - int allowedTime = timeoutMillis + 2000; - - Result result = runInSeparateThread(allowedTime, writer); - - // Check the message was a timeout, it's all we have to go on. - String expectedMessage = Os.strerror(OsConstants.EAGAIN); - result.assertThrewIOException(expectedMessage); - } - } - - public void testAvailable() throws Exception { - String address = ADDRESS_PREFIX + "_testAvailable"; - - try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { - LocalSocket clientSocket = socketPair.clientSocket; - LocalSocket serverSocket = socketPair.serverSocket.accept(); - - OutputStream clientOutputStream = clientSocket.getOutputStream(); - InputStream serverInputStream = serverSocket.getInputStream(); - assertEquals(0, serverInputStream.available()); - - byte[] buffer = new byte[50]; - clientOutputStream.write(buffer); - assertEquals(50, serverInputStream.available()); - - InputStream clientInputStream = clientSocket.getInputStream(); - OutputStream serverOutputStream = serverSocket.getOutputStream(); - assertEquals(0, clientInputStream.available()); - serverOutputStream.write(buffer); - assertEquals(50, serverInputStream.available()); - - serverSocket.close(); - } - } - - // http://b/34095140 - public void testLocalSocketCreatedFromFileDescriptor() throws Exception { - String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor"; - - // Establish connection between a local client and server to get a valid client socket file - // descriptor. - try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { - // Extract the client FileDescriptor we can use. - FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor(); - assertTrue(fileDescriptor.valid()); - - // Create the LocalSocket we want to test. - LocalSocket clientSocketCreatedFromFileDescriptor = - LocalSocket.createConnectedLocalSocket(fileDescriptor); - assertTrue(clientSocketCreatedFromFileDescriptor.isConnected()); - assertTrue(clientSocketCreatedFromFileDescriptor.isBound()); - - // Test the LocalSocket can be used for communication. - LocalSocket serverSocket = socketPair.serverSocket.accept(); - OutputStream clientOutputStream = - clientSocketCreatedFromFileDescriptor.getOutputStream(); - InputStream serverInputStream = serverSocket.getInputStream(); - - clientOutputStream.write(12); - assertEquals(12, serverInputStream.read()); - - // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor. - clientSocketCreatedFromFileDescriptor.close(); - assertTrue(fileDescriptor.valid()); - - // .. while closing the LocalSocket that owned the file descriptor does. - socketPair.clientSocket.close(); - assertFalse(fileDescriptor.valid()); - } - } - - public void testFlush() throws Exception { - String address = ADDRESS_PREFIX + "_testFlush"; - - try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) { - LocalSocket clientSocket = socketPair.clientSocket; - LocalSocket serverSocket = socketPair.serverSocket.accept(); - - OutputStream clientOutputStream = clientSocket.getOutputStream(); - InputStream serverInputStream = serverSocket.getInputStream(); - testFlushWorks(clientOutputStream, serverInputStream); - - OutputStream serverOutputStream = serverSocket.getOutputStream(); - InputStream clientInputStream = clientSocket.getInputStream(); - testFlushWorks(serverOutputStream, clientInputStream); - - serverSocket.close(); - } - } - - private void testFlushWorks(OutputStream outputStream, InputStream inputStream) - throws Exception { - final int bytesToTransfer = 50; - StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer); - - byte[] buffer = new byte[bytesToTransfer]; - outputStream.write(buffer); - assertEquals(bytesToTransfer, inputStream.available()); - - // Start consuming the data. - inputStreamReader.start(); - - // This doesn't actually flush any buffers, it just polls until the reader has read all the - // bytes. - outputStream.flush(); - - inputStreamReader.waitForCompletion(5000); - inputStreamReader.assertBytesRead(bytesToTransfer); - assertEquals(0, inputStream.available()); - } - - private static class StreamReader extends Thread { - private final InputStream is; - private final int expectedByteCount; - private final CountDownLatch completeLatch = new CountDownLatch(1); - - private volatile Exception exception; - private int bytesRead; - - private StreamReader(InputStream is, int expectedByteCount) { - this.is = is; - this.expectedByteCount = expectedByteCount; - } - - @Override - public void run() { - try { - byte[] buffer = new byte[10]; - int readCount; - while ((readCount = is.read(buffer)) >= 0) { - bytesRead += readCount; - if (bytesRead >= expectedByteCount) { - break; - } - } - } catch (IOException e) { - exception = e; - } finally { - completeLatch.countDown(); - } - } - - public void waitForCompletion(long waitMillis) throws Exception { - if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) { - fail("Timeout waiting for completion"); - } - if (exception != null) { - throw new Exception("Read failed", exception); - } - } - - public void assertBytesRead(int expected) { - assertEquals(expected, bytesRead); - } - } - - private static class Result { - private final String type; - private final Exception e; - - private Result(String type, Exception e) { - this.type = type; - this.e = e; - } - - static Result noException(String description) { - return new Result(description, null); - } - - static Result exception(Exception e) { - return new Result(e.getClass().getName(), e); - } - - void assertThrewIOException(String expectedMessage) { - assertEquals("Unexpected result type", IOException.class.getName(), type); - assertEquals("Unexpected exception message", expectedMessage, e.getMessage()); - } - } - - private static Result runInSeparateThread(int allowedTime, final Callable<Result> callable) - throws Exception { - ExecutorService service = Executors.newSingleThreadScheduledExecutor(); - Future<Result> future = service.submit(callable); - Result result = future.get(allowedTime, TimeUnit.MILLISECONDS); - if (!future.isDone()) { - fail("Worker thread appears blocked"); - } - return result; - } - - private static class LocalSocketPair implements AutoCloseable { - static LocalSocketPair createConnectedSocketPair(String address) throws Exception { - LocalServerSocket localServerSocket = new LocalServerSocket(address); - final LocalSocket clientSocket = new LocalSocket(); - - // Establish connection between client and server - LocalSocketAddress locSockAddr = new LocalSocketAddress(address); - clientSocket.connect(locSockAddr); - assertTrue(clientSocket.isConnected()); - return new LocalSocketPair(localServerSocket, clientSocket); - } - - final LocalServerSocket serverSocket; - final LocalSocket clientSocket; - - LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) { - this.serverSocket = serverSocket; - this.clientSocket = clientSocket; - } - - public void close() throws Exception { - serverSocket.close(); - clientSocket.close(); - } - } -}
diff --git a/tests/cts/net/src/android/net/cts/MacAddressTest.java b/tests/cts/net/src/android/net/cts/MacAddressTest.java index 3fd3bba..e47155b 100644 --- a/tests/cts/net/src/android/net/cts/MacAddressTest.java +++ b/tests/cts/net/src/android/net/cts/MacAddressTest.java
@@ -20,7 +20,7 @@ import static android.net.MacAddress.TYPE_MULTICAST; import static android.net.MacAddress.TYPE_UNICAST; -import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; @@ -218,6 +218,6 @@ public void testParcelMacAddress() { final MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f"); - assertParcelSane(mac, 1); + assertParcelingIsLossless(mac); } }
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt index ef5dc77..344482b 100644 --- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -23,7 +23,6 @@ import android.net.INetworkAgentRegistry import android.net.InetAddresses import android.net.IpPrefix -import android.net.KeepalivePacketData import android.net.LinkAddress import android.net.LinkProperties import android.net.NattKeepalivePacketData @@ -42,6 +41,7 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED import android.net.NetworkCapabilities.TRANSPORT_TEST import android.net.NetworkCapabilities.TRANSPORT_VPN import android.net.NetworkInfo @@ -53,7 +53,6 @@ import android.net.QosCallback import android.net.QosCallbackException import android.net.QosCallback.QosCallbackRegistrationException -import android.net.QosFilter import android.net.QosSession import android.net.QosSessionAttributes import android.net.QosSocketInfo @@ -67,7 +66,6 @@ import android.os.Build import android.os.Handler import android.os.HandlerThread -import android.os.Looper import android.os.Message import android.os.SystemClock import android.telephony.TelephonyManager @@ -82,6 +80,8 @@ import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo import com.android.testutils.DevSdkIgnoreRunner import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus +import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged import com.android.testutils.RecorderCallback.CallbackEntry.Losing import com.android.testutils.RecorderCallback.CallbackEntry.Lost import com.android.testutils.TestableNetworkAgent @@ -100,7 +100,6 @@ import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus import com.android.testutils.TestableNetworkCallback import org.junit.After -import org.junit.Assert.assertArrayEquals import org.junit.Assume.assumeFalse import org.junit.Before import org.junit.Test @@ -462,6 +461,36 @@ } } + private fun ncWithAccessUids(vararg uids: Int) = NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_TEST) + .setAccessUids(uids.toSet()).build() + + @Test + fun testRejectedUpdates() { + val callback = TestableNetworkCallback() + // will be cleaned up in tearDown + registerNetworkCallback(makeTestNetworkRequest(), callback) + val agent = createNetworkAgent(initialNc = ncWithAccessUids(200)) + agent.register() + agent.markConnected() + + // Make sure the UIDs have been ignored. + callback.expectCallback<Available>(agent.network!!) + callback.expectCapabilitiesThat(agent.network!!) { + it.accessUids.isEmpty() && !it.hasCapability(NET_CAPABILITY_VALIDATED) + } + callback.expectCallback<LinkPropertiesChanged>(agent.network!!) + callback.expectCallback<BlockedStatus>(agent.network!!) + callback.expectCapabilitiesThat(agent.network!!) { + it.accessUids.isEmpty() && it.hasCapability(NET_CAPABILITY_VALIDATED) + } + callback.assertNoCallback(NO_CALLBACK_TIMEOUT) + + // Make sure that the UIDs are also ignored upon update + agent.sendNetworkCapabilities(ncWithAccessUids(200, 300)) + callback.assertNoCallback(NO_CALLBACK_TIMEOUT) + } + @Test fun testSendScore() { // This test will create two networks and check that the one with the stronger
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java new file mode 100644 index 0000000..147fca9 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -0,0 +1,878 @@ +/* + * Copyright (C) 2015 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 android.net.cts; + +import static android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL; +import static android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_NO; +import static android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_YES; +import static android.app.usage.NetworkStats.Bucket.METERED_ALL; +import static android.app.usage.NetworkStats.Bucket.METERED_NO; +import static android.app.usage.NetworkStats.Bucket.METERED_YES; +import static android.app.usage.NetworkStats.Bucket.STATE_ALL; +import static android.app.usage.NetworkStats.Bucket.STATE_DEFAULT; +import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND; +import static android.app.usage.NetworkStats.Bucket.TAG_NONE; +import static android.app.usage.NetworkStats.Bucket.UID_ALL; + +import android.app.AppOpsManager; +import android.app.usage.NetworkStats; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.net.TrafficStats; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.platform.test.annotations.AppModeFull; +import android.telephony.TelephonyManager; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.compatibility.common.util.ShellIdentityUtils; +import com.android.compatibility.common.util.SystemUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.UnknownHostException; +import java.text.MessageFormat; +import java.util.ArrayList; + +public class NetworkStatsManagerTest extends InstrumentationTestCase { + private static final String LOG_TAG = "NetworkStatsManagerTest"; + private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}"; + private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}"; + + private static final long MINUTE = 1000 * 60; + private static final int TIMEOUT_MILLIS = 15000; + + private static final String CHECK_CONNECTIVITY_URL = "http://www.265.com/"; + private static final int HOST_RESOLUTION_RETRIES = 4; + private static final int HOST_RESOLUTION_INTERVAL_MS = 500; + + private static final int NETWORK_TAG = 0xf00d; + private static final long THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB + + private abstract class NetworkInterfaceToTest { + private boolean mMetered; + private boolean mIsDefault; + + abstract int getNetworkType(); + abstract int getTransportType(); + + public boolean getMetered() { + return mMetered; + } + + public void setMetered(boolean metered) { + this.mMetered = metered; + } + + public boolean getIsDefault() { + return mIsDefault; + } + + public void setIsDefault(boolean isDefault) { + mIsDefault = isDefault; + } + + abstract String getSystemFeature(); + abstract String getErrorMessage(); + } + + private final NetworkInterfaceToTest[] mNetworkInterfacesToTest = + new NetworkInterfaceToTest[] { + new NetworkInterfaceToTest() { + @Override + public int getNetworkType() { + return ConnectivityManager.TYPE_WIFI; + } + + @Override + public int getTransportType() { + return NetworkCapabilities.TRANSPORT_WIFI; + } + + @Override + public String getSystemFeature() { + return PackageManager.FEATURE_WIFI; + } + + @Override + public String getErrorMessage() { + return " Please make sure you are connected to a WiFi access point."; + } + }, + new NetworkInterfaceToTest() { + @Override + public int getNetworkType() { + return ConnectivityManager.TYPE_MOBILE; + } + + @Override + public int getTransportType() { + return NetworkCapabilities.TRANSPORT_CELLULAR; + } + + @Override + public String getSystemFeature() { + return PackageManager.FEATURE_TELEPHONY; + } + + @Override + public String getErrorMessage() { + return " Please make sure you have added a SIM card with data plan to" + + " your phone, have enabled data over cellular and in case of" + + " dual SIM devices, have selected the right SIM " + + "for data connection."; + } + } + }; + + private String mPkg; + private NetworkStatsManager mNsm; + private ConnectivityManager mCm; + private PackageManager mPm; + private long mStartTime; + private long mEndTime; + + private long mBytesRead; + private String mWriteSettingsMode; + private String mUsageStatsMode; + + private void exerciseRemoteHost(Network network, URL url) throws Exception { + NetworkInfo networkInfo = mCm.getNetworkInfo(network); + if (networkInfo == null) { + Log.w(LOG_TAG, "Network info is null"); + } else { + Log.w(LOG_TAG, "Network: " + networkInfo.toString()); + } + InputStreamReader in = null; + HttpURLConnection urlc = null; + String originalKeepAlive = System.getProperty("http.keepAlive"); + System.setProperty("http.keepAlive", "false"); + try { + TrafficStats.setThreadStatsTag(NETWORK_TAG); + urlc = (HttpURLConnection) network.openConnection(url); + urlc.setConnectTimeout(TIMEOUT_MILLIS); + urlc.setUseCaches(false); + // Disable compression so we generate enough traffic that assertWithinPercentage will + // not be affected by the small amount of traffic (5-10kB) sent by the test harness. + urlc.setRequestProperty("Accept-Encoding", "identity"); + urlc.connect(); + boolean ping = urlc.getResponseCode() == 200; + if (ping) { + in = new InputStreamReader( + (InputStream) urlc.getContent()); + + mBytesRead = 0; + while (in.read() != -1) ++mBytesRead; + } + } catch (Exception e) { + Log.i(LOG_TAG, "Badness during exercising remote server: " + e); + } finally { + TrafficStats.clearThreadStatsTag(); + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // don't care + } + } + if (urlc != null) { + urlc.disconnect(); + } + if (originalKeepAlive == null) { + System.clearProperty("http.keepAlive"); + } else { + System.setProperty("http.keepAlive", originalKeepAlive); + } + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mNsm = (NetworkStatsManager) getInstrumentation().getContext() + .getSystemService(Context.NETWORK_STATS_SERVICE); + mNsm.setPollForce(true); + + mCm = (ConnectivityManager) getInstrumentation().getContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + + mPm = getInstrumentation().getContext().getPackageManager(); + + mPkg = getInstrumentation().getContext().getPackageName(); + + mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS); + setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow"); + mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS); + } + + @Override + protected void tearDown() throws Exception { + if (mWriteSettingsMode != null) { + setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode); + } + if (mUsageStatsMode != null) { + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode); + } + super.tearDown(); + } + + private void setAppOpsMode(String appop, String mode) throws Exception { + final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode); + SystemUtil.runShellCommand(command); + } + + private String getAppOpsMode(String appop) throws Exception { + final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop); + String result = SystemUtil.runShellCommand(command); + if (result == null) { + Log.w(LOG_TAG, "App op " + appop + " could not be read."); + } + return result; + } + + private boolean isInForeground() throws IOException { + String result = SystemUtil.runShellCommand(getInstrumentation(), + "cmd activity get-uid-state " + Process.myUid()); + return result.contains("FOREGROUND"); + } + + private class NetworkCallback extends ConnectivityManager.NetworkCallback { + private long mTolerance; + private URL mUrl; + public boolean success; + public boolean metered; + public boolean isDefault; + + NetworkCallback(long tolerance, URL url) { + mTolerance = tolerance; + mUrl = url; + success = false; + metered = false; + isDefault = false; + } + + // The test host only has IPv4. So on a dual-stack network where IPv6 connects before IPv4, + // we need to wait until IPv4 is available or the test will spuriously fail. + private void waitForHostResolution(Network network) { + for (int i = 0; i < HOST_RESOLUTION_RETRIES; i++) { + try { + network.getAllByName(mUrl.getHost()); + return; + } catch (UnknownHostException e) { + SystemClock.sleep(HOST_RESOLUTION_INTERVAL_MS); + } + } + fail(String.format("%s could not be resolved on network %s (%d attempts %dms apart)", + mUrl.getHost(), network, HOST_RESOLUTION_RETRIES, HOST_RESOLUTION_INTERVAL_MS)); + } + + @Override + public void onAvailable(Network network) { + try { + mStartTime = System.currentTimeMillis() - mTolerance; + isDefault = network.equals(mCm.getActiveNetwork()); + waitForHostResolution(network); + exerciseRemoteHost(network, mUrl); + mEndTime = System.currentTimeMillis() + mTolerance; + success = true; + metered = !mCm.getNetworkCapabilities(network) + .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + synchronized (NetworkStatsManagerTest.this) { + NetworkStatsManagerTest.this.notify(); + } + } catch (Exception e) { + Log.w(LOG_TAG, "exercising remote host failed.", e); + success = false; + } + } + } + + private boolean shouldTestThisNetworkType(int networkTypeIndex, final long tolerance) + throws Exception { + boolean hasFeature = mPm.hasSystemFeature( + mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature()); + if (!hasFeature) { + return false; + } + NetworkCallback callback = new NetworkCallback(tolerance, new URL(CHECK_CONNECTIVITY_URL)); + mCm.requestNetwork(new NetworkRequest.Builder() + .addTransportType(mNetworkInterfacesToTest[networkTypeIndex].getTransportType()) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(), callback); + synchronized (this) { + try { + wait((int) (TIMEOUT_MILLIS * 1.2)); + } catch (InterruptedException e) { + } + } + if (callback.success) { + mNetworkInterfacesToTest[networkTypeIndex].setMetered(callback.metered); + mNetworkInterfacesToTest[networkTypeIndex].setIsDefault(callback.isDefault); + return true; + } + + // This will always fail at this point as we know 'hasFeature' is true. + assertFalse(mNetworkInterfacesToTest[networkTypeIndex].getSystemFeature() + + " is a reported system feature, " + + "however no corresponding connected network interface was found or the attempt " + + "to connect has timed out (timeout = " + TIMEOUT_MILLIS + "ms)." + + mNetworkInterfacesToTest[networkTypeIndex].getErrorMessage(), hasFeature); + return false; + } + + private String getSubscriberId(int networkIndex) { + int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType(); + if (ConnectivityManager.TYPE_MOBILE == networkType) { + TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext() + .getSystemService(Context.TELEPHONY_SERVICE); + return ShellIdentityUtils.invokeMethodWithShellPermissions(tm, + (telephonyManager) -> telephonyManager.getSubscriberId()); + } + return ""; + } + + @AppModeFull + public void testDeviceSummary() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + if (!shouldTestThisNetworkType(i, MINUTE / 2)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats.Bucket bucket = null; + try { + bucket = mNsm.querySummaryForDevice( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + } catch (RemoteException | SecurityException e) { + fail("testDeviceSummary fails with exception: " + e.toString()); + } + assertNotNull(bucket); + assertTimestamps(bucket); + assertEquals(bucket.getState(), STATE_ALL); + assertEquals(bucket.getUid(), UID_ALL); + assertEquals(bucket.getMetered(), METERED_ALL); + assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL); + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + bucket = mNsm.querySummaryForDevice( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + fail("negative testDeviceSummary fails: no exception thrown."); + } catch (RemoteException e) { + fail("testDeviceSummary fails with exception: " + e.toString()); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testUserSummary() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + if (!shouldTestThisNetworkType(i, MINUTE / 2)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats.Bucket bucket = null; + try { + bucket = mNsm.querySummaryForUser( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + } catch (RemoteException | SecurityException e) { + fail("testUserSummary fails with exception: " + e.toString()); + } + assertNotNull(bucket); + assertTimestamps(bucket); + assertEquals(bucket.getState(), STATE_ALL); + assertEquals(bucket.getUid(), UID_ALL); + assertEquals(bucket.getMetered(), METERED_ALL); + assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL); + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + bucket = mNsm.querySummaryForUser( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + fail("negative testUserSummary fails: no exception thrown."); + } catch (RemoteException e) { + fail("testUserSummary fails with exception: " + e.toString()); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testAppSummary() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Use tolerance value that large enough to make sure stats of at + // least one bucket is included. However, this is possible that + // the test will see data of different app but with the same UID + // that created before testing. + // TODO: Consider query stats before testing and use the difference to verify. + if (!shouldTestThisNetworkType(i, MINUTE * 120)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats result = null; + try { + result = mNsm.querySummary( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + assertNotNull(result); + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + long totalTxPackets = 0; + long totalRxPackets = 0; + long totalTxBytes = 0; + long totalRxBytes = 0; + boolean hasCorrectMetering = false; + boolean hasCorrectDefaultStatus = false; + int expectedMetering = mNetworkInterfacesToTest[i].getMetered() + ? METERED_YES : METERED_NO; + int expectedDefaultStatus = mNetworkInterfacesToTest[i].getIsDefault() + ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO; + while (result.hasNextBucket()) { + assertTrue(result.getNextBucket(bucket)); + assertTimestamps(bucket); + hasCorrectMetering |= bucket.getMetered() == expectedMetering; + if (bucket.getUid() == Process.myUid()) { + totalTxPackets += bucket.getTxPackets(); + totalRxPackets += bucket.getRxPackets(); + totalTxBytes += bucket.getTxBytes(); + totalRxBytes += bucket.getRxBytes(); + hasCorrectDefaultStatus |= + bucket.getDefaultNetworkStatus() == expectedDefaultStatus; + } + } + assertFalse(result.getNextBucket(bucket)); + assertTrue("Incorrect metering for NetworkType: " + + mNetworkInterfacesToTest[i].getNetworkType(), hasCorrectMetering); + assertTrue("Incorrect isDefault for NetworkType: " + + mNetworkInterfacesToTest[i].getNetworkType(), hasCorrectDefaultStatus); + assertTrue("No Rx bytes usage for uid " + Process.myUid(), totalRxBytes > 0); + assertTrue("No Rx packets usage for uid " + Process.myUid(), totalRxPackets > 0); + assertTrue("No Tx bytes usage for uid " + Process.myUid(), totalTxBytes > 0); + assertTrue("No Tx packets usage for uid " + Process.myUid(), totalTxPackets > 0); + } finally { + if (result != null) { + result.close(); + } + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + result = mNsm.querySummary( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + fail("negative testAppSummary fails: no exception thrown."); + } catch (RemoteException e) { + fail("testAppSummary fails with exception: " + e.toString()); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testAppDetails() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Relatively large tolerance to accommodate for history bucket size. + if (!shouldTestThisNetworkType(i, MINUTE * 120)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats result = null; + try { + result = mNsm.queryDetails( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + long totalBytesWithSubscriberId = getTotalAndAssertNotEmpty(result); + + // Test without filtering by subscriberId + result = mNsm.queryDetails( + mNetworkInterfacesToTest[i].getNetworkType(), null, + mStartTime, mEndTime); + + assertTrue("More bytes with subscriberId filter than without.", + getTotalAndAssertNotEmpty(result) >= totalBytesWithSubscriberId); + } catch (RemoteException | SecurityException e) { + fail("testAppDetails fails with exception: " + e.toString()); + } finally { + if (result != null) { + result.close(); + } + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + result = mNsm.queryDetails( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime); + fail("negative testAppDetails fails: no exception thrown."); + } catch (RemoteException e) { + fail("testAppDetails fails with exception: " + e.toString()); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testUidDetails() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Relatively large tolerance to accommodate for history bucket size. + if (!shouldTestThisNetworkType(i, MINUTE * 120)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats result = null; + try { + result = mNsm.queryDetailsForUid( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid()); + assertNotNull(result); + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + long totalTxPackets = 0; + long totalRxPackets = 0; + long totalTxBytes = 0; + long totalRxBytes = 0; + while (result.hasNextBucket()) { + assertTrue(result.getNextBucket(bucket)); + assertTimestamps(bucket); + assertEquals(bucket.getState(), STATE_ALL); + assertEquals(bucket.getMetered(), METERED_ALL); + assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL); + assertEquals(bucket.getUid(), Process.myUid()); + totalTxPackets += bucket.getTxPackets(); + totalRxPackets += bucket.getRxPackets(); + totalTxBytes += bucket.getTxBytes(); + totalRxBytes += bucket.getRxBytes(); + } + assertFalse(result.getNextBucket(bucket)); + assertTrue("No Rx bytes usage for uid " + Process.myUid(), totalRxBytes > 0); + assertTrue("No Rx packets usage for uid " + Process.myUid(), totalRxPackets > 0); + assertTrue("No Tx bytes usage for uid " + Process.myUid(), totalTxBytes > 0); + assertTrue("No Tx packets usage for uid " + Process.myUid(), totalTxPackets > 0); + } finally { + if (result != null) { + result.close(); + } + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + result = mNsm.queryDetailsForUid( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid()); + fail("negative testUidDetails fails: no exception thrown."); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testTagDetails() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Relatively large tolerance to accommodate for history bucket size. + if (!shouldTestThisNetworkType(i, MINUTE * 120)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats result = null; + try { + result = mNsm.queryDetailsForUidTag( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid(), NETWORK_TAG); + assertNotNull(result); + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + long totalTxPackets = 0; + long totalRxPackets = 0; + long totalTxBytes = 0; + long totalRxBytes = 0; + while (result.hasNextBucket()) { + assertTrue(result.getNextBucket(bucket)); + assertTimestamps(bucket); + assertEquals(bucket.getState(), STATE_ALL); + assertEquals(bucket.getMetered(), METERED_ALL); + assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL); + assertEquals(bucket.getUid(), Process.myUid()); + if (bucket.getTag() == NETWORK_TAG) { + totalTxPackets += bucket.getTxPackets(); + totalRxPackets += bucket.getRxPackets(); + totalTxBytes += bucket.getTxBytes(); + totalRxBytes += bucket.getRxBytes(); + } + } + assertTrue("No Rx bytes tagged with 0x" + Integer.toHexString(NETWORK_TAG) + + " for uid " + Process.myUid(), totalRxBytes > 0); + assertTrue("No Rx packets tagged with 0x" + Integer.toHexString(NETWORK_TAG) + + " for uid " + Process.myUid(), totalRxPackets > 0); + assertTrue("No Tx bytes tagged with 0x" + Integer.toHexString(NETWORK_TAG) + + " for uid " + Process.myUid(), totalTxBytes > 0); + assertTrue("No Tx packets tagged with 0x" + Integer.toHexString(NETWORK_TAG) + + " for uid " + Process.myUid(), totalTxPackets > 0); + } finally { + if (result != null) { + result.close(); + } + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + result = mNsm.queryDetailsForUidTag( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid(), NETWORK_TAG); + fail("negative testUidDetails fails: no exception thrown."); + } catch (SecurityException e) { + // expected outcome + } + } + } + + class QueryResult { + public final int tag; + public final int state; + public final long total; + + QueryResult(int tag, int state, NetworkStats stats) { + this.tag = tag; + this.state = state; + total = getTotalAndAssertNotEmpty(stats, tag, state); + } + + public String toString() { + return String.format("QueryResult(tag=%s state=%s total=%d)", + tagToString(tag), stateToString(state), total); + } + } + + private NetworkStats getNetworkStatsForTagState(int i, int tag, int state) { + return mNsm.queryDetailsForUidTagState( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid(), tag, state); + } + + private void assertWithinPercentage(String msg, long expected, long actual, int percentage) { + long lowerBound = expected * (100 - percentage) / 100; + long upperBound = expected * (100 + percentage) / 100; + msg = String.format("%s: %d not within %d%% of %d", msg, actual, percentage, expected); + assertTrue(msg, lowerBound <= actual); + assertTrue(msg, upperBound >= actual); + } + + private void assertAlmostNoUnexpectedTraffic(NetworkStats result, int expectedTag, + int expectedState, long maxUnexpected) { + long total = 0; + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + while (result.hasNextBucket()) { + assertTrue(result.getNextBucket(bucket)); + total += bucket.getRxBytes() + bucket.getTxBytes(); + } + if (total <= maxUnexpected) return; + + fail(String.format("More than %d bytes of traffic when querying for " + + "tag %s state %s. Last bucket: uid=%d tag=%s state=%s bytes=%d/%d", + maxUnexpected, tagToString(expectedTag), stateToString(expectedState), + bucket.getUid(), tagToString(bucket.getTag()), stateToString(bucket.getState()), + bucket.getRxBytes(), bucket.getTxBytes())); + } + + @AppModeFull + public void testUidTagStateDetails() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Relatively large tolerance to accommodate for history bucket size. + if (!shouldTestThisNetworkType(i, MINUTE * 120)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + NetworkStats result = null; + try { + int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT; + int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT; + + int[] tagsWithTraffic = {NETWORK_TAG, TAG_NONE}; + int[] statesWithTraffic = {currentState, STATE_ALL}; + ArrayList<QueryResult> resultsWithTraffic = new ArrayList<>(); + + int[] statesWithNoTraffic = {otherState}; + int[] tagsWithNoTraffic = {NETWORK_TAG + 1}; + ArrayList<QueryResult> resultsWithNoTraffic = new ArrayList<>(); + + // Expect to see traffic when querying for any combination of a tag in + // tagsWithTraffic and a state in statesWithTraffic. + for (int tag : tagsWithTraffic) { + for (int state : statesWithTraffic) { + result = getNetworkStatsForTagState(i, tag, state); + resultsWithTraffic.add(new QueryResult(tag, state, result)); + result.close(); + result = null; + } + } + + // Expect that the results are within a few percentage points of each other. + // This is ensures that FIN retransmits after the transfer is complete don't cause + // the test to be flaky. The test URL currently returns just over 100k so this + // should not be too noisy. It also ensures that the traffic sent by the test + // harness, which is untagged, won't cause a failure. + long firstTotal = resultsWithTraffic.get(0).total; + for (QueryResult queryResult : resultsWithTraffic) { + assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 10); + } + + // Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any + // state in statesWithNoTraffic. + for (int tag : tagsWithNoTraffic) { + for (int state : statesWithTraffic) { + result = getNetworkStatsForTagState(i, tag, state); + assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100); + result.close(); + result = null; + } + } + for (int tag : tagsWithTraffic) { + for (int state : statesWithNoTraffic) { + result = getNetworkStatsForTagState(i, tag, state); + assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100); + result.close(); + result = null; + } + } + } finally { + if (result != null) { + result.close(); + } + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny"); + try { + result = mNsm.queryDetailsForUidTag( + mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i), + mStartTime, mEndTime, Process.myUid(), NETWORK_TAG); + fail("negative testUidDetails fails: no exception thrown."); + } catch (SecurityException e) { + // expected outcome + } + } + } + + @AppModeFull + public void testCallback() throws Exception { + for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) { + // Relatively large tolerance to accommodate for history bucket size. + if (!shouldTestThisNetworkType(i, MINUTE / 2)) { + continue; + } + setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow"); + + TestUsageCallback usageCallback = new TestUsageCallback(); + HandlerThread thread = new HandlerThread("callback-thread"); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(), + getSubscriberId(i), THRESHOLD_BYTES, usageCallback, handler); + + // TODO: Force traffic and check whether the callback is invoked. + // Right now the test only covers whether the callback can be registered, but not + // whether it is invoked upon data usage since we don't have a scalable way of + // storing files of >2MB in CTS. + + mNsm.unregisterUsageCallback(usageCallback); + } + } + + private String tagToString(Integer tag) { + if (tag == null) return "null"; + switch (tag) { + case TAG_NONE: + return "TAG_NONE"; + default: + return "0x" + Integer.toHexString(tag); + } + } + + private String stateToString(Integer state) { + if (state == null) return "null"; + switch (state) { + case STATE_ALL: + return "STATE_ALL"; + case STATE_DEFAULT: + return "STATE_DEFAULT"; + case STATE_FOREGROUND: + return "STATE_FOREGROUND"; + } + throw new IllegalArgumentException("Unknown state " + state); + } + + private long getTotalAndAssertNotEmpty(NetworkStats result, Integer expectedTag, + Integer expectedState) { + assertTrue(result != null); + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + long totalTxPackets = 0; + long totalRxPackets = 0; + long totalTxBytes = 0; + long totalRxBytes = 0; + while (result.hasNextBucket()) { + assertTrue(result.getNextBucket(bucket)); + assertTimestamps(bucket); + if (expectedTag != null) assertEquals(bucket.getTag(), (int) expectedTag); + if (expectedState != null) assertEquals(bucket.getState(), (int) expectedState); + assertEquals(bucket.getMetered(), METERED_ALL); + assertEquals(bucket.getDefaultNetworkStatus(), DEFAULT_NETWORK_ALL); + if (bucket.getUid() == Process.myUid()) { + totalTxPackets += bucket.getTxPackets(); + totalRxPackets += bucket.getRxPackets(); + totalTxBytes += bucket.getTxBytes(); + totalRxBytes += bucket.getRxBytes(); + } + } + assertFalse(result.getNextBucket(bucket)); + String msg = String.format("uid %d tag %s state %s", + Process.myUid(), tagToString(expectedTag), stateToString(expectedState)); + assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0); + assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0); + assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0); + assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0); + + return totalRxBytes + totalTxBytes; + } + + private long getTotalAndAssertNotEmpty(NetworkStats result) { + return getTotalAndAssertNotEmpty(result, null, STATE_ALL); + } + + private void assertTimestamps(final NetworkStats.Bucket bucket) { + assertTrue("Start timestamp " + bucket.getStartTimeStamp() + " is less than " + + mStartTime, bucket.getStartTimeStamp() >= mStartTime); + assertTrue("End timestamp " + bucket.getEndTimeStamp() + " is greater than " + + mEndTime, bucket.getEndTimeStamp() <= mEndTime); + } + + private static class TestUsageCallback extends NetworkStatsManager.UsageCallback { + @Override + public void onThresholdReached(int networkType, String subscriberId) { + Log.v(LOG_TAG, "Called onThresholdReached for networkType=" + networkType + + " subscriberId=" + subscriberId); + } + } +}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index 9307c27..9506081 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -15,6 +15,19 @@ */ package android.net.cts +import android.Manifest.permission.MANAGE_TEST_NETWORKS +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkRequest +import android.net.TestNetworkInterface +import android.net.TestNetworkManager +import android.net.TestNetworkSpecifier import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound @@ -32,14 +45,27 @@ import android.net.nsd.NsdManager.RegistrationListener import android.net.nsd.NsdManager.ResolveListener import android.net.nsd.NsdServiceInfo +import android.os.HandlerThread import android.platform.test.annotations.AppModeFull import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import com.android.net.module.util.ArrayTrackRecord import com.android.net.module.util.TrackRecord +import com.android.networkstack.apishim.ConstantsShim +import com.android.networkstack.apishim.NsdShimImpl +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.SC_V2 +import com.android.testutils.TestableNetworkAgent +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.runAsShell +import com.android.testutils.tryTest +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.net.ServerSocket @@ -57,12 +83,37 @@ private const val TIMEOUT_MS = 2000L private const val DBG = false +private val nsdShim = NsdShimImpl.newInstance() + @AppModeFull(reason = "Socket cannot bind in instant app mode") @RunWith(AndroidJUnit4::class) class NsdManagerTest { + // NsdManager is not updatable before S, so tests do not need to be backwards compatible + @get:Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2) + private val context by lazy { InstrumentationRegistry.getInstrumentation().context } private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) } - private val serviceName = "NsdTest%04d".format(Random().nextInt(1000)) + + private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) } + private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000)) + private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName) + + private lateinit var testNetwork1: TestTapNetwork + private lateinit var testNetwork2: TestTapNetwork + + private class TestTapNetwork( + val iface: TestNetworkInterface, + val requestCb: NetworkCallback, + val agent: TestableNetworkAgent, + val network: Network + ) { + fun close(cm: ConnectivityManager) { + cm.unregisterNetworkCallback(requestCb) + agent.unregister() + iface.fileDescriptor.close() + } + } private interface NsdEvent private open class NsdRecord<T : NsdEvent> private constructor( @@ -163,9 +214,14 @@ add(ServiceLost(si)) } - fun waitForServiceDiscovered(serviceName: String): NsdServiceInfo { + fun waitForServiceDiscovered( + serviceName: String, + expectedNetwork: Network? = null + ): NsdServiceInfo { return expectCallbackEventually<ServiceFound> { - it.serviceInfo.serviceName == serviceName + it.serviceInfo.serviceName == serviceName && + (expectedNetwork == null || + expectedNetwork == nsdShim.getNetwork(it.serviceInfo)) }.serviceInfo } } @@ -188,6 +244,58 @@ } } + @Before + fun setUp() { + handlerThread.start() + + runAsShell(MANAGE_TEST_NETWORKS) { + testNetwork1 = createTestNetwork() + testNetwork2 = createTestNetwork() + } + } + + private fun createTestNetwork(): TestTapNetwork { + val tnm = context.getSystemService(TestNetworkManager::class.java) + val iface = tnm.createTapInterface() + val cb = TestableNetworkCallback() + val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName) + cm.requestNetwork(NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(testNetworkSpecifier) + .build(), cb) + val agent = registerTestNetworkAgent(iface.interfaceName) + val network = agent.network ?: fail("Registered agent should have a network") + // The network has no INTERNET capability, so will be marked validated immediately + cb.expectAvailableThenValidatedCallbacks(network) + return TestTapNetwork(iface, cb, agent, network) + } + + private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent { + val agent = TestableNetworkAgent(context, handlerThread.looper, + NetworkCapabilities().apply { + removeCapability(NET_CAPABILITY_TRUSTED) + addTransportType(TRANSPORT_TEST) + setNetworkSpecifier(TestNetworkSpecifier(ifaceName)) + }, + LinkProperties().apply { + interfaceName = ifaceName + }, + NetworkAgentConfig.Builder().build()) + agent.register() + agent.markConnected() + return agent + } + + @After + fun tearDown() { + runAsShell(MANAGE_TEST_NETWORKS) { + testNetwork1.close(cm) + testNetwork2.close(cm) + } + handlerThread.quitSafely() + } + @Test fun testNsdManager() { val si = NsdServiceInfo() @@ -298,6 +406,149 @@ registrationRecord2.expectCallback<ServiceUnregistered>() } + @Test + fun testNsdManager_DiscoverOnNetwork() { + // This tests requires shims supporting T+ APIs (discovering on specific network) + assumeTrue(ConstantsShim.VERSION > SC_V2) + + val si = NsdServiceInfo() + si.serviceType = SERVICE_TYPE + si.serviceName = this.serviceName + si.port = 12345 // Test won't try to connect so port does not matter + + val registrationRecord = NsdRegistrationRecord() + val registeredInfo = registerService(registrationRecord, si) + + tryTest { + val discoveryRecord = NsdDiscoveryRecord() + nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, discoveryRecord) + + val foundInfo = discoveryRecord.waitForServiceDiscovered( + serviceName, testNetwork1.network) + assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo)) + + // Rewind to ensure the service is not found on the other interface + discoveryRecord.nextEvents.rewind(0) + assertNull(discoveryRecord.nextEvents.poll(timeoutMs = 100L) { + it is ServiceFound && + it.serviceInfo.serviceName == registeredInfo.serviceName && + nsdShim.getNetwork(it.serviceInfo) != testNetwork1.network + }, "The service should not be found on this network") + } cleanup { + nsdManager.unregisterService(registrationRecord) + } + } + + @Test + fun testNsdManager_DiscoverWithNetworkRequest() { + // This tests requires shims supporting T+ APIs (discovering on network request) + assumeTrue(ConstantsShim.VERSION > SC_V2) + + val si = NsdServiceInfo() + si.serviceType = SERVICE_TYPE + si.serviceName = this.serviceName + si.port = 12345 // Test won't try to connect so port does not matter + + val registrationRecord = NsdRegistrationRecord() + val registeredInfo1 = registerService(registrationRecord, si) + val discoveryRecord = NsdDiscoveryRecord() + + tryTest { + val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName) + nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, + NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(specifier) + .build(), + discoveryRecord) + + val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>() + assertEquals(SERVICE_TYPE, discoveryStarted.serviceType) + + val serviceDiscovered = discoveryRecord.expectCallback<ServiceFound>() + assertEquals(registeredInfo1.serviceName, serviceDiscovered.serviceInfo.serviceName) + assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered.serviceInfo)) + + // Unregister, then register the service back: it should be lost and found again + nsdManager.unregisterService(registrationRecord) + val serviceLost1 = discoveryRecord.expectCallback<ServiceLost>() + assertEquals(registeredInfo1.serviceName, serviceLost1.serviceInfo.serviceName) + assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo)) + + registrationRecord.expectCallback<ServiceUnregistered>() + val registeredInfo2 = registerService(registrationRecord, si) + val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>() + assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName) + assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo)) + + // Teardown, then bring back up a network on the test interface: the service should + // go away, then come back + testNetwork1.agent.unregister() + val serviceLost = discoveryRecord.expectCallback<ServiceLost>() + assertEquals(registeredInfo2.serviceName, serviceLost.serviceInfo.serviceName) + assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost.serviceInfo)) + + val newAgent = runAsShell(MANAGE_TEST_NETWORKS) { + registerTestNetworkAgent(testNetwork1.iface.interfaceName) + } + val newNetwork = newAgent.network ?: fail("Registered agent should have a network") + val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>() + assertEquals(registeredInfo2.serviceName, serviceDiscovered3.serviceInfo.serviceName) + assertEquals(newNetwork, nsdShim.getNetwork(serviceDiscovered3.serviceInfo)) + } cleanupStep { + nsdManager.stopServiceDiscovery(discoveryRecord) + discoveryRecord.expectCallback<DiscoveryStopped>() + } cleanup { + nsdManager.unregisterService(registrationRecord) + } + } + + @Test + fun testNsdManager_ResolveOnNetwork() { + // This tests requires shims supporting T+ APIs (NsdServiceInfo.network) + assumeTrue(ConstantsShim.VERSION > SC_V2) + + val si = NsdServiceInfo() + si.serviceType = SERVICE_TYPE + si.serviceName = this.serviceName + si.port = 12345 // Test won't try to connect so port does not matter + + val registrationRecord = NsdRegistrationRecord() + val registeredInfo = registerService(registrationRecord, si) + tryTest { + val resolveRecord = NsdResolveRecord() + + val discoveryRecord = NsdDiscoveryRecord() + nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord) + + val foundInfo1 = discoveryRecord.waitForServiceDiscovered( + serviceName, testNetwork1.network) + assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1)) + // Rewind as the service could be found on each interface in any order + discoveryRecord.nextEvents.rewind(0) + val foundInfo2 = discoveryRecord.waitForServiceDiscovered( + serviceName, testNetwork2.network) + assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2)) + + nsdManager.resolveService(foundInfo1, resolveRecord) + val cb = resolveRecord.expectCallback<ServiceResolved>() + cb.serviceInfo.let { + // Resolved service type has leading dot + assertEquals(".$SERVICE_TYPE", it.serviceType) + assertEquals(registeredInfo.serviceName, it.serviceName) + assertEquals(si.port, it.port) + assertEquals(testNetwork1.network, nsdShim.getNetwork(it)) + } + // TODO: check that MDNS packets are sent only on testNetwork1. + } cleanupStep { + nsdManager.unregisterService(registrationRecord) + } cleanup { + registrationRecord.expectCallback<ServiceUnregistered>() + } + } + /** * Register a service and return its registration record. */
diff --git a/tests/common/java/android/net/StaticIpConfigurationTest.java b/tests/cts/net/src/android/net/cts/StaticIpConfigurationTest.java similarity index 81% rename from tests/common/java/android/net/StaticIpConfigurationTest.java rename to tests/cts/net/src/android/net/cts/StaticIpConfigurationTest.java index b5f23bf..9b2756c 100644 --- a/tests/common/java/android/net/StaticIpConfigurationTest.java +++ b/tests/cts/net/src/android/net/cts/StaticIpConfigurationTest.java
@@ -14,20 +14,30 @@ * limitations under the License. */ -package android.net; +package android.net.cts; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.RouteInfo; +import android.net.StaticIpConfiguration; +import android.os.Build; import android.os.Parcel; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.testutils.DevSdkIgnoreRule; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,15 +52,20 @@ private static final String ADDRSTR = "192.0.2.2/25"; private static final LinkAddress ADDR = new LinkAddress(ADDRSTR); - private static final InetAddress GATEWAY = IpAddress("192.0.2.1"); - private static final InetAddress OFFLINKGATEWAY = IpAddress("192.0.2.129"); - private static final InetAddress DNS1 = IpAddress("8.8.8.8"); - private static final InetAddress DNS2 = IpAddress("8.8.4.4"); - private static final InetAddress DNS3 = IpAddress("4.2.2.2"); + private static final InetAddress GATEWAY = ipAddress("192.0.2.1"); + private static final InetAddress OFFLINKGATEWAY = ipAddress("192.0.2.129"); + private static final InetAddress DNS1 = ipAddress("8.8.8.8"); + private static final InetAddress DNS2 = ipAddress("8.8.4.4"); + private static final InetAddress DNS3 = ipAddress("4.2.2.2"); + private static final InetAddress IPV6_ADDRESS = ipAddress("2001:4860:800d::68"); + private static final LinkAddress IPV6_LINK_ADDRESS = new LinkAddress("2001:db8::1/64"); private static final String IFACE = "eth0"; private static final String FAKE_DOMAINS = "google.com"; - private static InetAddress IpAddress(String addr) { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private static InetAddress ipAddress(String addr) { return InetAddress.parseNumericAddress(addr); } @@ -241,6 +256,29 @@ assertEquals(DNS1, s.getDnsServers().get(0)); } + @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testIllegalBuilders() { + assertThrows("Can't set IP Address to IPv6!", IllegalArgumentException.class, () -> { + StaticIpConfiguration.Builder b = new StaticIpConfiguration.Builder().setIpAddress( + IPV6_LINK_ADDRESS); + }); + + assertThrows("Can't set gateway to IPv6!", IllegalArgumentException.class, () -> { + StaticIpConfiguration.Builder b = new StaticIpConfiguration.Builder().setGateway( + IPV6_ADDRESS); + }); + + assertThrows("Can't set DNS servers using IPv6!", IllegalArgumentException.class, () -> { + final ArrayList<InetAddress> dnsServers = new ArrayList<>(); + dnsServers.add(DNS1); + dnsServers.add(IPV6_ADDRESS); + + StaticIpConfiguration.Builder b = new StaticIpConfiguration.Builder().setDnsServers( + dnsServers); + }); + } + @Test public void testAddDnsServers() { final StaticIpConfiguration s = new StaticIpConfiguration((StaticIpConfiguration) null);
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp index 7b5b44f..530fa91 100644 --- a/tests/integration/Android.bp +++ b/tests/integration/Android.bp
@@ -53,6 +53,8 @@ // android_library does not include JNI libs: include NetworkStack dependencies here "libnativehelper_compat_libc++", "libnetworkstackutilsjni", + "libandroid_net_connectivity_com_android_net_module_util_jni", + "libservice-connectivity", ], jarjar_rules: ":connectivity-jarjar-rules", }
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java index 4dc86ff..365c0cf 100644 --- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java +++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -21,6 +21,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; @@ -109,6 +110,9 @@ case TRANSPORT_WIFI_AWARE: mScore = new NetworkScore.Builder().setLegacyInt(20).build(); break; + case TRANSPORT_TEST: + mScore = new NetworkScore.Builder().build(); + break; case TRANSPORT_VPN: mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN); // VPNs deduce the SUSPENDED capability from their underlying networks and there
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp index a56f76e..2c44010 100644 --- a/tests/mts/Android.bp +++ b/tests/mts/Android.bp
@@ -23,6 +23,9 @@ "general-tests", "mts-tethering", ], + defaults: [ + "connectivity-mainline-presubmit-cc-defaults", + ], require_root: true, static_libs: [ "libbase",
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp index 5e3df57..142e013 100644 --- a/tests/mts/bpf_existence_test.cpp +++ b/tests/mts/bpf_existence_test.cpp
@@ -13,72 +13,92 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * bpf_existence_test.cpp - checks that the device runs expected BPF programs + * bpf_existence_test.cpp - checks that the device has expected BPF programs and maps */ #include <cstdint> +#include <set> #include <string> -#include <vector> -#include <android-base/strings.h> +#include <android/api-level.h> #include <android-base/properties.h> #include <android-modules-utils/sdk_level.h> #include <gtest/gtest.h> using std::find; +using std::set; using std::string; -using std::vector; using android::modules::sdklevel::IsAtLeastR; using android::modules::sdklevel::IsAtLeastS; using android::modules::sdklevel::IsAtLeastT; +// Mainline development branches lack the constant for the current development OS. +#ifndef __ANDROID_API_T__ +#define __ANDROID_API_T__ 33 +#endif + +#define PLATFORM "/sys/fs/bpf/" +#define TETHERING "/sys/fs/bpf/tethering/" + class BpfExistenceTest : public ::testing::Test { }; -static const vector<string> INTRODUCED_R = { - "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_ether", - "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_rawip", +static const set<string> INTRODUCED_R = { + PLATFORM "map_offload_tether_ingress_map", + PLATFORM "map_offload_tether_limit_map", + PLATFORM "map_offload_tether_stats_map", + PLATFORM "prog_offload_schedcls_ingress_tether_ether", + PLATFORM "prog_offload_schedcls_ingress_tether_rawip", }; -static const vector<string> INTRODUCED_S = { - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream4_ether", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream4_rawip", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_rawip", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream4_ether", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream4_rawip", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream6_ether", - "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_upstream6_rawip", +static const set<string> INTRODUCED_S = { + TETHERING "map_offload_tether_dev_map", + TETHERING "map_offload_tether_downstream4_map", + TETHERING "map_offload_tether_downstream64_map", + TETHERING "map_offload_tether_downstream6_map", + TETHERING "map_offload_tether_error_map", + TETHERING "map_offload_tether_limit_map", + TETHERING "map_offload_tether_stats_map", + TETHERING "map_offload_tether_upstream4_map", + TETHERING "map_offload_tether_upstream6_map", + TETHERING "map_test_tether_downstream6_map", + TETHERING "prog_offload_schedcls_tether_downstream4_ether", + TETHERING "prog_offload_schedcls_tether_downstream4_rawip", + TETHERING "prog_offload_schedcls_tether_downstream6_ether", + TETHERING "prog_offload_schedcls_tether_downstream6_rawip", + TETHERING "prog_offload_schedcls_tether_upstream4_ether", + TETHERING "prog_offload_schedcls_tether_upstream4_rawip", + TETHERING "prog_offload_schedcls_tether_upstream6_ether", + TETHERING "prog_offload_schedcls_tether_upstream6_rawip", }; -static const vector<string> REMOVED_S = { - "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_ether", - "/sys/fs/bpf/prog_offload_schedcls_ingress_tether_rawip", +static const set<string> REMOVED_S = { + PLATFORM "map_offload_tether_ingress_map", + PLATFORM "map_offload_tether_limit_map", + PLATFORM "map_offload_tether_stats_map", + PLATFORM "prog_offload_schedcls_ingress_tether_ether", + PLATFORM "prog_offload_schedcls_ingress_tether_rawip", }; -static const vector<string> INTRODUCED_T = { +static const set<string> INTRODUCED_T = { }; -static const vector<string> REMOVED_T = { +static const set<string> REMOVED_T = { }; -void addAll(vector<string>* a, const vector<string>& b) { - a->insert(a->end(), b.begin(), b.end()); +void addAll(set<string>* a, const set<string>& b) { + a->insert(b.begin(), b.end()); } -void removeAll(vector<string>* a, const vector<string> b) { +void removeAll(set<string>* a, const set<string>& b) { for (const auto& toRemove : b) { - auto iter = find(a->begin(), a->end(), toRemove); - while (iter != a->end()) { - a->erase(iter); - iter = find(a->begin(), a->end(), toRemove); - } + a->erase(toRemove); } } -void getFileLists(vector<string>* expected, vector<string>* unexpected) { +void getFileLists(set<string>* expected, set<string>* unexpected) { unexpected->clear(); expected->clear(); @@ -112,8 +132,8 @@ } void checkFiles() { - vector<string> mustExist; - vector<string> mustNotExist; + set<string> mustExist; + set<string> mustNotExist; getFileLists(&mustExist, &mustNotExist); @@ -132,9 +152,9 @@ TEST_F(BpfExistenceTest, TestPrograms) { // Pre-flight check to ensure test has been updated. - uint64_t buildVersionSdk = android::base::GetUintProperty<uint64_t>("ro.build.version.sdk", 0); + uint64_t buildVersionSdk = android_get_device_api_level(); ASSERT_NE(0, buildVersionSdk) << "Unable to determine device SDK version"; - if (buildVersionSdk > 33 && buildVersionSdk != 10000) { + if (buildVersionSdk > __ANDROID_API_T__ && buildVersionSdk != __ANDROID_API_FUTURE__) { FAIL() << "Unknown OS version " << buildVersionSdk << ", please update this test"; }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 9e80a25..fe0210c 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp
@@ -23,6 +23,7 @@ name: "FrameworksNetTests-jni-defaults", jni_libs: [ "ld-android", + "libandroid_net_frameworktests_util_jni", "libbase", "libbinder", "libbpf_bcc", @@ -38,8 +39,8 @@ "liblog", "liblzma", "libnativehelper", - "libnetdbpf", "libnetdutils", + "libnetworkstats", "libnetworkstatsfactorytestjni", "libpackagelistparser", "libpcre2", @@ -74,6 +75,7 @@ "java/android/net/TelephonyNetworkSpecifierTest.java", "java/android/net/VpnManagerTest.java", "java/android/net/ipmemorystore/*.java", + "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt", "java/android/net/nsd/*.java", "java/com/android/internal/net/NetworkUtilsInternalTest.java", "java/com/android/internal/net/VpnProfileTest.java", @@ -88,7 +90,9 @@ "java/com/android/server/connectivity/NetdEventListenerServiceTest.java", "java/com/android/server/connectivity/VpnTest.java", "java/com/android/server/net/ipmemorystore/*.java", + "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java", "java/com/android/server/net/NetworkStats*.java", + "java/com/android/server/net/TestableUsageCallback.kt", ] } @@ -130,6 +134,7 @@ "platform-compat-test-rules", "platform-test-annotations", "service-connectivity-pre-jarjar", + "service-connectivity-tiramisu-pre-jarjar", "services.core-vpn", ], libs: [ @@ -140,6 +145,7 @@ "ServiceConnectivityResources", ], visibility: ["//packages/modules/Connectivity/tests:__subpackages__"], + exclude_kotlinc_generated_files: false, } android_test {
diff --git a/tests/unit/jarjar-rules.txt b/tests/unit/jarjar-rules.txt index ca88672..eb3e32a 100644 --- a/tests/unit/jarjar-rules.txt +++ b/tests/unit/jarjar-rules.txt
@@ -1,2 +1,3 @@ # Module library in frameworks/libs/net rule com.android.net.module.util.** android.net.frameworktests.util.@1 +rule com.android.testutils.TestBpfMap* android.net.frameworktests.testutils.TestBpfMap@1
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java index 6e51069..561e621 100644 --- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java +++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -240,6 +240,27 @@ assertFalse(stats.hasNextBucket()); } + + @Test + public void testQueryDetailsForDevice() throws Exception { + final long startTime = 1; + final long endTime = 100; + + reset(mStatsSession); + when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession); + when(mStatsSession.getHistoryIntervalForNetwork(any(NetworkTemplate.class), + anyInt(), anyLong(), anyLong())) + .thenReturn(new NetworkStatsHistory(10, 0)); + final NetworkTemplate template = new NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE) + .setMeteredness(NetworkStats.Bucket.METERED_YES).build(); + NetworkStats stats = mManager.queryDetailsForDevice(template, startTime, endTime); + + verify(mStatsSession, times(1)).getHistoryIntervalForNetwork( + eq(template), eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)); + + assertFalse(stats.hasNextBucket()); + } + private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) { assertEquals(expected.uid, actual.getUid()); assertEquals(expected.rxBytes, actual.getRxBytes());
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java index 83de40e..a151f03 100644 --- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java +++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -35,6 +35,7 @@ import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,6 +65,9 @@ private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); private static final int TEST_MTU = 1300; + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + private final MockContext mMockContext = new MockContext() { @Override
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt index b1ffc92..4b2d874 100644 --- a/tests/unit/java/android/net/NetworkIdentityTest.kt +++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -17,11 +17,16 @@ package android.net import android.content.Context +import android.net.ConnectivityManager.MAX_NETWORK_TYPE +import android.net.ConnectivityManager.TYPE_ETHERNET import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_NONE +import android.net.ConnectivityManager.TYPE_WIFI import android.net.NetworkIdentity.OEM_NONE import android.net.NetworkIdentity.OEM_PAID import android.net.NetworkIdentity.OEM_PRIVATE import android.net.NetworkIdentity.getOemBitfield +import android.app.usage.NetworkStatsManager import android.telephony.TelephonyManager import android.os.Build import com.android.testutils.DevSdkIgnoreRule @@ -30,10 +35,12 @@ import org.junit.runner.RunWith import org.mockito.Mockito.mock import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue private const val TEST_IMSI = "testimsi" +private const val TEST_WIFI_KEY = "testwifikey" @RunWith(DevSdkIgnoreRunner::class) @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @@ -74,12 +81,12 @@ } @Test - fun testGetMetered() { + fun testIsMetered() { // Verify network is metered. val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext, buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI), false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) - assertTrue(netIdent1.getMetered()) + assertTrue(netIdent1.isMetered()) // Verify network is not metered because it has NET_CAPABILITY_NOT_METERED capability. val capsNotMetered = NetworkCapabilities.Builder().apply { @@ -88,7 +95,7 @@ val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext, buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI), false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) - assertFalse(netIdent2.getMetered()) + assertFalse(netIdent2.isMetered()) // Verify network is not metered because it has NET_CAPABILITY_TEMPORARILY_NOT_METERED // capability . @@ -98,6 +105,121 @@ val netIdent3 = NetworkIdentity.buildNetworkIdentity(mockContext, buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI), false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) - assertFalse(netIdent3.getMetered()) + assertFalse(netIdent3.isMetered()) + } + + @Test + fun testBuilder() { + val oemPrivateRoamingNotMeteredCap = NetworkCapabilities().apply { + addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + } + val identFromSnapshot = NetworkIdentity.Builder().setNetworkStateSnapshot( + buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI)) + .setDefaultNetwork(true) + .setRatType(TelephonyManager.NETWORK_TYPE_UMTS) + .build() + val identFromLegacyBuild = NetworkIdentity.buildNetworkIdentity(mockContext, + buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI), + true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) + val identFromConstructor = NetworkIdentity(TYPE_MOBILE, + TelephonyManager.NETWORK_TYPE_UMTS, + TEST_IMSI, + null /* wifiNetworkKey */, + true /* roaming */, + false /* metered */, + true /* defaultNetwork */, + NetworkTemplate.OEM_MANAGED_PRIVATE) + assertEquals(identFromLegacyBuild, identFromSnapshot) + assertEquals(identFromConstructor, identFromSnapshot) + + // Assert non-wifi can't have wifi network key. + assertFailsWith<IllegalArgumentException> { + NetworkIdentity.Builder() + .setType(TYPE_ETHERNET) + .setWifiNetworkKey(TEST_WIFI_KEY) + .build() + } + + // Assert non-mobile can't have ratType. + assertFailsWith<IllegalArgumentException> { + NetworkIdentity.Builder() + .setType(TYPE_WIFI) + .setRatType(TelephonyManager.NETWORK_TYPE_LTE) + .build() + } + } + + @Test + fun testBuilder_type() { + // Assert illegal type values cannot make an identity. + listOf(Integer.MIN_VALUE, TYPE_NONE - 1, MAX_NETWORK_TYPE + 1, Integer.MAX_VALUE) + .forEach { type -> + assertFailsWith<IllegalArgumentException> { + NetworkIdentity.Builder().setType(type).build() + } + } + + // Verify legitimate type values can make an identity. + for (type in TYPE_NONE..MAX_NETWORK_TYPE) { + NetworkIdentity.Builder().setType(type).build().also { + assertEquals(it.type, type) + } + } + } + + @Test + fun testBuilder_ratType() { + // Assert illegal ratTypes cannot make an identity. + listOf(Integer.MIN_VALUE, NetworkTemplate.NETWORK_TYPE_ALL, + NetworkStatsManager.NETWORK_TYPE_5G_NSA - 1, Integer.MAX_VALUE) + .forEach { + assertFailsWith<IllegalArgumentException> { + NetworkIdentity.Builder() + .setType(TYPE_MOBILE) + .setRatType(it) + .build() + } + } + + // Verify legitimate ratTypes can make an identity. + TelephonyManager.getAllNetworkTypes().toMutableList().also { + it.add(TelephonyManager.NETWORK_TYPE_UNKNOWN) + it.add(NetworkStatsManager.NETWORK_TYPE_5G_NSA) + }.forEach { rat -> + NetworkIdentity.Builder() + .setType(TYPE_MOBILE) + .setRatType(rat) + .build().also { + assertEquals(it.ratType, rat) + } + } + } + + @Test + fun testBuilder_oemManaged() { + // Assert illegal oemManage values cannot make an identity. + listOf(Integer.MIN_VALUE, NetworkTemplate.OEM_MANAGED_ALL, NetworkTemplate.OEM_MANAGED_YES, + Integer.MAX_VALUE) + .forEach { oemManaged -> + assertFailsWith<IllegalArgumentException> { + NetworkIdentity.Builder() + .setType(TYPE_MOBILE) + .setOemManaged(oemManaged) + .build() + } + } + + // Verify legitimate oem managed values can make an identity. + listOf(NetworkTemplate.OEM_MANAGED_NO, NetworkTemplate.OEM_MANAGED_PAID, + NetworkTemplate.OEM_MANAGED_PRIVATE, NetworkTemplate.OEM_MANAGED_PAID or + NetworkTemplate.OEM_MANAGED_PRIVATE) + .forEach { oemManaged -> + NetworkIdentity.Builder() + .setOemManaged(oemManaged) + .build().also { + assertEquals(it.oemManaged, oemManaged) + } + } } }
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java index e4fc118..97a93ca 100644 --- a/tests/unit/java/android/net/NetworkStatsAccessTest.java +++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -16,6 +16,8 @@ package android.net; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @@ -25,7 +27,6 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.PackageManager; -import android.os.Build; import android.telephony.TelephonyManager; import androidx.test.filters.SmallTest; @@ -42,7 +43,7 @@ @RunWith(DevSdkIgnoreRunner.class) @SmallTest [email protected](Build.VERSION_CODES.S) [email protected](SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public class NetworkStatsAccessTest { private static final String TEST_PKG = "com.example.test"; private static final int TEST_PID = 1234;
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java index 1c557d6..c27ee93 100644 --- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java +++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -29,6 +29,7 @@ import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static com.android.testutils.MiscAsserts.assertThrows; import static org.junit.Assert.assertArrayEquals; @@ -37,12 +38,13 @@ import static org.junit.Assert.fail; import android.content.res.Resources; -import android.os.Build; +import android.net.NetworkStatsCollection.Key; import android.os.Process; import android.os.UserHandle; import android.telephony.SubscriptionPlan; import android.telephony.TelephonyManager; import android.text.format.DateUtils; +import android.util.ArrayMap; import android.util.RecurrenceRule; import androidx.test.InstrumentationRegistry; @@ -59,6 +61,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -72,13 +75,14 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Tests for {@link NetworkStatsCollection}. */ @RunWith(DevSdkIgnoreRunner.class) @SmallTest [email protected](Build.VERSION_CODES.S) [email protected](SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public class NetworkStatsCollectionTest { private static final String TEST_FILE = "test.bin"; @@ -195,8 +199,8 @@ // record empty data straddling between buckets final NetworkStats.Entry entry = new NetworkStats.Entry(); entry.rxBytes = 32; - collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS, - 90 * MINUTE_IN_MILLIS, entry); + collection.recordData(Mockito.mock(NetworkIdentitySet.class), UID_ALL, SET_DEFAULT, + TAG_NONE, 30 * MINUTE_IN_MILLIS, 90 * MINUTE_IN_MILLIS, entry); // assert that we report boundary in atomic buckets assertEquals(0, collection.getStartMillis()); @@ -529,6 +533,52 @@ assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0)); } + @Test + public void testBuilder() { + final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>(); + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final NetworkIdentitySet ident = new NetworkIdentitySet(); + final Key key1 = new Key(ident, 0, 0, 0); + final Key key2 = new Key(ident, 1, 0, 0); + final long bucketDuration = 10; + + final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40, + 4, 50, 5, 60); + final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3, + 41, 7, 1, 0); + + NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5) + .addEntry(entry1) + .addEntry(entry2) + .build(); + + NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5); + + NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration) + .addEntry(key1, history1) + .addEntry(key2, history2) + .build(); + + // The builder will omit any entry with empty history. Thus, history2 + // is not expected in the result collection. + expectedEntries.put(key1, history1); + + final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries(); + + assertEquals(expectedEntries.size(), actualEntries.size()); + for (Key expectedKey : expectedEntries.keySet()) { + final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey); + + final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey); + assertNotNull(actualHistory); + + assertEquals(expectedHistory.getEntries(), actualHistory.getEntries()); + + actualEntries.remove(expectedKey); + } + assertEquals(0, actualEntries.size()); + } + /** * Copy a {@link Resources#openRawResource(int)} into {@link File} for * testing purposes. @@ -586,6 +636,14 @@ actual.txBytes, actual.txPackets, 0L)); } + private static void assertEntry(NetworkStatsHistory.Entry expected, + NetworkStatsHistory.Entry actual) { + assertEntry(new NetworkStats.Entry(actual.rxBytes, actual.rxPackets, + actual.txBytes, actual.txPackets, 0L), + new NetworkStats.Entry(actual.rxBytes, actual.rxPackets, + actual.txBytes, actual.txPackets, 0L)); + } + private static void assertEntry(NetworkStats.Entry expected, NetworkStats.Entry actual) { assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes);
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java index c5f8c00..c170605 100644 --- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java +++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,6 +56,7 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.util.List; import java.util.Random; @RunWith(DevSdkIgnoreRunner.class) @@ -532,6 +533,40 @@ assertEquals(512L + 4096L, stats.getTotalBytes()); } + @Test + public void testBuilder() { + final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40, + 4, 50, 5, 60); + final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3, + 41, 7, 1, 0); + final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11, + 14, 31, 2, 80); + + final NetworkStatsHistory statsEmpty = new NetworkStatsHistory + .Builder(HOUR_IN_MILLIS, 10).build(); + assertEquals(0, statsEmpty.getEntries().size()); + assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration()); + + NetworkStatsHistory statsSingle = new NetworkStatsHistory + .Builder(HOUR_IN_MILLIS, 8) + .addEntry(entry1) + .build(); + assertEquals(1, statsSingle.getEntries().size()); + assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration()); + assertEquals(entry1, statsSingle.getEntries().get(0)); + + NetworkStatsHistory statsMultiple = new NetworkStatsHistory + .Builder(SECOND_IN_MILLIS, 0) + .addEntry(entry1).addEntry(entry2).addEntry(entry3) + .build(); + final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries(); + assertEquals(3, entries.size()); + assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration()); + assertEquals(entry1, entries.get(0)); + assertEquals(entry2, entries.get(1)); + assertEquals(entry3, entries.get(2)); + } + private static void assertIndexBeforeAfter( NetworkStatsHistory stats, int before, int after, long time) { assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java index c971da1..b0cc16c 100644 --- a/tests/unit/java/android/net/NetworkStatsTest.java +++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -37,6 +37,7 @@ import static android.net.NetworkStats.UID_ALL; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Build; @@ -53,8 +54,10 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.Iterator; @RunWith(DevSdkIgnoreRunner.class) @SmallTest @@ -1037,6 +1040,29 @@ assertEquals(secondEntry, stats.getValues(1, null)); } + @Test + public void testIterator() { + final NetworkStats emptyStats = new NetworkStats(0, 0); + final Iterator emptyIterator = emptyStats.iterator(); + assertFalse(emptyIterator.hasNext()); + + final int numEntries = 10; + final ArrayList<NetworkStats.Entry> entries = new ArrayList<>(); + final NetworkStats stats = new NetworkStats(TEST_START, 1); + for (int i = 0; i < numEntries; ++i) { + NetworkStats.Entry entry = new NetworkStats.Entry("test1", 10100, SET_DEFAULT, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, + i * 10L /* rxBytes */, i * 3L /* rxPackets */, + i * 15L /* txBytes */, i * 2L /* txPackets */, 0L /* operations */); + stats.insertEntry(entry); + entries.add(entry); + } + + for (NetworkStats.Entry e : stats) { + assertEquals(e, entries.remove(0)); + } + } + private static void assertContains(NetworkStats stats, String iface, int uid, int set, int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { @@ -1057,22 +1083,22 @@ private static void assertValues( NetworkStats.Entry entry, String iface, int uid, int set, int tag, int metered, int roaming, int defaultNetwork) { - assertEquals(iface, entry.iface); - assertEquals(uid, entry.uid); - assertEquals(set, entry.set); - assertEquals(tag, entry.tag); - assertEquals(metered, entry.metered); - assertEquals(roaming, entry.roaming); - assertEquals(defaultNetwork, entry.defaultNetwork); + assertEquals(iface, entry.getIface()); + assertEquals(uid, entry.getUid()); + assertEquals(set, entry.getSet()); + assertEquals(tag, entry.getTag()); + assertEquals(metered, entry.getMetered()); + assertEquals(roaming, entry.getRoaming()); + assertEquals(defaultNetwork, entry.getDefaultNetwork()); } private static void assertValues(NetworkStats.Entry entry, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { - assertEquals(rxBytes, entry.rxBytes); - assertEquals(rxPackets, entry.rxPackets); - assertEquals(txBytes, entry.txBytes); - assertEquals(txPackets, entry.txPackets); - assertEquals(operations, entry.operations); + assertEquals(rxBytes, entry.getRxBytes()); + assertEquals(rxPackets, entry.getRxPackets()); + assertEquals(txBytes, entry.getTxBytes()); + assertEquals(txPackets, entry.getTxPackets()); + assertEquals(operations, entry.getOperations()); } }
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt index 15db45c..453612f 100644 --- a/tests/unit/java/android/net/NetworkTemplateTest.kt +++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -16,13 +16,13 @@ package android.net +import android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA import android.content.Context import android.net.ConnectivityManager.TYPE_MOBILE import android.net.ConnectivityManager.TYPE_WIFI import android.net.NetworkIdentity.OEM_NONE import android.net.NetworkIdentity.OEM_PAID import android.net.NetworkIdentity.OEM_PRIVATE -import android.net.NetworkIdentity.SUBTYPE_COMBINED import android.net.NetworkIdentity.buildNetworkIdentity import android.net.NetworkStats.DEFAULT_NETWORK_ALL import android.net.NetworkStats.METERED_ALL @@ -37,7 +37,6 @@ import android.net.NetworkTemplate.MATCH_PROXY import android.net.NetworkTemplate.MATCH_WIFI import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD -import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA import android.net.NetworkTemplate.NETWORK_TYPE_ALL import android.net.NetworkTemplate.OEM_MANAGED_ALL import android.net.NetworkTemplate.OEM_MANAGED_NO @@ -57,6 +56,7 @@ import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.SC_V2 import com.android.testutils.assertParcelSane import org.junit.Before import org.junit.Test @@ -95,7 +95,7 @@ oemManaged: Int = OEM_NONE, metered: Boolean = true ): NetworkStateSnapshot { - `when`(mockWifiInfo.getCurrentNetworkKey()).thenReturn(wifiKey) + `when`(mockWifiInfo.getNetworkKey()).thenReturn(wifiKey) val lp = LinkProperties() val caps = NetworkCapabilities().apply { setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered) @@ -312,7 +312,7 @@ val identLteMetered = buildNetworkIdentity( mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_LTE) val identCombinedMetered = buildNetworkIdentity( - mockContext, stateMobileImsi1Metered, false, SUBTYPE_COMBINED) + mockContext, stateMobileImsi1Metered, false, NetworkTemplate.NETWORK_TYPE_ALL) val identImsi2UmtsMetered = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2), false, TelephonyManager.NETWORK_TYPE_UMTS) val identWifi = buildNetworkIdentity( @@ -326,7 +326,7 @@ val identLteNonMetered = buildNetworkIdentity( mockContext, stateMobileImsi1NonMetered, false, TelephonyManager.NETWORK_TYPE_LTE) val identCombinedNonMetered = buildNetworkIdentity( - mockContext, stateMobileImsi1NonMetered, false, SUBTYPE_COMBINED) + mockContext, stateMobileImsi1NonMetered, false, NetworkTemplate.NETWORK_TYPE_ALL) val identImsi2UmtsNonMetered = buildNetworkIdentity(mockContext, stateMobileImsi2NonMetered, false, TelephonyManager.NETWORK_TYPE_UMTS) @@ -556,7 +556,7 @@ } } - @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S) + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available @Test fun testBuilderMatchRules() { // Verify unknown match rules cannot construct templates. @@ -657,7 +657,7 @@ } } - @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S) + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available @Test fun testBuilderWifiNetworkKeys() { // Verify template builder which generates same template with the given different
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt new file mode 100644 index 0000000..e4943ea --- /dev/null +++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -0,0 +1,112 @@ +/* + * 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 android.net.netstats + +import android.net.NetworkStatsCollection +import androidx.test.InstrumentationRegistry +import androidx.test.filters.SmallTest +import com.android.frameworks.tests.net.R +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.SC_V2 +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import java.io.DataInputStream +import java.net.ProtocolException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.fail + +private const val BUCKET_DURATION_MS = 2 * 60 * 60 * 1000L + +@RunWith(DevSdkIgnoreRunner::class) +@SmallTest [email protected](SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available +class NetworkStatsDataMigrationUtilsTest { + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testReadPlatformCollection() { + // Verify the method throws for wrong file version. + assertFailsWith<ProtocolException> { + NetworkStatsDataMigrationUtils.readPlatformCollection( + NetworkStatsCollection.Builder(BUCKET_DURATION_MS), + getInputStreamForResource(R.raw.netstats_uid_v4)) + } + + val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS) + NetworkStatsDataMigrationUtils.readPlatformCollection(builder, + getInputStreamForResource(R.raw.netstats_uid_v16)) + // The values are obtained by dumping from NetworkStatsCollection that + // read by the logic inside the service. + assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L) + } + + @Test + fun testMaybeReadLegacyUid() { + val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS) + NetworkStatsDataMigrationUtils.readLegacyUid(builder, + getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */) + assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L) + } + + private fun assertValues( + collection: NetworkStatsCollection, + expectedSize: Int, + expectedTxBytes: Long, + expectedTxPackets: Long, + expectedRxBytes: Long, + expectedRxPackets: Long + ) { + var txBytes = 0L + var txPackets = 0L + var rxBytes = 0L + var rxPackets = 0L + val entries = collection.entries + + for (history in entries.values) { + for (historyEntry in history.entries) { + txBytes += historyEntry.txBytes + txPackets += historyEntry.txPackets + rxBytes += historyEntry.rxBytes + rxPackets += historyEntry.rxPackets + } + } + if (expectedSize != entries.size || + expectedTxBytes != txBytes || + expectedTxPackets != txPackets || + expectedRxBytes != rxBytes || + expectedRxPackets != rxPackets) { + fail("expected size=$expectedSize" + + "txb=$expectedTxBytes txp=$expectedTxPackets " + + "rxb=$expectedRxBytes rxp=$expectedRxPackets bus was " + + "size=${entries.size} txb=$txBytes txp=$txPackets " + + "rxb=$rxBytes rxp=$rxPackets") + } + assertEquals(txBytes + rxBytes, collection.totalBytes) + } + + private fun getInputStreamForResource(resourceId: Int): DataInputStream { + return DataInputStream(InstrumentationRegistry.getContext() + .getResources().openRawResource(resourceId)) + } +}
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java index ca8cf07..e5e7ebc 100644 --- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java +++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.net.Network; import android.os.Build; import android.os.Bundle; import android.os.Parcel; @@ -123,6 +124,7 @@ fullInfo.setServiceType("_kitten._tcp"); fullInfo.setPort(4242); fullInfo.setHost(LOCALHOST); + fullInfo.setNetwork(new Network(123)); checkParcelable(fullInfo); NsdServiceInfo noHostInfo = new NsdServiceInfo(); @@ -172,6 +174,7 @@ assertEquals(original.getServiceType(), result.getServiceType()); assertEquals(original.getHost(), result.getHost()); assertTrue(original.getPort() == result.getPort()); + assertEquals(original.getNetwork(), result.getNetwork()); // Assert equality of attribute map. Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java index 960a9f1..943a559 100644 --- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java +++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -50,6 +50,7 @@ private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23; private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24; private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25; + private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26; @Test public void testDefaults() throws Exception { @@ -78,10 +79,13 @@ assertEquals(1360, p.maxMtu); assertFalse(p.areAuthParamsInline); assertFalse(p.isRestrictedToTestNetworks); + assertFalse(p.excludeLocalRoutes); + assertFalse(p.requiresInternetValidation); } private VpnProfile getSampleIkev2Profile(String key) { - final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */); + final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */, + false /* excludesLocalRoutes */, true /* requiresPlatformValidation */); p.name = "foo"; p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; @@ -129,8 +133,8 @@ @Test public void testParcelUnparcel() { if (isAtLeastT()) { - // excludeLocalRoutes is added in T. - assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 24); + // excludeLocalRoutes, requiresPlatformValidation were added in T. + assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 25); } else { assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23); } @@ -174,7 +178,8 @@ getEncodedDecodedIkev2ProfileMissingValues( ENCODED_INDEX_AUTH_PARAMS_INLINE, ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS, - ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE /* missingIndices */); + ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE, + ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */); assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes())); } @@ -194,14 +199,26 @@ public void testEncodeDecodeMissingExcludeLocalRoutes() { final String tooFewValues = getEncodedDecodedIkev2ProfileMissingValues( - ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE /* missingIndices */); + ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE, + ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */); - // Verify decoding without isRestrictedToTestNetworks defaults to false + // Verify decoding without excludeLocalRoutes defaults to false final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); assertFalse(decoded.excludeLocalRoutes); } @Test + public void testEncodeDecodeMissingRequiresValidation() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION /* missingIndices */); + + // Verify decoding without requiresValidation defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.requiresInternetValidation); + } + + @Test public void testEncodeDecodeLoginsNotSaved() { final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); profile.saveLogin = false;
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java new file mode 100644 index 0000000..ac21e77 --- /dev/null +++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -0,0 +1,86 @@ +/* + * 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; + +import static android.net.INetd.FIREWALL_CHAIN_DOZABLE; +import static android.net.INetd.FIREWALL_RULE_ALLOW; +import static android.net.INetd.PERMISSION_INTERNET; + +import static org.junit.Assume.assumeFalse; +import static org.mockito.Mockito.verify; + +import android.net.INetd; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.modules.utils.build.SdkLevel; +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.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(DevSdkIgnoreRunner.class) +@SmallTest [email protected](Build.VERSION_CODES.R) +public final class BpfNetMapsTest { + private static final String TAG = "BpfNetMapsTest"; + private static final int TEST_UID = 10086; + private static final int[] TEST_UIDS = {10002, 10003}; + private static final String IFNAME = "wlan0"; + private static final String CHAINNAME = "fw_dozable"; + private BpfNetMaps mBpfNetMaps; + + @Mock INetd mNetd; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mBpfNetMaps = new BpfNetMaps(mNetd); + } + + @Test + public void testBpfNetMapsBeforeT() throws Exception { + assumeFalse(SdkLevel.isAtLeastT()); + mBpfNetMaps.addNaughtyApp(TEST_UID); + verify(mNetd).bandwidthAddNaughtyApp(TEST_UID); + mBpfNetMaps.removeNaughtyApp(TEST_UID); + verify(mNetd).bandwidthRemoveNaughtyApp(TEST_UID); + mBpfNetMaps.addNiceApp(TEST_UID); + verify(mNetd).bandwidthAddNiceApp(TEST_UID); + mBpfNetMaps.removeNiceApp(TEST_UID); + verify(mNetd).bandwidthRemoveNiceApp(TEST_UID); + mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true); + verify(mNetd).firewallEnableChildChain(FIREWALL_CHAIN_DOZABLE, true); + mBpfNetMaps.replaceUidChain(CHAINNAME, true, TEST_UIDS); + verify(mNetd).firewallReplaceUidChain(CHAINNAME, true, TEST_UIDS); + mBpfNetMaps.setUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW); + verify(mNetd).firewallSetUidRule(FIREWALL_CHAIN_DOZABLE, TEST_UID, FIREWALL_RULE_ALLOW); + mBpfNetMaps.addUidInterfaceRules(IFNAME, TEST_UIDS); + verify(mNetd).firewallAddUidInterfaceRules(IFNAME, TEST_UIDS); + mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS); + verify(mNetd).firewallRemoveUidInterfaceRules(TEST_UIDS); + mBpfNetMaps.swapActiveStatsMap(); + verify(mNetd).trafficSwapActiveStatsMap(); + mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS); + verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS); + } +}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 8d91552..0132525 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -23,6 +23,7 @@ import static android.Manifest.permission.DUMP; import static android.Manifest.permission.GET_INTENT_SENDER_INTENT; import static android.Manifest.permission.LOCAL_MAC_ADDRESS; +import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.Manifest.permission.NETWORK_FACTORY; import static android.Manifest.permission.NETWORK_SETTINGS; import static android.Manifest.permission.NETWORK_STACK; @@ -51,6 +52,7 @@ import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE; +import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK; import static android.net.ConnectivityManager.TYPE_ETHERNET; import static android.net.ConnectivityManager.TYPE_MOBILE; import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; @@ -100,12 +102,14 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM; import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP; +import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; import static android.net.NetworkCapabilities.REDACT_NONE; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; @@ -250,6 +254,7 @@ import android.net.NetworkTestResultParcelable; import android.net.OemNetworkPreferences; import android.net.PacProxyManager; +import android.net.ProfileNetworkPreference; import android.net.Proxy; import android.net.ProxyInfo; import android.net.QosCallbackException; @@ -259,6 +264,7 @@ import android.net.RouteInfo; import android.net.RouteInfoParcel; import android.net.SocketKeepalive; +import android.net.TelephonyNetworkSpecifier; import android.net.TransportInfo; import android.net.UidRange; import android.net.UidRangeParcel; @@ -329,6 +335,7 @@ import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo; import com.android.server.ConnectivityService.NetworkRequestInfo; import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces; +import com.android.server.connectivity.CarrierPrivilegeAuthenticator; import com.android.server.connectivity.ConnectivityFlags; import com.android.server.connectivity.MockableSystemProperties; import com.android.server.connectivity.Nat464Xlat; @@ -336,6 +343,7 @@ import com.android.server.connectivity.NetworkNotificationManager.NotificationType; import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; +import com.android.server.connectivity.UidRangeUtils; import com.android.server.connectivity.Vpn; import com.android.server.connectivity.VpnProfileStore; import com.android.server.net.NetworkPinner; @@ -348,6 +356,7 @@ import com.android.testutils.TestableNetworkOfferCallback; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -441,6 +450,10 @@ private static final int TEST_WORK_PROFILE_USER_ID = 2; private static final int TEST_WORK_PROFILE_APP_UID = UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID); + private static final int TEST_APP_ID_2 = 104; + private static final int TEST_WORK_PROFILE_APP_UID_2 = + UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2); + private static final String CLAT_PREFIX = "v4-"; private static final String MOBILE_IFNAME = "test_rmnet_data0"; private static final String CLAT_MOBILE_IFNAME = CLAT_PREFIX + MOBILE_IFNAME; @@ -490,6 +503,8 @@ private TestNetworkCallback mSystemDefaultNetworkCallback; private TestNetworkCallback mProfileDefaultNetworkCallback; private TestNetworkCallback mTestPackageDefaultNetworkCallback; + private TestNetworkCallback mProfileDefaultNetworkCallbackAsAppUid2; + private TestNetworkCallback mTestPackageDefaultNetworkCallback2; // State variables required to emulate NetworkPolicyManagerService behaviour. private int mBlockedReasons = BLOCKED_REASON_NONE; @@ -515,6 +530,8 @@ @Mock SystemConfigManager mSystemConfigManager; @Mock Resources mResources; @Mock PacProxyManager mPacProxyManager; + @Mock BpfNetMaps mBpfNetMaps; + @Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator; // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the // underlying binder calls. @@ -956,8 +973,6 @@ * @param hasInternet Indicate if network should pretend to have NET_CAPABILITY_INTERNET. */ public void connect(boolean validated, boolean hasInternet, boolean isStrictMode) { - assertFalse(getNetworkCapabilities().hasCapability(NET_CAPABILITY_INTERNET)); - ConnectivityManager.NetworkCallback callback = null; final ConditionVariable validatedCv = new ConditionVariable(); if (validated) { @@ -1479,6 +1494,10 @@ r -> new UidRangeParcel(r.start, r.stop)).toArray(UidRangeParcel[]::new); } + private UidRangeParcel[] intToUidRangeStableParcels(final @NonNull Set<Integer> ranges) { + return ranges.stream().map(r -> new UidRangeParcel(r, r)).toArray(UidRangeParcel[]::new); + } + private VpnManagerService makeVpnManagerService() { final VpnManagerService.Dependencies deps = new VpnManagerService.Dependencies() { public int getCallingUid() { @@ -1841,6 +1860,12 @@ } @Override + public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator( + @NonNull final Context context, @NonNull final TelephonyManager tm) { + return SdkLevel.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null; + } + + @Override public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) { return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b)); } @@ -1933,6 +1958,11 @@ return super.isFeatureEnabled(context, name, defaultEnabled); } } + + @Override + public BpfNetMaps getBpfNetMaps(INetd netd) { + return mBpfNetMaps; + } } private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) { @@ -3802,14 +3832,14 @@ public void testNoMutableNetworkRequests() throws Exception { final PendingIntent pendingIntent = PendingIntent.getBroadcast( mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); - NetworkRequest request1 = new NetworkRequest.Builder() + final NetworkRequest request1 = new NetworkRequest.Builder() .addCapability(NET_CAPABILITY_VALIDATED) .build(); - NetworkRequest request2 = new NetworkRequest.Builder() + final NetworkRequest request2 = new NetworkRequest.Builder() .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL) .build(); - Class<IllegalArgumentException> expected = IllegalArgumentException.class; + final Class<IllegalArgumentException> expected = IllegalArgumentException.class; assertThrows(expected, () -> mCm.requestNetwork(request1, new NetworkCallback())); assertThrows(expected, () -> mCm.requestNetwork(request1, pendingIntent)); assertThrows(expected, () -> mCm.requestNetwork(request2, new NetworkCallback())); @@ -3817,6 +3847,36 @@ } @Test + public void testNoAccessUidsInNetworkRequests() throws Exception { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + final NetworkRequest r = new NetworkRequest.Builder().build(); + final ArraySet<Integer> accessUids = new ArraySet<>(); + accessUids.add(6); + accessUids.add(9); + r.networkCapabilities.setAccessUids(accessUids); + + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + final NetworkCallback cb = new NetworkCallback(); + + final Class<IllegalArgumentException> expected = IllegalArgumentException.class; + assertThrows(expected, () -> mCm.requestNetwork(r, cb)); + assertThrows(expected, () -> mCm.requestNetwork(r, pendingIntent)); + assertThrows(expected, () -> mCm.registerNetworkCallback(r, cb)); + assertThrows(expected, () -> mCm.registerNetworkCallback(r, cb, handler)); + assertThrows(expected, () -> mCm.registerNetworkCallback(r, pendingIntent)); + assertThrows(expected, () -> mCm.registerBestMatchingNetworkCallback(r, cb, handler)); + + // Make sure that resetting the access UIDs to the empty set will allow calling + // requestNetwork and registerNetworkCallback. + r.networkCapabilities.setAccessUids(Collections.emptySet()); + mCm.requestNetwork(r, cb); + mCm.unregisterNetworkCallback(cb); + mCm.registerNetworkCallback(r, cb); + mCm.unregisterNetworkCallback(cb); + } + + @Test public void testMMSonWiFi() throws Exception { // Test bringing up cellular without MMS NetworkRequest gets reaped mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); @@ -5716,6 +5776,22 @@ } } + /** + * Validate the callback flow CBS request without carrier privilege. + */ + @Test + public void testCBSRequestWithoutCarrierPrivilege() throws Exception { + final NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_CBS).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + + mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED); + // Now file the test request and expect it. + mCm.requestNetwork(nr, networkCallback); + networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null); + mCm.unregisterNetworkCallback(networkCallback); + } + private static class TestKeepaliveCallback extends PacketKeepaliveCallback { public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR } @@ -10063,7 +10139,7 @@ // A connected VPN should have interface rules set up. There are two expected invocations, // one during the VPN initial connection, one during the VPN LinkProperties update. ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); - verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture()); assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange)); @@ -10072,7 +10148,7 @@ waitForIdle(); // Disconnected VPN should have interface rules removed - verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0")); } @@ -10089,7 +10165,7 @@ assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); // Legacy VPN should not have interface rules set up - verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any()); } @Test @@ -10105,7 +10181,7 @@ assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); // IPv6 unreachable route should not be misinterpreted as a default route - verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any()); } @Test @@ -10122,33 +10198,33 @@ // Connected VPN should have interface rules set up. There are two expected invocations, // one during VPN uid update, one during VPN LinkProperties update ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); - verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture()); assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); - reset(mMockNetd); - InOrder inOrder = inOrder(mMockNetd); + reset(mBpfNetMaps); + InOrder inOrder = inOrder(mBpfNetMaps); lp.setInterfaceName("tun1"); mMockVpn.sendLinkProperties(lp); waitForIdle(); // VPN handover (switch to a new interface) should result in rules being updated (old rules // removed first, then new rules added) - inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + inOrder.verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); - inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + inOrder.verify(mBpfNetMaps).addUidInterfaceRules(eq("tun1"), uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); - reset(mMockNetd); + reset(mBpfNetMaps); lp = new LinkProperties(); lp.setInterfaceName("tun1"); lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun1")); mMockVpn.sendLinkProperties(lp); waitForIdle(); // VPN not routing everything should no longer have interface filtering rules - verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); - reset(mMockNetd); + reset(mBpfNetMaps); lp = new LinkProperties(); lp.setInterfaceName("tun1"); lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); @@ -10156,7 +10232,7 @@ mMockVpn.sendLinkProperties(lp); waitForIdle(); // Back to routing all IPv6 traffic should have filtering rules - verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun1"), uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); } @@ -10185,8 +10261,8 @@ mMockVpn.establish(lp, VPN_UID, vpnRanges); assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); - reset(mMockNetd); - InOrder inOrder = inOrder(mMockNetd); + reset(mBpfNetMaps); + InOrder inOrder = inOrder(mBpfNetMaps); // Update to new range which is old range minus APP1, i.e. only APP2 final Set<UidRange> newRanges = new HashSet<>(asList( @@ -10197,9 +10273,9 @@ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); // Verify old rules are removed before new rules are added - inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + inOrder.verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); - inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + inOrder.verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), uidCaptor.capture()); assertContainsExactly(uidCaptor.getValue(), APP2_UID); } @@ -12245,6 +12321,8 @@ private void registerDefaultNetworkCallbacks() { if (mSystemDefaultNetworkCallback != null || mDefaultNetworkCallback != null || mProfileDefaultNetworkCallback != null + || mProfileDefaultNetworkCallbackAsAppUid2 != null + || mTestPackageDefaultNetworkCallback2 != null || mTestPackageDefaultNetworkCallback != null) { throw new IllegalStateException("Default network callbacks already registered"); } @@ -12255,12 +12333,18 @@ mDefaultNetworkCallback = new TestNetworkCallback(); mProfileDefaultNetworkCallback = new TestNetworkCallback(); mTestPackageDefaultNetworkCallback = new TestNetworkCallback(); + mProfileDefaultNetworkCallbackAsAppUid2 = new TestNetworkCallback(); + mTestPackageDefaultNetworkCallback2 = new TestNetworkCallback(); mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback, new Handler(ConnectivityThread.getInstanceLooper())); mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback); registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback, TEST_WORK_PROFILE_APP_UID); registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback, TEST_PACKAGE_UID); + registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallbackAsAppUid2, + TEST_WORK_PROFILE_APP_UID_2); + registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback2, + TEST_PACKAGE_UID2); // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well. mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); } @@ -12278,6 +12362,12 @@ if (null != mTestPackageDefaultNetworkCallback) { mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback); } + if (null != mProfileDefaultNetworkCallbackAsAppUid2) { + mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallbackAsAppUid2); + } + if (null != mTestPackageDefaultNetworkCallback2) { + mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback2); + } } private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest( @@ -13706,10 +13796,34 @@ } private UidRangeParcel[] uidRangeFor(final UserHandle handle) { - UidRange range = UidRange.createForUser(handle); + final UidRange range = UidRange.createForUser(handle); return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) }; } + private UidRangeParcel[] uidRangeFor(final UserHandle handle, + ProfileNetworkPreference profileNetworkPreference) { + final Set<UidRange> uidRangeSet; + UidRange range = UidRange.createForUser(handle); + if (profileNetworkPreference.getIncludedUids().size() != 0) { + uidRangeSet = UidRangeUtils.convertListToUidRange( + profileNetworkPreference.getIncludedUids()); + } else if (profileNetworkPreference.getExcludedUids().size() != 0) { + uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange( + range, UidRangeUtils.convertListToUidRange( + profileNetworkPreference.getExcludedUids())); + } else { + uidRangeSet = new ArraySet<>(); + uidRangeSet.add(range); + } + UidRangeParcel[] uidRangeParcels = new UidRangeParcel[uidRangeSet.size()]; + int i = 0; + for (UidRange range1 : uidRangeSet) { + uidRangeParcels[i] = new UidRangeParcel(range1.start, range1.stop); + i++; + } + return uidRangeParcels; + } + private static class TestOnCompleteListener implements Runnable { final class OnComplete {} final ArrayTrackRecord<OnComplete>.ReadHead mHistory = @@ -13732,6 +13846,14 @@ return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc); } + private TestNetworkAgentWrapper makeEnterpriseNetworkAgent(int enterpriseId) throws Exception { + final NetworkCapabilities workNc = new NetworkCapabilities(); + workNc.addCapability(NET_CAPABILITY_ENTERPRISE); + workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + workNc.addEnterpriseId(enterpriseId); + return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc); + } + private TestNetworkCallback mEnterpriseCallback; private UserHandle setupEnterpriseNetwork() { final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); @@ -13755,72 +13877,116 @@ } /** - * Make sure per-profile networking preference behaves as expected when the enterprise network - * goes up and down while the preference is active. Make sure they behave as expected whether - * there is a general default network or not. + * Make sure per profile network preferences behave as expected for a given + * profile network preference. */ - @Test - public void testPreferenceForUserNetworkUpDown() throws Exception { + public void testPreferenceForUserNetworkUpDownForGivenPreference( + ProfileNetworkPreference profileNetworkPreference, + boolean connectWorkProfileAgentAhead, + UserHandle testHandle, + TestNetworkCallback profileDefaultNetworkCallback, + TestNetworkCallback disAllowProfileDefaultNetworkCallback) throws Exception { final InOrder inOrder = inOrder(mMockNetd); - final UserHandle testHandle = setupEnterpriseNetwork(); - registerDefaultNetworkCallbacks(); mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); mCellNetworkAgent.connect(true); mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); - mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + profileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks( + mCellNetworkAgent); + } inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + final TestNetworkAgentWrapper workAgent = + makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId()); + if (connectWorkProfileAgentAhead) { + workAgent.connect(false); + } final TestOnCompleteListener listener = new TestOnCompleteListener(); - mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + mCm.setProfileNetworkPreferences(testHandle, List.of(profileNetworkPreference), r -> r.run(), listener); listener.expectOnComplete(); - - // Setting a network preference for this user will create a new set of routing rules for - // the UID range that corresponds to this user, so as to define the default network - // for these apps separately. This is true because the multi-layer request relevant to - // this UID range contains a TRACK_DEFAULT, so the range will be moved through UID-specific - // rules to the correct network – in this case the system default network. The case where - // the default network for the profile happens to be the same as the system default - // is not handled specially, the rules are always active as long as a preference is set. - inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( - mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle), - PREFERENCE_ORDER_PROFILE)); + boolean allowFallback = true; + if (profileNetworkPreference.getPreference() + == PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK) { + allowFallback = false; + } + if (allowFallback && !connectWorkProfileAgentAhead) { + // Setting a network preference for this user will create a new set of routing rules for + // the UID range that corresponds to this user, inorder to define the default network + // for these apps separately. This is true because the multi-layer request relevant to + // this UID range contains a TRACK_DEFAULT, so the range will be moved through + // UID-specific rules to the correct network – in this case the system default network. + // The case where the default network for the profile happens to be the same as the + // system default is not handled specially, the rules are always active as long as + // a preference is set. + inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( + mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle, profileNetworkPreference), + PREFERENCE_ORDER_PROFILE)); + } // The enterprise network is not ready yet. - assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, - mProfileDefaultNetworkCallback); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + if (allowFallback && !connectWorkProfileAgentAhead) { + assertNoCallbacks(profileDefaultNetworkCallback); + } else if (!connectWorkProfileAgentAhead) { + profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } + } - final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); - workAgent.connect(false); + if (!connectWorkProfileAgentAhead) { + workAgent.connect(false); + } - mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent); + profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.assertNoCallback(); + } mSystemDefaultNetworkCallback.assertNoCallback(); mDefaultNetworkCallback.assertNoCallback(); inOrder.verify(mMockNetd).networkCreate( nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM)); inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( - workAgent.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE)); - inOrder.verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig( - mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle), + workAgent.getNetwork().netId, + uidRangeFor(testHandle, profileNetworkPreference), PREFERENCE_ORDER_PROFILE)); + if (allowFallback && !connectWorkProfileAgentAhead) { + inOrder.verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig( + mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle, profileNetworkPreference), + PREFERENCE_ORDER_PROFILE)); + } + // Make sure changes to the work agent send callbacks to the app in the work profile, but // not to the other apps. workAgent.setNetworkValid(true /* isStrictMode */); workAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); - mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, + profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED) - && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)); + && nc.hasCapability(NET_CAPABILITY_ENTERPRISE) + && nc.hasEnterpriseId( + profileNetworkPreference.getPreferenceEnterpriseId()) + && nc.getEnterpriseIds().length == 1); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); - mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc -> + profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc -> nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); // Conversely, change a capability on the system-wide default network and make sure @@ -13830,7 +13996,11 @@ nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); - mProfileDefaultNetworkCallback.assertNoCallback(); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + } + profileDefaultNetworkCallback.assertNoCallback(); // Disconnect and reconnect the system-wide default network and make sure that the // apps on this network see the appropriate callbacks, and the app on the work profile @@ -13838,32 +14008,55 @@ mCellNetworkAgent.disconnect(); mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); - mProfileDefaultNetworkCallback.assertNoCallback(); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.expectCallback( + CallbackEntry.LOST, mCellNetworkAgent); + } + profileDefaultNetworkCallback.assertNoCallback(); inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId); mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); mCellNetworkAgent.connect(true); mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); - mProfileDefaultNetworkCallback.assertNoCallback(); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks( + mCellNetworkAgent); + + } + profileDefaultNetworkCallback.assertNoCallback(); inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); // When the agent disconnects, test that the app on the work profile falls back to the // default network. workAgent.disconnect(); - mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent); - mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent); + if (allowFallback) { + profileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); - inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( - mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle), - PREFERENCE_ORDER_PROFILE)); + if (allowFallback) { + inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( + mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle, profileNetworkPreference), + PREFERENCE_ORDER_PROFILE)); + } inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId); mCellNetworkAgent.disconnect(); mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); - mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + if (disAllowProfileDefaultNetworkCallback != null) { + disAllowProfileDefaultNetworkCallback.expectCallback( + CallbackEntry.LOST, mCellNetworkAgent); + } + if (allowFallback) { + profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + } // Waiting for the handler to be idle before checking for networkDestroy is necessary // here because ConnectivityService calls onLost before the network is fully torn down. @@ -13873,38 +14066,321 @@ // If the control comes here, callbacks seem to behave correctly in the presence of // a default network when the enterprise network goes up and down. Now, make sure they // also behave correctly in the absence of a system-wide default network. - final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(); + final TestNetworkAgentWrapper workAgent2 = + makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId()); workAgent2.connect(false); - mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2); + profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM)); inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig( - workAgent2.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE)); + workAgent2.getNetwork().netId, + uidRangeFor(testHandle, profileNetworkPreference), PREFERENCE_ORDER_PROFILE)); workAgent2.setNetworkValid(true /* isStrictMode */); workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid()); - mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2, + profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2, nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE) - && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + && nc.hasEnterpriseId( + profileNetworkPreference.getPreferenceEnterpriseId()) + && nc.getEnterpriseIds().length == 1); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); inOrder.verify(mMockNetd, never()).networkAddUidRangesParcel(any()); - // When the agent disconnects, test that the app on the work profile falls back to the + // When the agent disconnects, test that the app on the work profile fall back to the // default network. workAgent2.disconnect(); - mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2); + profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2); + if (disAllowProfileDefaultNetworkCallback != null) { + assertNoCallbacks(disAllowProfileDefaultNetworkCallback); + } assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId); assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, - mProfileDefaultNetworkCallback); + profileDefaultNetworkCallback); // Callbacks will be unregistered by tearDown() } /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not. + */ + @Test + public void testPreferenceForUserNetworkUpDown() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + registerDefaultNetworkCallbacks(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), false, + testHandle, mProfileDefaultNetworkCallback, null); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not when configured to not fallback to default network. + */ + @Test + public void testPreferenceForUserNetworkUpDownWithNoFallback() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), false, + testHandle, mProfileDefaultNetworkCallback, null); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not when configured to not fallback to default network + * along with already connected enterprise work agent + */ + @Test + public void testPreferenceForUserNetworkUpDownWithNoFallbackWithAlreadyConnectedWorkAgent() + throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), true, testHandle, + mProfileDefaultNetworkCallback, null); + } + + /** + * Make sure per-profile networking preference for specific uid of test handle + * behaves as expected + */ + @Test + public void testPreferenceForDefaultUidOfTestHandle() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + profileNetworkPreferenceBuilder.setIncludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID))); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), false, testHandle, + mProfileDefaultNetworkCallback, null); + } + + /** + * Make sure per-profile networking preference for specific uid of test handle + * behaves as expected + */ + @Test + public void testPreferenceForSpecificUidOfOnlyOneApp() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + profileNetworkPreferenceBuilder.setIncludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), false, + testHandle, mProfileDefaultNetworkCallbackAsAppUid2, null); + } + + /** + * Make sure per-profile networking preference for specific uid of test handle + * behaves as expected + */ + @Test + public void testPreferenceForDisallowSpecificUidOfApp() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + profileNetworkPreferenceBuilder.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), false, + testHandle, mProfileDefaultNetworkCallback, + mProfileDefaultNetworkCallbackAsAppUid2); + } + + /** + * Make sure per-profile networking preference for specific uid of test handle + * invalid uid inputs + */ + @Test + public void testPreferenceForInvalidUids() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + profileNetworkPreferenceBuilder.setExcludedUids( + List.of(testHandle.getUid(0) - 1)); + final TestOnCompleteListener listener = new TestOnCompleteListener(); + Assert.assertThrows(IllegalArgumentException.class, () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build()), + r -> r.run(), listener)); + + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setIncludedUids( + List.of(testHandle.getUid(0) - 1)); + Assert.assertThrows(IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build()), + r -> r.run(), listener)); + + + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setIncludedUids( + List.of(testHandle.getUid(0) - 1)); + profileNetworkPreferenceBuilder.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + Assert.assertThrows(IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build()), + r -> r.run(), listener)); + + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + profileNetworkPreferenceBuilder2.setIncludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + profileNetworkPreferenceBuilder.setIncludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + Assert.assertThrows(IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build(), + profileNetworkPreferenceBuilder2.build()), + r -> r.run(), listener)); + + profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder2.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + profileNetworkPreferenceBuilder.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + Assert.assertThrows(IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build(), + profileNetworkPreferenceBuilder2.build()), + r -> r.run(), listener)); + + profileNetworkPreferenceBuilder2.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder2.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + profileNetworkPreferenceBuilder.setExcludedUids( + List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2))); + Assert.assertThrows(IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreferences( + testHandle, List.of(profileNetworkPreferenceBuilder.build(), + profileNetworkPreferenceBuilder2.build()), + r -> r.run(), listener)); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not when configured to fallback to default network + * along with already connected enterprise work agent + */ + @Test + public void testPreferenceForUserNetworkUpDownWithFallbackWithAlreadyConnectedWorkAgent() + throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), true, + testHandle, mProfileDefaultNetworkCallback, + null); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active for a given enterprise identifier + */ + @Test + public void testPreferenceForUserNetworkUpDownWithDefaultEnterpriseId() + throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), true, + testHandle, mProfileDefaultNetworkCallback, + null); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active for a given enterprise identifier + */ + @Test + public void testPreferenceForUserNetworkUpDownWithId2() + throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId( + NetworkCapabilities.NET_ENTERPRISE_ID_2); + registerDefaultNetworkCallbacks(); + testPreferenceForUserNetworkUpDownForGivenPreference( + profileNetworkPreferenceBuilder.build(), true, + testHandle, mProfileDefaultNetworkCallback, null); + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active for a given enterprise identifier + */ + @Test + public void testPreferenceForUserNetworkUpDownWithInvalidId() + throws Exception { + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(0); + registerDefaultNetworkCallbacks(); + assertThrows("Should not be able to set invalid enterprise id", + IllegalStateException.class, () -> profileNetworkPreferenceBuilder.build()); + } + + /** * Test that, in a given networking context, calling setPreferenceForUser to set per-profile * defaults on then off works as expected. */ @@ -14054,10 +14530,16 @@ public void testProfileNetworkPrefWrongPreference() throws Exception { final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); mServiceContext.setWorkProfile(testHandle, true); + ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder = + new ProfileNetworkPreference.Builder(); + profileNetworkPreferenceBuilder.setPreference( + PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1); + profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1); assertThrows("Should not be able to set an illegal preference", IllegalArgumentException.class, - () -> mCm.setProfileNetworkPreference(testHandle, - PROFILE_NETWORK_PREFERENCE_ENTERPRISE + 1, null, null)); + () -> mCm.setProfileNetworkPreferences(testHandle, + List.of(profileNetworkPreferenceBuilder.build()), + null, null)); } /** @@ -14138,6 +14620,189 @@ () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback())); } + @Test + public void testAccessUids() throws Exception { + final int preferenceOrder = + ConnectivityService.PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT; + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED); + final TestNetworkCallback cb = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .build(), + cb); + + final ArraySet<Integer> uids = new ArraySet<>(); + uids.add(200); + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_TEST) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .setAccessUids(uids) + .build(); + final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(TRANSPORT_TEST, + new LinkProperties(), nc); + agent.connect(true); + cb.expectAvailableThenValidatedCallbacks(agent); + + final InOrder inOrder = inOrder(mMockNetd); + final NativeUidRangeConfig uids200Parcel = new NativeUidRangeConfig( + agent.getNetwork().getNetId(), + intToUidRangeStableParcels(uids), + preferenceOrder); + if (SdkLevel.isAtLeastT()) { + inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids200Parcel); + } + + uids.add(300); + uids.add(400); + nc.setAccessUids(uids); + agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids)); + } else { + cb.assertNoCallback(); + } + + uids.remove(200); + final NativeUidRangeConfig uids300400Parcel = new NativeUidRangeConfig( + agent.getNetwork().getNetId(), + intToUidRangeStableParcels(uids), + preferenceOrder); + if (SdkLevel.isAtLeastT()) { + inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids300400Parcel); + } + + nc.setAccessUids(uids); + agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids)); + inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids200Parcel); + } else { + cb.assertNoCallback(); + } + + uids.clear(); + uids.add(600); + nc.setAccessUids(uids); + agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().equals(uids)); + } else { + cb.assertNoCallback(); + } + final NativeUidRangeConfig uids600Parcel = new NativeUidRangeConfig( + agent.getNetwork().getNetId(), + intToUidRangeStableParcels(uids), + preferenceOrder); + if (SdkLevel.isAtLeastT()) { + inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids600Parcel); + inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids300400Parcel); + } + + uids.clear(); + nc.setAccessUids(uids); + agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(agent, caps -> caps.getAccessUids().isEmpty()); + inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids600Parcel); + } else { + cb.assertNoCallback(); + verify(mMockNetd, never()).networkAddUidRangesParcel(any()); + verify(mMockNetd, never()).networkRemoveUidRangesParcel(any()); + } + + } + + @Test + public void testCbsAccessUids() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED); + + // In this test TEST_PACKAGE_UID will be the UID of the carrier service UID. + doReturn(true).when(mCarrierPrivilegeAuthenticator) + .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any()); + + final ArraySet<Integer> serviceUidSet = new ArraySet<>(); + serviceUidSet.add(TEST_PACKAGE_UID); + final ArraySet<Integer> nonServiceUidSet = new ArraySet<>(); + nonServiceUidSet.add(TEST_PACKAGE_UID2); + final ArraySet<Integer> serviceUidSetPlus = new ArraySet<>(); + serviceUidSetPlus.add(TEST_PACKAGE_UID); + serviceUidSetPlus.add(TEST_PACKAGE_UID2); + + final TestNetworkCallback cb = new TestNetworkCallback(); + + // Simulate a restricted telephony network. The telephony factory is entitled to set + // the access UID to the service package on any of its restricted networks. + final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */)); + + // Cell gets to set the service UID as access UID + mCm.requestNetwork(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(), cb); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, + new LinkProperties(), ncb.build()); + mCellNetworkAgent.connect(true); + cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + ncb.setAccessUids(serviceUidSet); + mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(mCellNetworkAgent, + caps -> caps.getAccessUids().equals(serviceUidSet)); + } else { + // S must ignore access UIDs. + cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS); + } + + // ...but not to some other UID. Rejection sets UIDs to the empty set + ncb.setAccessUids(nonServiceUidSet); + mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */); + if (SdkLevel.isAtLeastT()) { + cb.expectCapabilitiesThat(mCellNetworkAgent, + caps -> caps.getAccessUids().isEmpty()); + } else { + // S must ignore access UIDs. + cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS); + } + + // ...and also not to multiple UIDs even including the service UID + ncb.setAccessUids(serviceUidSetPlus); + mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */); + cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS); + + mCellNetworkAgent.disconnect(); + cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mCm.unregisterNetworkCallback(cb); + + // Must be unset before touching the transports, because remove and add transport types + // check the specifier on the builder immediately, contradicting normal builder semantics + // TODO : fix the builder + ncb.setNetworkSpecifier(null); + ncb.removeTransportType(TRANSPORT_CELLULAR); + ncb.addTransportType(TRANSPORT_WIFI); + // Wifi does not get to set access UID, even to the correct UID + mCm.requestNetwork(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(), cb); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, + new LinkProperties(), ncb.build()); + mWiFiNetworkAgent.connect(true); + cb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + ncb.setAccessUids(serviceUidSet); + mWiFiNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */); + cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS); + mCm.unregisterNetworkCallback(cb); + } + /** * Validate request counts are counted accurately on setProfileNetworkPreference on set/replace. */
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java index ea29da0..0c58582 100644 --- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java +++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -32,6 +33,7 @@ import android.annotation.NonNull; import android.content.Context; +import android.net.ConnectivityManager; import android.net.INetd; import android.net.INetdUnsolicitedEventListener; import android.net.LinkAddress; @@ -71,6 +73,7 @@ public class NetworkManagementServiceTest { private NetworkManagementService mNMService; @Mock private Context mContext; + @Mock private ConnectivityManager mCm; @Mock private IBatteryStats.Stub mBatteryStatsService; @Mock private INetd.Stub mNetdService; @@ -113,6 +116,9 @@ MockitoAnnotations.initMocks(this); doNothing().when(mNetdService) .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture()); + doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName( + eq(ConnectivityManager.class)); + doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE)); // Start the service and wait until it connects to our socket. mNMService = NetworkManagementService.create(mContext, mDeps); } @@ -239,6 +245,7 @@ mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true); assertTrue("Should be true since mobile data usage is restricted", mNMService.isNetworkRestricted(TEST_UID)); + verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* enabled */); mNMService.setDataSaverModeEnabled(true); verify(mNetdService).bandwidthEnableDataSaver(true); @@ -246,13 +253,16 @@ mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false); assertTrue("Should be true since data saver is on and the uid is not allowlisted", mNMService.isNetworkRestricted(TEST_UID)); + verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* false */); mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true); assertFalse("Should be false since data saver is on and the uid is allowlisted", mNMService.isNetworkRestricted(TEST_UID)); + verify(mCm).updateMeteredNetworkAllowList(TEST_UID, true /* enabled */); // remove uid from allowlist and turn datasaver off again mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false); + verify(mCm).updateMeteredNetworkAllowList(TEST_UID, false /* enabled */); mNMService.setDataSaverModeEnabled(false); verify(mNetdService).bandwidthEnableDataSaver(false); assertFalse("Network should not be restricted when data saver is off", @@ -306,12 +316,14 @@ for (int chain : chains) { final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain); mNMService.setFirewallChainEnabled(chain, true); + verify(mCm).setFirewallChainEnabled(chain, true /* enabled */); for (int state : states) { mNMService.setFirewallUidRule(chain, TEST_UID, state); assertEquals(errorMsg.apply(chain, state), expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID)); } mNMService.setFirewallChainEnabled(chain, false); + verify(mCm).setFirewallChainEnabled(chain, false /* enabled */); } } }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java index 6d1d765..5086943 100644 --- a/tests/unit/java/com/android/server/NsdServiceTest.java +++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -218,14 +218,14 @@ client.discoverServices("a_type", PROTOCOL, listener2); waitForIdle(); verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommand("discover 3 a_type"); + verifyDaemonCommand("discover 3 a_type 0"); // Client resolve request NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); client.resolveService(request, listener3); waitForIdle(); verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommand("resolve 4 a_name a_type local."); + verifyDaemonCommand("resolve 4 a_name a_type local. 0"); // Client disconnects, stop the daemon after CLEANUP_DELAY_MS. deathRecipient.binderDied();
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java new file mode 100644 index 0000000..553cb83 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -0,0 +1,247 @@ +/* + * 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.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED; + +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.TelephonyNetworkSpecifier; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.networkstack.apishim.TelephonyManagerShimImpl; +import com.android.networkstack.apishim.common.TelephonyManagerShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.util.Collections; +import java.util.List; + +/** + * Tests for CarrierPrivilegeAuthenticatorTest. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.CarrierPrivilegeAuthenticatorTest + */ +@RunWith(DevSdkIgnoreRunner.class) +@IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available +public class CarrierPrivilegeAuthenticatorTest { + // TODO : use ConstantsShim.RECEIVER_NOT_EXPORTED when it's available in tests. + private static final int RECEIVER_NOT_EXPORTED = 4; + private static final int SUBSCRIPTION_COUNT = 2; + private static final int TEST_SUBSCRIPTION_ID = 1; + + @NonNull private final Context mContext; + @NonNull private final TelephonyManager mTelephonyManager; + @NonNull private final TelephonyManagerShimImpl mTelephonyManagerShim; + @NonNull private final PackageManager mPackageManager; + @NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator; + private final int mCarrierConfigPkgUid = 12345; + private final String mTestPkg = "com.android.server.connectivity.test"; + + public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator { + TestCarrierPrivilegeAuthenticator(@NonNull final Context c, + @NonNull final TelephonyManager t) { + super(c, t, mTelephonyManagerShim); + } + @Override + protected int getSlotIndex(int subId) { + if (SubscriptionManager.DEFAULT_SUBSCRIPTION_ID == subId) return TEST_SUBSCRIPTION_ID; + return subId; + } + } + + public CarrierPrivilegeAuthenticatorTest() { + mContext = mock(Context.class); + mTelephonyManager = mock(TelephonyManager.class); + mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class); + mPackageManager = mock(PackageManager.class); + } + + @Before + public void setUp() throws Exception { + doReturn(SUBSCRIPTION_COUNT).when(mTelephonyManager).getActiveModemCount(); + doReturn(mTestPkg).when(mTelephonyManagerShim) + .getCarrierServicePackageNameForLogicalSlot(anyInt()); + doReturn(mPackageManager).when(mContext).getPackageManager(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = mCarrierConfigPkgUid; + doReturn(applicationInfo).when(mPackageManager) + .getApplicationInfo(eq(mTestPkg), anyInt()); + mCarrierPrivilegeAuthenticator = + new TestCarrierPrivilegeAuthenticator(mContext, mTelephonyManager); + } + + private IntentFilter getIntentFilter() { + final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class); + verify(mContext).registerReceiver(any(), captor.capture(), any(), any(), anyInt()); + return captor.getValue(); + } + + private List<TelephonyManagerShim.CarrierPrivilegesListenerShim> + getCarrierPrivilegesListeners() { + final ArgumentCaptor<TelephonyManagerShim.CarrierPrivilegesListenerShim> captor = + ArgumentCaptor.forClass(TelephonyManagerShim.CarrierPrivilegesListenerShim.class); + try { + verify(mTelephonyManagerShim, atLeastOnce()) + .addCarrierPrivilegesListener(anyInt(), any(), captor.capture()); + } catch (UnsupportedApiLevelException e) { + } + return captor.getAllValues(); + } + + private Intent buildTestMultiSimConfigBroadcastIntent() { + final Intent intent = new Intent(ACTION_MULTI_SIM_CONFIG_CHANGED); + return intent; + } + @Test + public void testConstructor() throws Exception { + verify(mContext).registerReceiver( + eq(mCarrierPrivilegeAuthenticator), + any(IntentFilter.class), + any(), + any(), + eq(RECEIVER_NOT_EXPORTED)); + final IntentFilter filter = getIntentFilter(); + assertEquals(1, filter.countActions()); + assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED)); + + verify(mTelephonyManagerShim, times(2)) + .addCarrierPrivilegesListener(anyInt(), any(), any()); + verify(mTelephonyManagerShim) + .addCarrierPrivilegesListener(eq(0), any(), any()); + verify(mTelephonyManagerShim) + .addCarrierPrivilegesListener(eq(1), any(), any()); + assertEquals(2, getCarrierPrivilegesListeners().size()); + + final TelephonyNetworkSpecifier telephonyNetworkSpecifier = + new TelephonyNetworkSpecifier(0); + final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder(); + networkRequestBuilder.addTransportType(TRANSPORT_CELLULAR); + networkRequestBuilder.setNetworkSpecifier(telephonyNetworkSpecifier); + + assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid + 1, networkRequestBuilder.build().networkCapabilities)); + } + + @Test + public void testMultiSimConfigChanged() throws Exception { + doReturn(1).when(mTelephonyManager).getActiveModemCount(); + final List<TelephonyManagerShim.CarrierPrivilegesListenerShim> carrierPrivilegesListeners = + getCarrierPrivilegesListeners(); + + mCarrierPrivilegeAuthenticator.onReceive( + mContext, buildTestMultiSimConfigBroadcastIntent()); + for (TelephonyManagerShim.CarrierPrivilegesListenerShim carrierPrivilegesListener + : carrierPrivilegesListeners) { + verify(mTelephonyManagerShim) + .removeCarrierPrivilegesListener(eq(carrierPrivilegesListener)); + } + + // Expect a new CarrierPrivilegesListener to have been registered for slot 0, and none other + // (2 previously registered during startup, for slots 0 & 1) + verify(mTelephonyManagerShim, times(3)) + .addCarrierPrivilegesListener(anyInt(), any(), any()); + verify(mTelephonyManagerShim, times(2)) + .addCarrierPrivilegesListener(eq(0), any(), any()); + + final TelephonyNetworkSpecifier telephonyNetworkSpecifier = + new TelephonyNetworkSpecifier(0); + final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder(); + networkRequestBuilder.addTransportType(TRANSPORT_CELLULAR); + networkRequestBuilder.setNetworkSpecifier(telephonyNetworkSpecifier); + assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid + 1, networkRequestBuilder.build().networkCapabilities)); + } + + @Test + public void testOnCarrierPrivilegesChanged() throws Exception { + final TelephonyManagerShim.CarrierPrivilegesListenerShim listener = + getCarrierPrivilegesListeners().get(0); + + final TelephonyNetworkSpecifier telephonyNetworkSpecifier = + new TelephonyNetworkSpecifier(0); + final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder(); + networkRequestBuilder.addTransportType(TRANSPORT_CELLULAR); + networkRequestBuilder.setNetworkSpecifier(telephonyNetworkSpecifier); + + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = mCarrierConfigPkgUid + 1; + doReturn(applicationInfo).when(mPackageManager) + .getApplicationInfo(eq(mTestPkg), anyInt()); + listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {}); + + assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid + 1, networkRequestBuilder.build().networkCapabilities)); + } + + @Test + public void testDefaultSubscription() throws Exception { + final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder(); + networkRequestBuilder.addTransportType(TRANSPORT_CELLULAR); + assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + + networkRequestBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0)); + assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + + // The builder for NetworkRequest doesn't allow removing the transport as long as a + // specifier is set, so unset it first. TODO : fix the builder + networkRequestBuilder.setNetworkSpecifier((NetworkSpecifier) null); + networkRequestBuilder.removeTransportType(TRANSPORT_CELLULAR); + networkRequestBuilder.addTransportType(TRANSPORT_WIFI); + networkRequestBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0)); + assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities( + mCarrierConfigPkgUid, networkRequestBuilder.build().networkCapabilities)); + } +}
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..84e02ce --- /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 [email protected](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 is already running 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/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java index c86e699..ec51537 100644 --- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java +++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -296,7 +296,7 @@ false /* roaming */); verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); } @Test @@ -315,7 +315,7 @@ // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); } @Test @@ -334,7 +334,7 @@ // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); } @Test @@ -351,7 +351,7 @@ // Default global setting should be used: 12 - 7 = 5 verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any()); } @Test @@ -366,7 +366,7 @@ false /* roaming */); verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); // Update setting setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14)); @@ -376,7 +376,7 @@ // Callback must have been re-registered with new setting verify(mStatsManager, times(1)).unregisterUsageCallback(any()); verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); } @Test @@ -391,7 +391,7 @@ false /* roaming */); verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes)) .thenReturn((int) DataUnit.MEGABYTES.toBytes(16)); @@ -402,6 +402,6 @@ // Uses the new setting (16 - 2 = 14MB) verify(mStatsManager, times(1)).registerUsageCallback( - any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any()); + any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any()); } }
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java index 99ef80b..6590543 100644 --- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -89,6 +89,7 @@ import androidx.test.filters.SmallTest; import com.android.net.module.util.CollectionUtils; +import com.android.server.BpfNetMaps; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; @@ -146,9 +147,11 @@ @Mock private UserManager mUserManager; @Mock private PermissionMonitor.Dependencies mDeps; @Mock private SystemConfigManager mSystemConfigManager; + @Mock private BpfNetMaps mBpfNetMaps; private PermissionMonitor mPermissionMonitor; private NetdMonitor mNetdMonitor; + private BpfMapMonitor mBpfMapMonitor; @Before public void setUp() throws Exception { @@ -177,8 +180,9 @@ // by default. doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt(); - mPermissionMonitor = new PermissionMonitor(mContext, mNetdService, mDeps); + mPermissionMonitor = new PermissionMonitor(mContext, mNetdService, mBpfNetMaps, mDeps); mNetdMonitor = new NetdMonitor(mNetdService); + mBpfMapMonitor = new BpfMapMonitor(mBpfNetMaps); doReturn(List.of()).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt()); } @@ -511,9 +515,37 @@ assertBackgroundPermission(true, MOCK_PACKAGE2, MOCK_UID12, NETWORK_STACK); } + private class BpfMapMonitor { + private final SparseIntArray mAppIdsTrafficPermission = new SparseIntArray(); + private static final int DOES_NOT_EXIST = -2; + + BpfMapMonitor(BpfNetMaps mockBpfmap) throws Exception { + // Add hook to verify and track result of trafficSetNetPerm. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final int permission = (int) args[0]; + for (final int appId : (int[]) args[1]) { + mAppIdsTrafficPermission.put(appId, permission); + } + return null; + }).when(mockBpfmap).setNetPermForUids(anyInt(), any(int[].class)); + } + + public void expectTrafficPerm(int permission, int... appIds) { + for (final int appId : appIds) { + if (mAppIdsTrafficPermission.get(appId, DOES_NOT_EXIST) == DOES_NOT_EXIST) { + fail("appId " + appId + " does not exist."); + } + if (mAppIdsTrafficPermission.get(appId) != permission) { + fail("appId " + appId + " has wrong permission: " + + mAppIdsTrafficPermission.get(appId)); + } + } + } + } + private class NetdMonitor { private final SparseIntArray mUidsNetworkPermission = new SparseIntArray(); - private final SparseIntArray mAppIdsTrafficPermission = new SparseIntArray(); private static final int DOES_NOT_EXIST = -2; NetdMonitor(INetd mockNetd) throws Exception { @@ -545,16 +577,6 @@ } return null; }).when(mockNetd).networkClearPermissionForUser(any(int[].class)); - - // Add hook to verify and track result of trafficSetNetPerm. - doAnswer((InvocationOnMock invocation) -> { - final Object[] args = invocation.getArguments(); - final int permission = (int) args[0]; - for (final int appId : (int[]) args[1]) { - mAppIdsTrafficPermission.put(appId, permission); - } - return null; - }).when(mockNetd).trafficSetNetPermForUids(anyInt(), any(int[].class)); } public void expectNetworkPerm(int permission, UserHandle[] users, int... appIds) { @@ -581,18 +603,6 @@ } } } - - public void expectTrafficPerm(int permission, int... appIds) { - for (final int appId : appIds) { - if (mAppIdsTrafficPermission.get(appId, DOES_NOT_EXIST) == DOES_NOT_EXIST) { - fail("appId " + appId + " does not exist."); - } - if (mAppIdsTrafficPermission.get(appId) != permission) { - fail("appId " + appId + " has wrong permission: " - + mAppIdsTrafficPermission.get(appId)); - } - } - } } @Test @@ -702,30 +712,30 @@ // When VPN is connected, expect a rule to be set up for user app MOCK_UID11 mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID); - verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); - reset(mNetdService); + reset(mBpfNetMaps); // When MOCK_UID11 package is uninstalled and reinstalled, expect Netd to be updated mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11); - verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[]{MOCK_UID11})); + verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11})); mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11); - verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); - reset(mNetdService); + reset(mBpfNetMaps); // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the // old UID rules then adds the new ones. Expect netd to be updated mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID); - verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID11})); + verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11})); mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID); - verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12})); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12})); - reset(mNetdService); + reset(mBpfNetMaps); // When VPN is disconnected, expect rules to be torn down mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID); - verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID12})); + verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12})); assertNull(mPermissionMonitor.getVpnUidRanges("tun0")); } @@ -744,13 +754,13 @@ // Newly-installed package should have uid rules added addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1); - verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); - verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21})); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11})); + verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21})); // Removed package should have its uid rules removed mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11); - verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[]{MOCK_UID11})); - verify(mNetdService, never()).firewallRemoveUidInterfaceRules(aryEq(new int[]{MOCK_UID21})); + verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11})); + verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21})); } @@ -784,91 +794,91 @@ // Send the permission information to netd, expect permission updated. mPermissionMonitor.sendAppIdsTrafficPermission(netdPermissionsAppIds); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID2); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, SYSTEM_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, SYSTEM_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, SYSTEM_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, SYSTEM_APPID2); // Update permission of MOCK_APPID1, expect new permission show up. mPermissionMonitor.sendPackagePermissionsForAppId(MOCK_APPID1, PERMISSION_TRAFFIC_ALL); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); // Change permissions of SYSTEM_APPID2, expect new permission show up and old permission // revoked. mPermissionMonitor.sendPackagePermissionsForAppId(SYSTEM_APPID2, PERMISSION_INTERNET); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, SYSTEM_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, SYSTEM_APPID2); // Revoke permission from SYSTEM_APPID1, expect no permission stored. mPermissionMonitor.sendPackagePermissionsForAppId(SYSTEM_APPID1, PERMISSION_NONE); - mNetdMonitor.expectTrafficPerm(PERMISSION_NONE, SYSTEM_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, SYSTEM_APPID1); } @Test public void testPackageInstall() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); addPackage(MOCK_PACKAGE2, MOCK_UID12, INTERNET); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID2); } @Test public void testPackageInstallSharedUid() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); // Install another package with the same uid and no permissions should not cause the app id // to lose permissions. addPackage(MOCK_PACKAGE2, MOCK_UID11); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); } @Test public void testPackageUninstallBasic() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{}); mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11); - mNetdMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); } @Test public void testPackageRemoveThenAdd() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{}); mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11); - mNetdMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); } @Test public void testPackageUpdate() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11); - mNetdMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1); addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); } @Test public void testPackageUninstallWithMultiplePackages() throws Exception { addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); // Install another package with the same uid but different permissions. addPackage(MOCK_PACKAGE2, MOCK_UID11, INTERNET); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_UID11); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_UID11); // Uninstall MOCK_PACKAGE1 and expect only INTERNET permission left. when(mPackageManager.getPackagesForUid(eq(MOCK_UID11))) .thenReturn(new String[]{MOCK_PACKAGE2}); mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); } @Test @@ -876,7 +886,8 @@ // Use the real context as this test must ensure the *real* system package holds the // necessary permission. final Context realContext = InstrumentationRegistry.getContext(); - final PermissionMonitor monitor = new PermissionMonitor(realContext, mNetdService); + final PermissionMonitor monitor = new PermissionMonitor(realContext, mNetdService, + mBpfNetMaps); final PackageManager manager = realContext.getPackageManager(); final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME, GET_PERMISSIONS | MATCH_ANY_USER); @@ -891,8 +902,8 @@ .thenReturn(new int[]{ MOCK_UID12 }); mPermissionMonitor.startMonitoring(); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID2); } private BroadcastReceiver expectBroadcastReceiver(String... actions) { @@ -923,7 +934,7 @@ buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); receiver.onReceive(mContext, addedIntent); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); // Verify receiving PACKAGE_REMOVED intent. when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{}); @@ -931,7 +942,7 @@ Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID11); receiver.onReceive(mContext, removedIntent); - mNetdMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); } private ContentObserver expectRegisterContentObserver(Uri expectedUri) { @@ -1070,7 +1081,7 @@ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt()); mPermissionMonitor.startMonitoring(); mNetdMonitor.expectNoNetworkPerm(new UserHandle[]{MOCK_USER1}, MOCK_APPID1, MOCK_APPID2); - mNetdMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1, MOCK_APPID2); final BroadcastReceiver receiver = expectBroadcastReceiver( Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); @@ -1087,8 +1098,8 @@ MOCK_APPID1); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, MOCK_APPID2); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID2); } @Test @@ -1114,8 +1125,8 @@ MOCK_APPID1); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, MOCK_APPID2); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID2); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID2); } @Test @@ -1128,7 +1139,7 @@ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt()); mPermissionMonitor.startMonitoring(); mNetdMonitor.expectNoNetworkPerm(new UserHandle[]{MOCK_USER1}, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1); final BroadcastReceiver receiver = expectBroadcastReceiver( Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); @@ -1140,7 +1151,7 @@ receiver.onReceive(mContext, externalIntent); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID1); } @Test @@ -1155,7 +1166,7 @@ mPermissionMonitor.startMonitoring(); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); final BroadcastReceiver receiver = expectBroadcastReceiver( Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); @@ -1169,7 +1180,7 @@ receiver.onReceive(mContext, externalIntent); mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); - mNetdMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); + mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); } @Test
diff --git a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java new file mode 100644 index 0000000..b8c2673 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
@@ -0,0 +1,354 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.UidRange; +import android.os.Build; +import android.util.ArraySet; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Tests for UidRangeUtils. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.UidRangeUtilsTest + */ +@RunWith(DevSdkIgnoreRunner.class) [email protected](Build.VERSION_CODES.R) +public class UidRangeUtilsTest { + private static void assertInSameRange(@NonNull final String msg, + @Nullable final UidRange r1, + @Nullable final Set<UidRange> s2) { + assertTrue(msg + " : " + s2 + " unexpectedly is not in range of " + r1, + UidRangeUtils.isRangeSetInUidRange(r1, s2)); + } + + private static void assertNotInSameRange(@NonNull final String msg, + @Nullable final UidRange r1, @Nullable final Set<UidRange> s2) { + assertFalse(msg + " : " + s2 + " unexpectedly is in range of " + r1, + UidRangeUtils.isRangeSetInUidRange(r1, s2)); + } + + @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + public void testRangeSetInUidRange() { + final UidRange uids1 = new UidRange(1, 100); + final UidRange uids2 = new UidRange(3, 300); + final UidRange uids3 = new UidRange(1, 1000); + final UidRange uids4 = new UidRange(1, 100); + final UidRange uids5 = new UidRange(2, 20); + final UidRange uids6 = new UidRange(3, 30); + + assertThrows(NullPointerException.class, + () -> UidRangeUtils.isRangeSetInUidRange(null, null)); + assertThrows(NullPointerException.class, + () -> UidRangeUtils.isRangeSetInUidRange(uids1, null)); + + final ArraySet<UidRange> set1 = new ArraySet<>(); + final ArraySet<UidRange> set2 = new ArraySet<>(); + + assertThrows(NullPointerException.class, + () -> UidRangeUtils.isRangeSetInUidRange(null, set1)); + assertInSameRange("uids1 <=> empty", uids1, set2); + + set2.add(uids1); + assertInSameRange("uids1 <=> uids1", uids1, set2); + + set2.clear(); + set2.add(uids2); + assertNotInSameRange("uids1 <=> uids2", uids1, set2); + set2.clear(); + set2.add(uids3); + assertNotInSameRange("uids1 <=> uids3", uids1, set2); + set2.clear(); + set2.add(uids4); + assertInSameRange("uids1 <=> uids4", uids1, set2); + + set2.clear(); + set2.add(uids5); + set2.add(uids6); + assertInSameRange("uids1 <=> uids5, 6", uids1, set2); + + set2.clear(); + set2.add(uids2); + set2.add(uids6); + assertNotInSameRange("uids1 <=> uids2, 6", uids1, set2); + } + + @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + public void testRemoveRangeSetFromUidRange() { + final UidRange uids1 = new UidRange(1, 100); + final UidRange uids2 = new UidRange(3, 300); + final UidRange uids3 = new UidRange(1, 1000); + final UidRange uids4 = new UidRange(1, 100); + final UidRange uids5 = new UidRange(2, 20); + final UidRange uids6 = new UidRange(3, 30); + final UidRange uids7 = new UidRange(30, 39); + + final UidRange uids8 = new UidRange(1, 1); + final UidRange uids9 = new UidRange(21, 100); + final UidRange uids10 = new UidRange(1, 2); + final UidRange uids11 = new UidRange(31, 100); + + final UidRange uids12 = new UidRange(1, 1); + final UidRange uids13 = new UidRange(21, 29); + final UidRange uids14 = new UidRange(40, 100); + + final UidRange uids15 = new UidRange(3, 30); + final UidRange uids16 = new UidRange(31, 39); + + assertThrows(NullPointerException.class, + () -> UidRangeUtils.removeRangeSetFromUidRange(null, null)); + Set<UidRange> expected = new ArraySet<>(); + expected.add(uids1); + assertThrows(NullPointerException.class, + () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, null)); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, new ArraySet<>())); + + expected.clear(); + final ArraySet<UidRange> set2 = new ArraySet<>(); + set2.add(uids1); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + set2.clear(); + set2.add(uids4); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.add(uids10); + set2.clear(); + set2.add(uids2); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.clear(); + set2.clear(); + set2.add(uids3); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + set2.clear(); + set2.add(uids3); + set2.add(uids6); + assertThrows(IllegalArgumentException.class, + () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.clear(); + expected.add(uids8); + expected.add(uids9); + set2.clear(); + set2.add(uids5); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.clear(); + expected.add(uids10); + expected.add(uids11); + set2.clear(); + set2.add(uids6); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.clear(); + expected.add(uids12); + expected.add(uids13); + expected.add(uids14); + set2.clear(); + set2.add(uids5); + set2.add(uids7); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + + expected.clear(); + expected.add(uids10); + expected.add(uids14); + set2.clear(); + set2.add(uids15); + set2.add(uids16); + assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2)); + } + + private static void assertRangeOverlaps(@NonNull final String msg, + @Nullable final Set<UidRange> s1, + @Nullable final Set<UidRange> s2) { + assertTrue(msg + " : " + s2 + " unexpectedly does not overlap with " + s1, + UidRangeUtils.doesRangeSetOverlap(s1, s2)); + } + + private static void assertRangeDoesNotOverlap(@NonNull final String msg, + @Nullable final Set<UidRange> s1, @Nullable final Set<UidRange> s2) { + assertFalse(msg + " : " + s2 + " unexpectedly ovelaps with " + s1, + UidRangeUtils.doesRangeSetOverlap(s1, s2)); + } + + @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + public void testRangeSetOverlap() { + final UidRange uids1 = new UidRange(1, 100); + final UidRange uids2 = new UidRange(3, 300); + final UidRange uids3 = new UidRange(1, 1000); + final UidRange uids4 = new UidRange(1, 100); + final UidRange uids5 = new UidRange(2, 20); + final UidRange uids6 = new UidRange(3, 30); + final UidRange uids7 = new UidRange(0, 0); + final UidRange uids8 = new UidRange(1, 500); + final UidRange uids9 = new UidRange(101, 200); + + assertThrows(NullPointerException.class, + () -> UidRangeUtils.doesRangeSetOverlap(null, null)); + + final ArraySet<UidRange> set1 = new ArraySet<>(); + final ArraySet<UidRange> set2 = new ArraySet<>(); + assertThrows(NullPointerException.class, + () -> UidRangeUtils.doesRangeSetOverlap(set1, null)); + assertThrows(NullPointerException.class, + () -> UidRangeUtils.doesRangeSetOverlap(null, set2)); + assertRangeDoesNotOverlap("empty <=> null", set1, set2); + + set2.add(uids1); + set1.add(uids1); + assertRangeOverlaps("uids1 <=> uids1", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids2); + assertRangeOverlaps("uids1 <=> uids2", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids3); + assertRangeOverlaps("uids1 <=> uids3", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids4); + assertRangeOverlaps("uids1 <=> uids4", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids5); + set2.add(uids6); + assertRangeOverlaps("uids1 <=> uids5,6", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids7); + assertRangeDoesNotOverlap("uids1 <=> uids7", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids9); + assertRangeDoesNotOverlap("uids1 <=> uids9", set1, set2); + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids8); + assertRangeOverlaps("uids1 <=> uids8", set1, set2); + + + set1.clear(); + set1.add(uids1); + set2.clear(); + set2.add(uids8); + set2.add(uids7); + assertRangeOverlaps("uids1 <=> uids7, 8", set1, set2); + } + + @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + public void testConvertListToUidRange() { + final UidRange uids1 = new UidRange(1, 1); + final UidRange uids2 = new UidRange(1, 2); + final UidRange uids3 = new UidRange(100, 100); + final UidRange uids4 = new UidRange(10, 10); + + final UidRange uids5 = new UidRange(10, 14); + final UidRange uids6 = new UidRange(20, 24); + + final Set<UidRange> expected = new ArraySet<>(); + final List<Integer> input = new ArrayList<Integer>(); + + assertThrows(NullPointerException.class, () -> UidRangeUtils.convertListToUidRange(null)); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.add(1); + expected.add(uids1); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.add(2); + expected.clear(); + expected.add(uids2); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.clear(); + input.add(1); + input.add(100); + expected.clear(); + expected.add(uids1); + expected.add(uids3); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.clear(); + input.add(100); + input.add(1); + expected.clear(); + expected.add(uids1); + expected.add(uids3); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.clear(); + input.add(100); + input.add(1); + input.add(2); + input.add(1); + input.add(10); + expected.clear(); + expected.add(uids2); + expected.add(uids4); + expected.add(uids3); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + + input.clear(); + input.add(10); + input.add(11); + input.add(12); + input.add(13); + input.add(14); + input.add(20); + input.add(21); + input.add(22); + input.add(23); + input.add(24); + expected.clear(); + expected.add(uids5); + expected.add(uids6); + assertEquals(expected, UidRangeUtils.convertListToUidRange(input)); + } +}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java new file mode 100644 index 0000000..987b7b7 --- /dev/null +++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -0,0 +1,118 @@ +/* + * 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.net; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.INetd; +import android.net.MacAddress; +import android.os.Handler; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.net.module.util.IBpfMap; +import com.android.net.module.util.InterfaceParams; +import com.android.net.module.util.Struct.U32; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class BpfInterfaceMapUpdaterTest { + private static final int TEST_INDEX = 1; + private static final int TEST_INDEX2 = 2; + private static final String TEST_INTERFACE_NAME = "test1"; + private static final String TEST_INTERFACE_NAME2 = "test2"; + + private final TestLooper mLooper = new TestLooper(); + private BaseNetdUnsolicitedEventListener mListener; + private BpfInterfaceMapUpdater mUpdater; + @Mock private IBpfMap<U32, InterfaceMapValue> mBpfMap; + @Mock private INetd mNetd; + @Mock private Context mContext; + + private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies { + @Override + public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() { + return mBpfMap; + } + + @Override + public InterfaceParams getInterfaceParams(String ifaceName) { + if (ifaceName.equals(TEST_INTERFACE_NAME)) { + return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX, + MacAddress.ALL_ZEROS_ADDRESS); + } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) { + return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2, + MacAddress.ALL_ZEROS_ADDRESS); + } + + return null; + } + + @Override + public INetd getINetd(Context ctx) { + return mNetd; + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME}); + mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()), + new TestDependencies()); + } + + private void verifyStartUpdater() throws Exception { + mUpdater.start(); + mLooper.dispatchAll(); + final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor = + ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class); + verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture()); + mListener = listenerCaptor.getValue(); + verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX)), + eq(new InterfaceMapValue(TEST_INTERFACE_NAME))); + } + + @Test + public void testUpdateInterfaceMap() throws Exception { + verifyStartUpdater(); + + mListener.onInterfaceAdded(TEST_INTERFACE_NAME2); + mLooper.dispatchAll(); + verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX2)), + eq(new InterfaceMapValue(TEST_INTERFACE_NAME2))); + + // Check that when onInterfaceRemoved is called, nothing happens. + mListener.onInterfaceRemoved(TEST_INTERFACE_NAME); + mLooper.dispatchAll(); + verifyNoMoreInteractions(mBpfMap); + } +}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java index 8340a13..6872f80 100644 --- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java +++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -28,13 +28,16 @@ import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; -import static com.android.server.NetworkManagementSocketTagger.kernelToTag; +import static com.android.server.net.NetworkStatsFactory.kernelToTag; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import android.content.Context; import android.content.res.Resources; -import android.net.INetd; +import android.net.ConnectivityManager; import android.net.NetworkStats; import android.net.TrafficStats; import android.net.UnderlyingNetworkInfo; @@ -73,8 +76,8 @@ private File mTestProc; private NetworkStatsFactory mFactory; - @Mock - private INetd mNetd; + @Mock private Context mContext; + @Mock private ConnectivityManager mCm; @Before public void setUp() throws Exception { @@ -85,7 +88,10 @@ // applications. So in order to have a test support native library, the native code // related to networkStatsFactory is compiled to a minimal native library and loaded here. System.loadLibrary("networkstatsfactorytestjni"); - mFactory = new NetworkStatsFactory(mTestProc, false, mNetd); + doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName( + eq(ConnectivityManager.class)); + doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE)); + mFactory = new NetworkStatsFactory(mContext, mTestProc, false); mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]); }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java index 416549c..66dcf6d 100644 --- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java +++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -29,25 +29,22 @@ import static android.net.TrafficStats.MB_IN_BYTES; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; -import android.app.usage.NetworkStatsManager; import android.net.DataUsageRequest; import android.net.NetworkIdentity; import android.net.NetworkIdentitySet; import android.net.NetworkStats; import android.net.NetworkStatsAccess; import android.net.NetworkTemplate; -import android.os.Build; -import android.os.ConditionVariable; -import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; -import android.os.Messenger; import android.os.Process; import android.os.UserHandle; import android.telephony.TelephonyManager; @@ -55,7 +52,6 @@ import androidx.test.filters.SmallTest; -import com.android.server.net.NetworkStatsServiceTest.LatchedHandler; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.HandlerUtils; @@ -74,7 +70,7 @@ */ @RunWith(DevSdkIgnoreRunner.class) @SmallTest [email protected](Build.VERSION_CODES.S) [email protected](SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public class NetworkStatsObserversTest { private static final String TEST_IFACE = "test0"; private static final String TEST_IFACE2 = "test1"; @@ -96,21 +92,15 @@ private static final long WAIT_TIMEOUT_MS = 500; private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES; private static final long BASE_BYTES = 7 * MB_IN_BYTES; - private static final int INVALID_TYPE = -1; - - private long mElapsedRealtime; private HandlerThread mObserverHandlerThread; - private Handler mObserverNoopHandler; - - private LatchedHandler mHandler; private NetworkStatsObservers mStatsObservers; - private Messenger mMessenger; private ArrayMap<String, NetworkIdentitySet> mActiveIfaces; private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces; - @Mock private IBinder mockBinder; + @Mock private IBinder mUsageCallbackBinder; + private TestableUsageCallback mUsageCallback; @Before public void setUp() throws Exception { @@ -126,24 +116,29 @@ } }; - mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable()); - mMessenger = new Messenger(mHandler); - mActiveIfaces = new ArrayMap<>(); mActiveUidIfaces = new ArrayMap<>(); + mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder); } @Test public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception { - long thresholdTooLowBytes = 1L; - DataUsageRequest inputRequest = new DataUsageRequest( + final long thresholdTooLowBytes = 1L; + final DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, - Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); - assertTrue(request.requestId > 0); - assertTrue(Objects.equals(sTemplateWifi, request.template)); - assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + final DataUsageRequest requestByApp = mStatsObservers.register(inputRequest, mUsageCallback, + UID_RED, NetworkStatsAccess.Level.DEVICE); + assertTrue(requestByApp.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, requestByApp.template)); + assertEquals(THRESHOLD_BYTES, requestByApp.thresholdInBytes); + + // Verify the threshold requested by system uid won't be overridden. + final DataUsageRequest requestBySystem = mStatsObservers.register(inputRequest, + mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(requestBySystem.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template)); + assertEquals(1, requestBySystem.thresholdInBytes); } @Test @@ -152,7 +147,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateWifi, request.template)); @@ -164,13 +159,13 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES); - DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request1 = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request1.requestId > 0); assertTrue(Objects.equals(sTemplateWifi, request1.template)); assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes); - DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request2 = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request2.requestId > request1.requestId); assertTrue(Objects.equals(sTemplateWifi, request2.template)); @@ -190,17 +185,19 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); - Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + Mockito.verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class), + anyInt()); mStatsObservers.unregister(request, Process.SYSTEM_UID); waitForObserverToIdle(); - Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + Mockito.verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), + anyInt()); } @Test @@ -208,17 +205,18 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); - Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + Mockito.verify(mUsageCallbackBinder) + .linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); mStatsObservers.unregister(request, UID_BLUE); waitForObserverToIdle(); - Mockito.verifyZeroInteractions(mockBinder); + Mockito.verifyZeroInteractions(mUsageCallbackBinder); } private NetworkIdentitySet makeTestIdentSet() { @@ -235,7 +233,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -259,7 +257,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -289,7 +287,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -312,7 +310,7 @@ mStatsObservers.updateStats( xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); waitForObserverToIdle(); - assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + mUsageCallback.expectOnThresholdReached(request); } @Test @@ -320,7 +318,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEFAULT); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -345,7 +343,7 @@ mStatsObservers.updateStats( xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); waitForObserverToIdle(); - assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + mUsageCallback.expectOnThresholdReached(request); } @Test @@ -353,7 +351,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, UID_BLUE, NetworkStatsAccess.Level.DEFAULT); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -385,7 +383,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, UID_BLUE, NetworkStatsAccess.Level.USER); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -410,7 +408,7 @@ mStatsObservers.updateStats( xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); waitForObserverToIdle(); - assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + mUsageCallback.expectOnThresholdReached(request); } @Test @@ -418,7 +416,7 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); - DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback, UID_RED, NetworkStatsAccess.Level.USER); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateImsi1, request.template)); @@ -447,6 +445,5 @@ private void waitForObserverToIdle() { HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS); - HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS); } }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java index 40587c5..76c0c38 100644 --- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java +++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -66,21 +66,22 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.app.AlarmManager; -import android.app.usage.NetworkStatsManager; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; @@ -101,13 +102,9 @@ import android.net.netstats.provider.INetworkStatsProviderCallback; import android.net.wifi.WifiInfo; import android.os.Build; -import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.Messenger; import android.os.PowerManager; import android.os.SimpleClock; import android.provider.Settings; @@ -117,8 +114,11 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.net.module.util.IBpfMap; +import com.android.net.module.util.LocationPermissionChecker; +import com.android.net.module.util.Struct.U32; +import com.android.net.module.util.Struct.U8; import com.android.server.net.NetworkStatsService.AlertObserver; import com.android.server.net.NetworkStatsService.NetworkStatsSettings; import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config; @@ -188,11 +188,17 @@ private @Mock TetheringManager mTetheringManager; private @Mock NetworkStatsFactory mStatsFactory; private @Mock NetworkStatsSettings mSettings; - private @Mock IBinder mBinder; + private @Mock IBinder mUsageCallbackBinder; + private TestableUsageCallback mUsageCallback; private @Mock AlarmManager mAlarmManager; @Mock private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor; + private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater; private HandlerThread mHandlerThread; + @Mock + private LocationPermissionChecker mLocationPermissionChecker; + private @Mock IBpfMap<U32, U8> mUidCounterSetMap; + private @Mock NetworkStatsService.TagStatsDeleter mTagStatsDeleter; private NetworkStatsService mService; private INetworkStatsSession mSession; @@ -262,7 +268,9 @@ MockitoAnnotations.initMocks(this); final Context context = InstrumentationRegistry.getContext(); mServiceContext = new MockContext(context); - when(sWifiInfo.getCurrentNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY); + when(mLocationPermissionChecker.checkCallersLocationPermission( + any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true); + when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY); mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName()); PowerManager powerManager = (PowerManager) mServiceContext.getSystemService( @@ -309,6 +317,8 @@ verify(mTetheringManager).registerTetheringEventCallback( any(), tetheringEventCbCaptor.capture()); mTetheringEventCallback = tetheringEventCbCaptor.getValue(); + + mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder); } @NonNull @@ -334,6 +344,26 @@ return mContentObserver = super.makeContentObserver(handler, settings, monitor); } + @Override + public LocationPermissionChecker makeLocationPermissionChecker(final Context context) { + return mLocationPermissionChecker; + } + + @Override + public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater( + @NonNull Context ctx, @NonNull Handler handler) { + return mBpfInterfaceMapUpdater; + } + + @Override + public IBpfMap<U32, U8> getUidCounterSetMap() { + return mUidCounterSetMap; + } + + @Override + public NetworkStatsService.TagStatsDeleter getTagStatsDeleter() { + return mTagStatsDeleter; + } }; } @@ -459,8 +489,11 @@ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L) .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L)); mService.setUidForeground(UID_RED, false); + verify(mUidCounterSetMap, never()).deleteEntry(any()); mService.incrementOperationCount(UID_RED, 0xFAAD, 4); mService.setUidForeground(UID_RED, true); + verify(mUidCounterSetMap).updateEntry( + eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND))); mService.incrementOperationCount(UID_RED, 0xFAAD, 6); forcePollAndWaitForIdle(); @@ -669,8 +702,10 @@ final Intent intent = new Intent(ACTION_UID_REMOVED); intent.putExtra(EXTRA_UID, UID_BLUE); mServiceContext.sendBroadcast(intent); + verify(mTagStatsDeleter).deleteTagData(UID_BLUE); intent.putExtra(EXTRA_UID, UID_RED); mServiceContext.sendBroadcast(intent); + verify(mTagStatsDeleter).deleteTagData(UID_RED); // existing uid and total should remain unchanged; but removed UID // should be gone completely. @@ -985,7 +1020,7 @@ } @Test - public void testDetailedUidStats() throws Exception { + public void testUidStatsForTransport() throws Exception { // pretend that network comes online expectDefaultSettings(); NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; @@ -1011,7 +1046,7 @@ .insertEntry(entry3)); mService.incrementOperationCount(UID_RED, 0xF00D, 1); - NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL); + NetworkStats stats = mService.getUidStatsForTransport(NetworkCapabilities.TRANSPORT_WIFI); assertEquals(3, stats.size()); entry1.operations = 1; @@ -1022,68 +1057,6 @@ } @Test - public void testDetailedUidStats_Filtered() throws Exception { - // pretend that network comes online - expectDefaultSettings(); - - final String stackedIface = "stacked-test0"; - final LinkProperties stackedProp = new LinkProperties(); - stackedProp.setInterfaceName(stackedIface); - final NetworkStateSnapshot wifiState = buildWifiState(); - wifiState.getLinkProperties().addStackedLink(stackedProp); - NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState}; - - expectNetworkStatsSummary(buildEmptyStats()); - expectNetworkStatsUidDetail(buildEmptyStats()); - - mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), - new UnderlyingNetworkInfo[0]); - - NetworkStats.Entry uidStats = new NetworkStats.Entry( - TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L); - // Stacked on matching interface - NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry( - stackedIface, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L); - TetherStatsParcel tetherStatsParcel1 = - buildTetherStatsParcel(stackedIface, 1024L, 8L, 512L, 4L, 0); - // Different interface - TetherStatsParcel tetherStatsParcel2 = - buildTetherStatsParcel("otherif", 1024L, 8L, 512L, 4L, 0); - - final String[] ifaceFilter = new String[] { TEST_IFACE }; - final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE }; - incrementCurrentTime(HOUR_IN_MILLIS); - expectDefaultSettings(); - expectNetworkStatsSummary(buildEmptyStats()); - when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter))) - .thenReturn(augmentedIfaceFilter); - when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL))) - .thenReturn(new NetworkStats(getElapsedRealtime(), 1) - .insertEntry(uidStats)); - final TetherStatsParcel[] tetherStatsParcels = {tetherStatsParcel1, tetherStatsParcel2}; - when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels); - - NetworkStats stats = mService.getDetailedUidStats(ifaceFilter); - - // mStatsFactory#readNetworkStatsDetail() has the following invocations: - // 1) NetworkStatsService#systemReady from #setUp. - // 2) mService#notifyNetworkStatus in the test above. - // - // Additionally, we should have one call from the above call to mService#getDetailedUidStats - // with the augmented ifaceFilter. - verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); - verify(mStatsFactory, times(1)).readNetworkStatsDetail( - eq(UID_ALL), - eq(augmentedIfaceFilter), - eq(TAG_ALL)); - assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE)); - assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface)); - assertEquals(2, stats.size()); - assertEquals(uidStats, stats.getValues(0, null)); - assertEquals(tetheredStats1, stats.getValues(1, null)); - } - - @Test public void testForegroundBackground() throws Exception { // pretend that network comes online expectDefaultSettings(); @@ -1119,6 +1092,8 @@ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L) .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L)); mService.setUidForeground(UID_RED, true); + verify(mUidCounterSetMap).updateEntry( + eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND))); mService.incrementOperationCount(UID_RED, 0xFAAD, 1); forcePollAndWaitForIdle(); @@ -1294,20 +1269,14 @@ DataUsageRequest inputRequest = new DataUsageRequest( DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes); - // Create a messenger that waits for callback activity - ConditionVariable cv = new ConditionVariable(false); - LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv); - Messenger messenger = new Messenger(latchedHandler); - // Force poll expectDefaultSettings(); expectNetworkStatsSummary(buildEmptyStats()); expectNetworkStatsUidDetail(buildEmptyStats()); // Register and verify request and that binder was called - DataUsageRequest request = - mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest, - messenger, mBinder); + DataUsageRequest request = mService.registerUsageCallback( + mServiceContext.getOpPackageName(), inputRequest, mUsageCallback); assertTrue(request.requestId > 0); assertTrue(Objects.equals(sTemplateWifi, request.template)); long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB @@ -1316,7 +1285,7 @@ HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); // Make sure that the caller binder gets connected - verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); // modify some number on wifi, and trigger poll event // not enough traffic to call data usage callback @@ -1331,7 +1300,7 @@ assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); // make sure callback has not being called - assertEquals(INVALID_TYPE, latchedHandler.lastMessageType); + mUsageCallback.assertNoCallback(); // and bump forward again, with counters going higher. this is // important, since it will trigger the data usage callback @@ -1346,23 +1315,21 @@ assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0); - // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED - assertTrue(cv.block(WAIT_TIMEOUT)); - assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType); - cv.close(); + // Wait for the caller to invoke expectOnThresholdReached. + mUsageCallback.expectOnThresholdReached(request); // Allow binder to disconnect - when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true); + when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())) + .thenReturn(true); // Unregister request mService.unregisterUsageRequest(request); - // Wait for the caller to ack receipt of CALLBACK_RELEASED - assertTrue(cv.block(WAIT_TIMEOUT)); - assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType); + // Wait for the caller to invoke expectOnCallbackReleased. + mUsageCallback.expectOnCallbackReleased(request); // Make sure that the caller binder gets disconnected - verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); } @Test @@ -1688,6 +1655,28 @@ provider.expectOnRequestStatsUpdate(0 /* unused */); } + /** + * Verify the service will throw exceptions if the template is location sensitive but + * the permission is not granted. + */ + @Test + public void testEnforceTemplateLocationPermission() throws Exception { + when(mLocationPermissionChecker.checkCallersLocationPermission( + any(), any(), anyInt(), anyBoolean(), any())).thenReturn(false); + initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1)); + assertThrows(SecurityException.class, () -> + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0)); + // Templates w/o wifi network keys can query stats as usual. + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0); + + when(mLocationPermissionChecker.checkCallersLocationPermission( + any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true); + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0); + } + private static File getBaseDir(File statsDir) { File baseDir = new File(statsDir, "netstats"); baseDir.mkdirs(); @@ -1703,7 +1692,8 @@ private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) throws Exception { // verify history API - final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL); + final NetworkStatsHistory history = + mSession.getHistoryIntervalForNetwork(template, FIELD_ALL, start, end); assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations); // verify summary API @@ -1915,21 +1905,4 @@ private void waitForIdle() { HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); } - - static class LatchedHandler extends Handler { - private final ConditionVariable mCv; - int lastMessageType = INVALID_TYPE; - - LatchedHandler(Looper looper, ConditionVariable cv) { - super(looper); - mCv = cv; - } - - @Override - public void handleMessage(Message msg) { - lastMessageType = msg.what; - mCv.open(); - super.handleMessage(msg); - } - } }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java index 43aeec6..0d34609 100644 --- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java +++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -35,8 +35,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.usage.NetworkStatsManager; import android.content.Context; -import android.net.NetworkTemplate; import android.os.Build; import android.os.Looper; import android.os.Parcel; @@ -282,7 +282,7 @@ // NETWORK_TYPE_5G_NSA. setRatTypeForSub(TEST_SUBID1, TelephonyManager.NETWORK_TYPE_LTE, OVERRIDE_NETWORK_TYPE_NR_NSA); - assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA); + assertRatTypeChangedForSub(TEST_IMSI1, NetworkStatsManager.NETWORK_TYPE_5G_NSA); reset(mDelegate); // Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE.
diff --git a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt new file mode 100644 index 0000000..1917ec3 --- /dev/null +++ b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
@@ -0,0 +1,79 @@ +/* + * 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.net + +import android.net.DataUsageRequest +import android.net.netstats.IUsageCallback +import android.os.IBinder +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.test.fail + +private const val DEFAULT_TIMEOUT_MS = 200L + +// TODO: Move the class to static libs once all downstream have IUsageCallback definition. +class TestableUsageCallback(private val binder: IBinder) : IUsageCallback.Stub() { + sealed class CallbackType(val request: DataUsageRequest) { + class OnThresholdReached(request: DataUsageRequest) : CallbackType(request) + class OnCallbackReleased(request: DataUsageRequest) : CallbackType(request) + } + + // TODO: Change to use ArrayTrackRecord once moved into to the module. + private val history = LinkedBlockingQueue<CallbackType>() + + override fun onThresholdReached(request: DataUsageRequest) { + history.add(CallbackType.OnThresholdReached(request)) + } + + override fun onCallbackReleased(request: DataUsageRequest) { + history.add(CallbackType.OnCallbackReleased(request)) + } + + fun expectOnThresholdReached(request: DataUsageRequest) { + expectCallback<CallbackType.OnThresholdReached>(request, DEFAULT_TIMEOUT_MS) + } + + fun expectOnCallbackReleased(request: DataUsageRequest) { + expectCallback<CallbackType.OnCallbackReleased>(request, DEFAULT_TIMEOUT_MS) + } + + @JvmOverloads + fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) { + val cb = history.poll(timeout, TimeUnit.MILLISECONDS) + cb?.let { fail("Expected no callback but got $cb") } + } + + // Expects a callback of the specified request on the specified network within the timeout. + // If no callback arrives, or a different callback arrives, fail. + private inline fun <reified T : CallbackType> expectCallback( + expectedRequest: DataUsageRequest, + timeoutMs: Long + ) { + history.poll(timeoutMs, TimeUnit.MILLISECONDS).let { + if (it !is T || it.request != expectedRequest) { + fail("Unexpected callback : $it," + + " expected ${T::class} with Request[$expectedRequest]") + } else { + it + } + } + } + + override fun asBinder(): IBinder { + return binder + } +} \ No newline at end of file
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp index fe971e7..04ba98f 100644 --- a/tests/unit/jni/Android.bp +++ b/tests/unit/jni/Android.bp
@@ -24,7 +24,28 @@ "libbpf_android", "liblog", "libnativehelper", - "libnetdbpf", "libnetdutils", + "libnetworkstats", + ], +} + +cc_library_shared { + name: "libandroid_net_frameworktests_util_jni", + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + srcs: [ + "android_net_frameworktests_util/onload.cpp", + ], + static_libs: [ + "libnet_utils_device_common_bpfjni", + "libtcutils", + ], + shared_libs: [ + "liblog", + "libnativehelper", ], }
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp new file mode 100644 index 0000000..06a3986 --- /dev/null +++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <nativehelper/JNIHelp.h> +#include "jni.h" + +#define LOG_TAG "NetFrameworkTestsJni" +#include <android/log.h> + +namespace android { + +int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); +int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name); + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (register_com_android_net_module_util_BpfMap(env, + "android/net/frameworktests/util/BpfMap") < 0) return JNI_ERR; + + if (register_com_android_net_module_util_TcUtils(env, + "android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} + +}; // namespace android
diff --git a/tests/unit/res/raw/netstats_uid_v16 b/tests/unit/res/raw/netstats_uid_v16 new file mode 100644 index 0000000..a6ee430 --- /dev/null +++ b/tests/unit/res/raw/netstats_uid_v16 Binary files differ