diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-02-09 08:06:14 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-02-09 08:06:14 +0000 |
commit | 4727ff0a95ffabb128f99f3bd86e238748d09dcd (patch) | |
tree | 85c885988c71872a6cfadca8ad4269a2bb04fa45 | |
parent | 9610048efd86c03918fd8e1490bc5f3141f1de1a (diff) | |
parent | 7256ac389c31e7e71ac86b016eaa909f055fa637 (diff) | |
download | net-android12-mainline-resolv-release.tar.gz |
Snap for 8164116 from 7256ac389c31e7e71ac86b016eaa909f055fa637 to mainline-resolv-releaseandroid-mainline-12.0.0_r94android12-mainline-resolv-release
Change-Id: I91e7293d68444fbe11f4f79ca31e892863612d55
32 files changed, 1883 insertions, 86 deletions
diff --git a/common/Android.bp b/common/Android.bp index 50cfb02b..047d51ee 100644 --- a/common/Android.bp +++ b/common/Android.bp @@ -111,21 +111,24 @@ java_library { name: "net-utils-device-common-bpf", srcs: [ "device/com/android/net/module/util/BpfMap.java", + "device/com/android/net/module/util/HexDump.java", + "device/com/android/net/module/util/IBpfMap.java", "device/com/android/net/module/util/JniUtil.java", + "device/com/android/net/module/util/Struct.java", + "device/com/android/net/module/util/TcUtils.java", ], - sdk_version: "system_current", + sdk_version: "module_current", min_sdk_version: "29", visibility: [ "//frameworks/libs/net/common/tests:__subpackages__", "//frameworks/libs/net/common/testutils:__subpackages__", "//packages/modules/Connectivity:__subpackages__", "//packages/modules/NetworkStack:__subpackages__", - ], - static_libs: [ - "net-utils-device-common-struct", + "//frameworks/base/services/core", ], libs: [ "androidx.annotation_annotation", + "framework-connectivity.stubs.module_lib", ], apex_available: [ "com.android.tethering", @@ -143,7 +146,7 @@ java_library { "device/com/android/net/module/util/Struct.java", "device/com/android/net/module/util/structs/*.java", ], - sdk_version: "system_current", + sdk_version: "module_current", min_sdk_version: "29", visibility: [ "//frameworks/libs/net/common/testutils:__subpackages__", @@ -155,6 +158,7 @@ java_library { ], libs: [ "androidx.annotation_annotation", + "framework-connectivity.stubs.module_lib", ], apex_available: [ "com.android.tethering", @@ -168,7 +172,7 @@ java_library { srcs: [ "device/com/android/net/module/util/netlink/*.java", ], - sdk_version: "system_current", + sdk_version: "module_current", min_sdk_version: "29", visibility: [ "//frameworks/libs/net/common/testutils:__subpackages__", @@ -180,6 +184,7 @@ java_library { ], libs: [ "androidx.annotation_annotation", + "framework-connectivity.stubs.module_lib", ], apex_available: [ "com.android.tethering", @@ -223,11 +228,12 @@ java_library { name: "net-utils-framework-common", srcs: [ ":net-utils-framework-common-srcs", - // TODO: avoid including all framework annotations as they end up in library users jars - // and need jarjaring - ":framework-annotations", ], - sdk_version: "system_current", + sdk_version: "module_current", + libs: [ + "framework-annotations-lib", + "framework-connectivity.stubs.module_lib", + ], jarjar_rules: "jarjar-rules-shared.txt", visibility: [ "//cts/tests/tests/net", @@ -246,6 +252,7 @@ java_library { "//frameworks/libs/net/common/tests:__subpackages__", "//frameworks/libs/net/common/device", "//packages/modules/Wifi/framework/tests:__subpackages__", + "//packages/apps/Settings", ], lint: { strict_updatability_linting: true }, } @@ -277,8 +284,10 @@ java_library { "framework-connectivity", ], visibility: [ + // TODO: remove after NetworkStatsService moves to the module. "//frameworks/base/services/net", "//packages/modules/Connectivity/tests:__subpackages__", + "//packages/modules/Bluetooth/android/app", ], lint: { strict_updatability_linting: true }, } diff --git a/common/device/com/android/net/module/util/BpfMap.java b/common/device/com/android/net/module/util/BpfMap.java index 5f05c7c9..b42c3885 100644 --- a/common/device/com/android/net/module/util/BpfMap.java +++ b/common/device/com/android/net/module/util/BpfMap.java @@ -40,7 +40,7 @@ import java.util.function.BiConsumer; * @param <K> the key of the map. * @param <V> the value of the map. */ -public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable { +public class BpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V>, AutoCloseable { static { System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage())); } @@ -100,6 +100,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * Update an existing or create a new key -> value entry in an eBbpf map. * (use insertOrReplaceEntry() if you need to know whether insert or replace happened) */ + @Override public void updateEntry(K key, V value) throws ErrnoException { writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY); } @@ -108,6 +109,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * If the key does not exist in the map, insert key -> value entry into eBpf map. * Otherwise IllegalStateException will be thrown. */ + @Override public void insertEntry(K key, V value) throws ErrnoException, IllegalStateException { try { @@ -123,6 +125,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * If the key already exists in the map, replace its value. Otherwise NoSuchElementException * will be thrown. */ + @Override public void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException { try { @@ -140,6 +143,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * (use updateEntry() if you don't care whether insert or replace happened) * Note: see inline comment below if running concurrently with delete operations. */ + @Override public boolean insertOrReplaceEntry(K key, V value) throws ErrnoException { try { @@ -164,11 +168,13 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable } /** Remove existing key from eBpf map. Return false if map was not modified. */ + @Override public boolean deleteEntry(K key) throws ErrnoException { return deleteMapEntry(mMapFd, key.writeToBytes()); } /** Returns {@code true} if this map contains no elements. */ + @Override public boolean isEmpty() throws ErrnoException { return getFirstKey() == null; } @@ -189,6 +195,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * * TODO: consider allowing null passed-in key. */ + @Override public K getNextKey(@NonNull K key) throws ErrnoException { Objects.requireNonNull(key); return getNextKeyInternal(key); @@ -202,11 +209,13 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable } /** Get the first key of eBpf map. */ + @Override public K getFirstKey() throws ErrnoException { return getNextKeyInternal(null); } /** Check whether a key exists in the map. */ + @Override public boolean containsKey(@NonNull K key) throws ErrnoException { Objects.requireNonNull(key); @@ -215,6 +224,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable } /** Retrieve a value from the map. Return null if there is no such key. */ + @Override public V getValue(@NonNull K key) throws ErrnoException { Objects.requireNonNull(key); final byte[] rawValue = getRawValue(key.writeToBytes()); @@ -239,6 +249,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * other structural modifications to the map, such as adding entries or deleting other entries. * Otherwise, iteration will result in undefined behaviour. */ + @Override public void forEach(BiConsumer<K, V> action) throws ErrnoException { @Nullable K nextKey = getFirstKey(); @@ -262,6 +273,7 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable * @throws ErrnoException if the map is already closed, if an error occurred during iteration, * or if a non-ENOENT error occurred when deleting a key. */ + @Override public void clear() throws ErrnoException { K key = getFirstKey(); while (key != null) { diff --git a/common/device/com/android/net/module/util/IBpfMap.java b/common/device/com/android/net/module/util/IBpfMap.java new file mode 100644 index 00000000..708cf61a --- /dev/null +++ b/common/device/com/android/net/module/util/IBpfMap.java @@ -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 com.android.net.module.util; + +import android.system.ErrnoException; + +import androidx.annotation.NonNull; + +import java.util.NoSuchElementException; +import java.util.function.BiConsumer; + +/** + * The interface of BpfMap. This could be used to inject for testing. + * So the testing code won't load the JNI and update the entries to kernel. + * + * @param <K> the key of the map. + * @param <V> the value of the map. + */ +public interface IBpfMap<K extends Struct, V extends Struct> { + /** Update an existing or create a new key -> value entry in an eBbpf map. */ + void updateEntry(K key, V value) throws ErrnoException; + + /** If the key does not exist in the map, insert key -> value entry into eBpf map. */ + void insertEntry(K key, V value) throws ErrnoException, IllegalStateException; + + /** If the key already exists in the map, replace its value. */ + void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException; + + /** + * Update an existing or create a new key -> value entry in an eBbpf map. Returns true if + * inserted, false if replaced. (use updateEntry() if you don't care whether insert or replace + * happened). + */ + boolean insertOrReplaceEntry(K key, V value) throws ErrnoException; + + /** Remove existing key from eBpf map. Return true if something was deleted. */ + boolean deleteEntry(K key) throws ErrnoException; + + /** Returns {@code true} if this map contains no elements. */ + boolean isEmpty() throws ErrnoException; + + /** Get the key after the passed-in key. */ + K getNextKey(@NonNull K key) throws ErrnoException; + + /** Get the first key of the eBpf map. */ + K getFirstKey() throws ErrnoException; + + /** Check whether a key exists in the map. */ + boolean containsKey(@NonNull K key) throws ErrnoException; + + /** Retrieve a value from the map. */ + V getValue(@NonNull K key) throws ErrnoException; + + /** + * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer. + */ + void forEach(BiConsumer<K, V> action) throws ErrnoException; + + /** Clears the map. */ + void clear() throws ErrnoException; +} diff --git a/common/device/com/android/net/module/util/Struct.java b/common/device/com/android/net/module/util/Struct.java index b43e2c45..d717bc70 100644 --- a/common/device/com/android/net/module/util/Struct.java +++ b/common/device/com/android/net/module/util/Struct.java @@ -518,7 +518,8 @@ public class Struct { private static FieldInfo[] getClassFieldInfo(final Class clazz) { if (!isStructSubclass(clazz)) { throw new IllegalArgumentException(clazz.getName() + " is not a subclass of " - + Struct.class.getName()); + + Struct.class.getName() + ", its superclass is " + + clazz.getSuperclass().getName()); } final FieldInfo[] cachedAnnotationFields = sFieldCache.get(clazz); @@ -730,4 +731,32 @@ public class Struct { } return sb.toString(); } + + /** A simple Struct which only contains a u8 field. */ + public static class U8 extends Struct { + @Struct.Field(order = 0, type = Struct.Type.U8) + public final short val; + + public U8(final short val) { + this.val = val; + } + } + + public static class U32 extends Struct { + @Struct.Field(order = 0, type = Struct.Type.U32) + public final long val; + + public U32(final long val) { + this.val = val; + } + } + + public static class S64 extends Struct { + @Struct.Field(order = 0, type = Struct.Type.S64) + public final long val; + + public S64(final long val) { + this.val = val; + } + } } diff --git a/common/device/com/android/net/module/util/TcUtils.java b/common/device/com/android/net/module/util/TcUtils.java new file mode 100644 index 00000000..cf01490b --- /dev/null +++ b/common/device/com/android/net/module/util/TcUtils.java @@ -0,0 +1,93 @@ +/* + * 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.net.module.util; + +import java.io.IOException; + +/** + * Contains mostly tc-related functionality. + */ +public class TcUtils { + static { + System.loadLibrary(JniUtil.getJniLibraryName(TcUtils.class.getPackage())); + } + + /** + * Checks if the network interface uses an ethernet L2 header. + * + * @param iface the network interface. + * @return true if the interface uses an ethernet L2 header. + * @throws IOException + */ + public static native boolean isEthernet(String iface) throws IOException; + + /** + * Attach a tc bpf filter. + * + * Equivalent to the following 'tc' command: + * tc filter add dev .. in/egress prio .. protocol ipv6/ip bpf object-pinned + * /sys/fs/bpf/... direct-action + * + * @param ifIndex the network interface index. + * @param ingress ingress or egress qdisc. + * @param prio + * @param proto + * @param bpfProgPath + * @throws IOException + */ + public static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, + short proto, String bpfProgPath) throws IOException; + + /** + * Attach a tc police action. + * + * Attaches a matchall filter to the clsact qdisc with a tc police and tc bpf action attached. + * This causes the ingress rate to be limited and exceeding packets to be forwarded to a bpf + * program (specified in bpfProgPah) that accounts for the packets before dropping them. + * + * Equivalent to the following 'tc' command: + * tc filter add dev .. ingress prio .. protocol .. matchall \ + * action police rate .. burst .. conform-exceed pipe/continue \ + * action bpf object-pinned .. \ + * drop + * + * @param ifIndex the network interface index. + * @param prio the filter preference. + * @param proto protocol. + * @param rateInBytesPerSec rate limit in bytes/s. + * @param bpfProgPath bpg program that accounts for rate exceeding packets before they are + * dropped. + * @throws IOException + */ + public static native void tcFilterAddDevIngressPolice(int ifIndex, short prio, short proto, + int rateInBytesPerSec, String bpfProgPath) throws IOException; + + /** + * Delete a tc filter. + * + * Equivalent to the following 'tc' command: + * tc filter del dev .. in/egress prio .. protocol .. + * + * @param ifIndex the network interface index. + * @param ingress ingress or egress qdisc. + * @param prio the filter preference. + * @param proto protocol. + * @throws IOException + */ + public static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio, + short proto) throws IOException; +} diff --git a/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java index 71a0c966..26c24f8d 100644 --- a/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java +++ b/common/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java @@ -85,7 +85,7 @@ public final class NetworkCapabilitiesUtils { * and {@code FORCE_RESTRICTED_CAPABILITIES}. */ @VisibleForTesting - static final long RESTRICTED_CAPABILITIES = packBitList( + public static final long RESTRICTED_CAPABILITIES = packBitList( NET_CAPABILITY_BIP, NET_CAPABILITY_CBS, NET_CAPABILITY_DUN, @@ -115,7 +115,7 @@ public final class NetworkCapabilitiesUtils { * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}. */ @VisibleForTesting - static final long UNRESTRICTED_CAPABILITIES = packBitList( + public static final long UNRESTRICTED_CAPABILITIES = packBitList( NET_CAPABILITY_INTERNET, NET_CAPABILITY_MMS, NET_CAPABILITY_SUPL, diff --git a/common/framework/com/android/net/module/util/NetworkStatsUtils.java b/common/framework/com/android/net/module/util/NetworkStatsUtils.java index 28ff7705..41a9428a 100644 --- a/common/framework/com/android/net/module/util/NetworkStatsUtils.java +++ b/common/framework/com/android/net/module/util/NetworkStatsUtils.java @@ -16,12 +16,23 @@ package com.android.net.module.util; +import android.app.usage.NetworkStats; + +import com.android.internal.annotations.VisibleForTesting; + /** * Various utilities used for NetworkStats related code. * * @hide */ public class NetworkStatsUtils { + // These constants must be synced with the definition in android.net.NetworkStats. + // TODO: update to formal APIs once all downstreams have these APIs. + private static final int SET_ALL = -1; + private static final int METERED_ALL = -1; + private static final int ROAMING_ALL = -1; + private static final int DEFAULT_NETWORK_ALL = -1; + /** * Safely multiple a value by a rational. * <p> @@ -88,4 +99,77 @@ public class NetworkStatsUtils { if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")"); return amount < low ? low : (amount > high ? high : amount); } + + /** + * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats. + */ + public static android.net.NetworkStats fromPublicNetworkStats( + NetworkStats publiceNetworkStats) { + android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0); + while (publiceNetworkStats.hasNextBucket()) { + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + publiceNetworkStats.getNextBucket(bucket); + final android.net.NetworkStats.Entry entry = fromBucket(bucket); + stats = stats.addEntry(entry); + } + return stats; + } + + @VisibleForTesting + public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) { + return new android.net.NetworkStats.Entry( + null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()), + convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()), + convertBucketRoaming(bucket.getRoaming()), + convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()), + bucket.getRxBytes(), bucket.getRxPackets(), + bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */); + } + + private static int convertBucketState(int networkStatsSet) { + switch (networkStatsSet) { + case NetworkStats.Bucket.STATE_ALL: return SET_ALL; + case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT; + case NetworkStats.Bucket.STATE_FOREGROUND: + return android.net.NetworkStats.SET_FOREGROUND; + } + return 0; + } + + private static int convertBucketTag(int tag) { + switch (tag) { + case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE; + } + return tag; + } + + private static int convertBucketMetered(int metered) { + switch (metered) { + case NetworkStats.Bucket.METERED_ALL: return METERED_ALL; + case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO; + case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES; + } + return 0; + } + + private static int convertBucketRoaming(int roaming) { + switch (roaming) { + case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL; + case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO; + case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES; + } + return 0; + } + + private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) { + switch (defaultNetworkStatus) { + case NetworkStats.Bucket.DEFAULT_NETWORK_ALL: + return DEFAULT_NETWORK_ALL; + case NetworkStats.Bucket.DEFAULT_NETWORK_NO: + return android.net.NetworkStats.DEFAULT_NETWORK_NO; + case NetworkStats.Bucket.DEFAULT_NETWORK_YES: + return android.net.NetworkStats.DEFAULT_NETWORK_YES; + } + return 0; + } } diff --git a/common/native/bpf_headers/Android.bp b/common/native/bpf_headers/Android.bp index 06ba1b05..834ef02b 100644 --- a/common/native/bpf_headers/Android.bp +++ b/common/native/bpf_headers/Android.bp @@ -18,7 +18,7 @@ package { cc_library_headers { name: "bpf_headers", - vendor_available: false, + vendor_available: true, host_supported: true, native_bridge_supported: true, header_libs: ["bpf_syscall_wrappers"], @@ -37,6 +37,7 @@ cc_library_headers { ], visibility: [ "//bootable/libbootloader/vts", + "//cts/tests/tests/net/native", "//frameworks/base/services/core/jni", "//frameworks/native/libs/cputimeinstate", "//frameworks/native/services/gpuservice", @@ -45,12 +46,13 @@ cc_library_headers { "//frameworks/native/services/gpuservice/tracing", "//packages/modules/Connectivity/bpf_progs", "//packages/modules/Connectivity/netd", + "//packages/modules/Connectivity/service/native", + "//packages/modules/Connectivity/service/native/libs/libclat", "//packages/modules/Connectivity/tests/unit/jni", "//packages/modules/DnsResolver/tests", "//system/bpf/bpfloader", "//system/bpf/libbpf_android", "//system/memory/libmeminfo", - "//system/netd/libnetdbpf", "//system/netd/server", "//system/netd/tests", "//system/netd/tests/benchmarks", diff --git a/common/native/bpf_headers/include/bpf/bpf_helpers.h b/common/native/bpf_headers/include/bpf/bpf_helpers.h index 878bb103..ac9f9bcb 100644 --- a/common/native/bpf_headers/include/bpf/bpf_helpers.h +++ b/common/native/bpf_headers/include/bpf/bpf_helpers.h @@ -116,6 +116,15 @@ static int (*bpf_map_update_elem_unsafe)(const struct bpf_map_def* map, const vo static int (*bpf_map_delete_elem_unsafe)(const struct bpf_map_def* map, const void* key) = (void*)BPF_FUNC_map_delete_elem; +#define BPF_ANNOTATE_KV_PAIR(name, type_key, type_val) \ + struct ____btf_map_##name { \ + type_key key; \ + type_val value; \ + }; \ + struct ____btf_map_##name \ + __attribute__ ((section(".maps." #name), used)) \ + ____btf_map_##name = { } + /* type safe macro to declare a map and related accessor functions */ #define DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, usr, grp, md) \ const struct bpf_map_def SECTION("maps") the_map = { \ @@ -132,6 +141,7 @@ static int (*bpf_map_delete_elem_unsafe)(const struct bpf_map_def* map, .min_kver = KVER_NONE, \ .max_kver = KVER_INF, \ }; \ + BPF_ANNOTATE_KV_PAIR(the_map, TypeOfKey, TypeOfValue); \ \ static inline __always_inline __unused TypeOfValue* bpf_##the_map##_lookup_elem( \ const TypeOfKey* k) { \ diff --git a/common/native/bpf_headers/include/bpf/bpf_map_def.h b/common/native/bpf_headers/include/bpf/bpf_map_def.h index 02b2096d..13716681 100644 --- a/common/native/bpf_headers/include/bpf/bpf_map_def.h +++ b/common/native/bpf_headers/include/bpf/bpf_map_def.h @@ -23,7 +23,7 @@ #include <linux/bpf.h> // Pull in AID_* constants from //system/core/libcutils/include/private/android_filesystem_config.h -#include <private/android_filesystem_config.h> +#include <cutils/android_filesystem_config.h> /****************************************************************************** * * diff --git a/common/native/bpf_syscall_wrappers/Android.bp b/common/native/bpf_syscall_wrappers/Android.bp index 088bcbbf..a20eed3a 100644 --- a/common/native/bpf_syscall_wrappers/Android.bp +++ b/common/native/bpf_syscall_wrappers/Android.bp @@ -18,7 +18,7 @@ package { cc_library_headers { name: "bpf_syscall_wrappers", - vendor_available: false, + vendor_available: true, host_supported: true, native_bridge_supported: true, export_include_dirs: ["include"], @@ -39,6 +39,8 @@ cc_library_headers { "//frameworks/libs/net/common/native/tcutils", "//packages/modules/Connectivity/netd", "//packages/modules/Connectivity/service", + "//packages/modules/Connectivity/service/native", + "//packages/modules/Connectivity/service/native/libs/libclat", "//packages/modules/Connectivity/Tethering", "//packages/providers/MediaProvider/jni", "//system/bpf/libbpf_android", diff --git a/common/native/bpfmapjni/Android.bp b/common/native/bpfmapjni/Android.bp index b7af22d3..cd254d4c 100644 --- a/common/native/bpfmapjni/Android.bp +++ b/common/native/bpfmapjni/Android.bp @@ -18,7 +18,10 @@ package { cc_library_static { name: "libnet_utils_device_common_bpfjni", - srcs: ["com_android_net_module_util_BpfMap.cpp"], + srcs: [ + "com_android_net_module_util_BpfMap.cpp", + "com_android_net_module_util_TcUtils.cpp", + ], header_libs: [ "bpf_syscall_wrappers", "jni_headers", @@ -27,6 +30,9 @@ cc_library_static { "liblog", "libnativehelper_compat_libc++", ], + whole_static_libs: [ + "libtcutils", + ], cflags: [ "-Wall", "-Werror", @@ -40,5 +46,7 @@ cc_library_static { ], visibility: [ "//packages/modules/Connectivity:__subpackages__", + // TODO: remove after NetworkStatsService moves to the module. + "//frameworks/base/packages/ConnectivityT/service", ], } diff --git a/common/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/common/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp new file mode 100644 index 00000000..2307a6bb --- /dev/null +++ b/common/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp @@ -0,0 +1,107 @@ +/* + * 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 <jni.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/scoped_utf_chars.h> +#include <tcutils/tcutils.h> + +namespace android { + +static void throwIOException(JNIEnv *env, const char *msg, int error) { + jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, + strerror(error)); +} + +static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env, + jobject clazz, + jstring iface) { + ScopedUtfChars interface(env, iface); + bool result = false; + int error = isEthernet(interface.c_str(), result); + if (error) { + throwIOException( + env, "com_android_net_module_util_TcUtils_isEthernet error: ", error); + } + // result is not touched when error is returned; leave false. + return result; +} + +// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned +// /sys/fs/bpf/... direct-action +static void com_android_net_module_util_TcUtils_tcFilterAddDevBpf( + JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, + jshort proto, jstring bpfProgPath) { + ScopedUtfChars pathname(env, bpfProgPath); + int error = tcAddBpfFilter(ifIndex, ingress, prio, proto, pathname.c_str()); + if (error) { + throwIOException( + env, + "com_android_net_module_util_TcUtils_tcFilterAddDevBpf error: ", error); + } +} + +// tc filter add dev .. ingress prio .. protocol .. matchall \ +// action police rate .. burst .. conform-exceed pipe/continue \ +// action bpf object-pinned .. \ +// drop +static void com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice( + JNIEnv *env, jobject clazz, jint ifIndex, jshort prio, jshort proto, + jint rateInBytesPerSec, jstring bpfProgPath) { + ScopedUtfChars pathname(env, bpfProgPath); + int error = tcAddIngressPoliceFilter(ifIndex, prio, proto, rateInBytesPerSec, + pathname.c_str()); + if (error) { + throwIOException(env, + "com_android_net_module_util_TcUtils_" + "tcFilterAddDevIngressPolice error: ", + error); + } +} + +// tc filter del dev .. in/egress prio .. protocol .. +static void com_android_net_module_util_TcUtils_tcFilterDelDev( + JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, + jshort proto) { + int error = tcDeleteFilter(ifIndex, ingress, prio, proto); + if (error) { + throwIOException( + env, + "com_android_net_module_util_TcUtils_tcFilterDelDev error: ", error); + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"isEthernet", "(Ljava/lang/String;)Z", + (void *)com_android_net_module_util_TcUtils_isEthernet}, + {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V", + (void *)com_android_net_module_util_TcUtils_tcFilterAddDevBpf}, + {"tcFilterAddDevIngressPolice", "(ISSILjava/lang/String;)V", + (void *)com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice}, + {"tcFilterDelDev", "(IZSS)V", + (void *)com_android_net_module_util_TcUtils_tcFilterDelDev}, +}; + +int register_com_android_net_module_util_TcUtils(JNIEnv *env, + char const *class_name) { + return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/common/native/nettestutils/Android.bp b/common/native/nettestutils/Android.bp new file mode 100644 index 00000000..42df8e0a --- /dev/null +++ b/common/native/nettestutils/Android.bp @@ -0,0 +1,32 @@ +// 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: "libnettestutils", + export_include_dirs: ["include"], + srcs: ["DumpService.cpp"], + + shared_libs: [ + "libbinder", + "libutils", + ], + cflags: [ + "-Werror", + "-Wall", + ], +} diff --git a/common/native/nettestutils/DumpService.cpp b/common/native/nettestutils/DumpService.cpp new file mode 100644 index 00000000..ba3d77e9 --- /dev/null +++ b/common/native/nettestutils/DumpService.cpp @@ -0,0 +1,57 @@ +/* + * 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 "nettestutils/DumpService.h" + +#include <android-base/file.h> + +#include <sstream> +#include <thread> + +android::status_t dumpService(const android::sp<android::IBinder>& binder, + const std::vector<std::string>& args, + std::vector<std::string>& outputLines) { + if (!outputLines.empty()) return -EUCLEAN; + + android::base::unique_fd localFd, remoteFd; + if (!Pipe(&localFd, &remoteFd)) return -errno; + + android::Vector<android::String16> str16Args; + for (const auto& arg : args) { + str16Args.push(android::String16(arg.c_str())); + } + android::status_t ret; + // dump() blocks until another thread has consumed all its output. + std::thread dumpThread = + std::thread([&ret, binder, remoteFd{std::move(remoteFd)}, str16Args]() { + ret = binder->dump(remoteFd, str16Args); + }); + + std::string dumpContent; + if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) { + return -errno; + } + dumpThread.join(); + if (ret != android::OK) return ret; + + std::stringstream dumpStream(std::move(dumpContent)); + std::string line; + while (std::getline(dumpStream, line)) { + outputLines.push_back(line); + } + + return android::OK; +} diff --git a/common/native/nettestutils/include/nettestutils/DumpService.h b/common/native/nettestutils/include/nettestutils/DumpService.h new file mode 100644 index 00000000..2a721817 --- /dev/null +++ b/common/native/nettestutils/include/nettestutils/DumpService.h @@ -0,0 +1,23 @@ +/* + * 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 <binder/Binder.h> + +#include <vector> + +android::status_t dumpService(const android::sp<android::IBinder>& binder, + const std::vector<std::string>& args, + std::vector<std::string>& outputLines); diff --git a/common/native/tcutils/Android.bp b/common/native/tcutils/Android.bp index 01b24241..e819e4c0 100644 --- a/common/native/tcutils/Android.bp +++ b/common/native/tcutils/Android.bp @@ -38,6 +38,31 @@ cc_library_static { ], visibility: [ "//frameworks/libs/net/common/native/bpfmapjni", + "//packages/modules/Connectivity:__subpackages__", "//system/netd/server", ], } + +cc_test { + name: "libtcutils_test", + srcs: [ + "tests/tcutils_test.cpp", + ], + cflags: [ + "-Wall", + "-Werror", + "-Wno-error=unused-variable", + ], + header_libs: ["bpf_syscall_wrappers"], + static_libs: [ + "libgmock", + "libtcutils", + ], + shared_libs: [ + "libbase", + "liblog", + ], + min_sdk_version: "30", + require_root: true, + test_suites: ["general-tests"], +} diff --git a/common/native/tcutils/include/tcutils/tcutils.h b/common/native/tcutils/include/tcutils/tcutils.h index d1e1bb76..a8ec2e87 100644 --- a/common/native/tcutils/include/tcutils/tcutils.h +++ b/common/native/tcutils/include/tcutils/tcutils.h @@ -17,12 +17,31 @@ #pragma once #include <cstdint> +#include <linux/rtnetlink.h> namespace android { int isEthernet(const char *iface, bool &isEthernet); + +int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags); + +static inline int tcAddQdiscClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE); +} + +static inline int tcReplaceQdiscClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE); +} + +static inline int tcDeleteQdiscClsact(int ifIndex) { + return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0); +} + int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto, const char *bpfProgPath); +int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto, + unsigned rateInBytesPerSec, + const char *bpfProgPath); int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto); } // namespace android diff --git a/common/native/tcutils/kernelversion.h b/common/native/tcutils/kernelversion.h new file mode 100644 index 00000000..3be1ad29 --- /dev/null +++ b/common/native/tcutils/kernelversion.h @@ -0,0 +1,59 @@ +/* + * 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. + */ + +// ----------------------------------------------------------------------------- +// TODO - This should be replaced with BpfUtils in bpf_headers. +// Currently, bpf_headers contains a bunch requirements it doesn't actually provide, such as a +// non-ndk liblog version, and some version of libbase. libtcutils does not have access to either of +// these, so I think this will have to wait until we figure out a way around this. +// +// In the mean time copying verbatim from: +// frameworks/libs/net/common/native/bpf_headers + +#pragma once + +#include <stdio.h> +#include <sys/utsname.h> + +#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c)) + +namespace android { + +static inline 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); +} + +} // namespace android diff --git a/common/native/tcutils/logging.h b/common/native/tcutils/logging.h new file mode 100644 index 00000000..70604b30 --- /dev/null +++ b/common/native/tcutils/logging.h @@ -0,0 +1,35 @@ +/* + * 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 <android/log.h> +#include <stdarg.h> + +#ifndef LOG_TAG +#define LOG_TAG "TcUtils_Undef" +#endif + +namespace android { + +static inline void ALOGE(const char *fmt...) { + va_list args; + va_start(args, fmt); + __android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args); + va_end(args); +} + +} diff --git a/common/native/tcutils/tcutils.cpp b/common/native/tcutils/tcutils.cpp index ca1c63c4..0e17f67c 100644 --- a/common/native/tcutils/tcutils.cpp +++ b/common/native/tcutils/tcutils.cpp @@ -18,12 +18,12 @@ #include "tcutils/tcutils.h" +#include "logging.h" +#include "kernelversion.h" #include "scopeguard.h" -#include <android/log.h> #include <arpa/inet.h> #include <cerrno> -#include <cstdio> #include <cstring> #include <libgen.h> #include <linux/if_arp.h> @@ -32,10 +32,10 @@ #include <linux/pkt_cls.h> #include <linux/pkt_sched.h> #include <linux/rtnetlink.h> +#include <linux/tc_act/tc_bpf.h> #include <net/if.h> -#include <stdarg.h> +#include <stdio.h> #include <sys/socket.h> -#include <sys/utsname.h> #include <unistd.h> #include <utility> @@ -52,12 +52,318 @@ namespace android { namespace { -void logError(const char *fmt...) { - va_list args; - va_start(args, fmt); - __android_log_vprint(ANDROID_LOG_ERROR, LOG_TAG, fmt, args); - va_end(args); -} +/** + * IngressPoliceFilterBuilder builds a nlmsg request equivalent to the following + * tc command: + * + * tc filter add dev .. ingress prio .. protocol .. matchall \ + * action police rate .. burst .. conform-exceed pipe/continue \ + * action bpf object-pinned .. \ + * drop + */ +class IngressPoliceFilterBuilder final { + // default mtu is 2047, so the cell logarithm factor (cell_log) is 3. + // 0x7FF >> 0x3FF x 2^1 >> 0x1FF x 2^2 >> 0xFF x 2^3 + static constexpr int RTAB_CELL_LOGARITHM = 3; + static constexpr size_t RTAB_SIZE = 256; + static constexpr unsigned TIME_UNITS_PER_SEC = 1000000; + + struct Request { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + char str[NLMSG_ALIGN(sizeof("matchall"))]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + struct { + nlattr attr; + struct { + nlattr attr; + char str[NLMSG_ALIGN(sizeof("police"))]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + struct tc_police obj; + } police; + struct { + nlattr attr; + uint32_t u32[RTAB_SIZE]; + } rtab; + struct { + nlattr attr; + int32_t s32; + } notexceedact; + } opt; + } act1; + struct { + nlattr attr; + struct { + nlattr attr; + char str[NLMSG_ALIGN(sizeof("bpf"))]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + uint32_t u32; + } fd; + struct { + nlattr attr; + char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)]; + } name; + struct { + nlattr attr; + struct tc_act_bpf obj; + } parms; + } opt; + } act2; + } acts; + } opt; + }; + + // class members + const unsigned mBurstInBytes; + const char *mBpfProgPath; + int mBpfFd; + Request mRequest; + + static double getTickInUsec() { + FILE *fp = fopen("/proc/net/psched", "re"); + if (!fp) { + ALOGE("fopen(\"/proc/net/psched\"): %s", strerror(errno)); + return 0.0; + } + auto scopeGuard = base::make_scope_guard([fp] { fclose(fp); }); + + uint32_t t2us; + uint32_t us2t; + uint32_t clockRes; + const bool isError = + fscanf(fp, "%08x%08x%08x", &t2us, &us2t, &clockRes) != 3; + + if (isError) { + ALOGE("fscanf(/proc/net/psched, \"%%08x%%08x%%08x\"): %s", + strerror(errno)); + return 0.0; + } + + const double clockFactor = + static_cast<double>(clockRes) / TIME_UNITS_PER_SEC; + return static_cast<double>(t2us) / static_cast<double>(us2t) * clockFactor; + } + + static inline const double kTickInUsec = getTickInUsec(); + +public: + // clang-format off + IngressPoliceFilterBuilder(int ifIndex, uint16_t prio, uint16_t proto, unsigned rateInBytesPerSec, + unsigned burstInBytes, const char* bpfProgPath) + : mBurstInBytes(burstInBytes), + mBpfProgPath(bpfProgPath), + mBpfFd(-1), + mRequest{ + .n = { + .nlmsg_len = sizeof(mRequest), + .nlmsg_type = RTM_NEWTFILTER, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | 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, TC_H_MIN_INGRESS), + .tcm_info = (static_cast<uint32_t>(prio) << 16) + | static_cast<uint32_t>(htons(proto)), + }, + .kind = { + .attr = { + .nla_len = sizeof(mRequest.kind), + .nla_type = TCA_KIND, + }, + .str = "matchall", + }, + .opt = { + .attr = { + .nla_len = sizeof(mRequest.opt), + .nla_type = TCA_OPTIONS, + }, + .acts = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts), + .nla_type = TCA_U32_ACT, + }, + .act1 = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1), + .nla_type = 1, // action priority + }, + .kind = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1.kind), + .nla_type = TCA_ACT_KIND, + }, + .str = "police", + }, + .opt = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1.opt), + .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED, + }, + .police = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1.opt.police), + .nla_type = TCA_POLICE_TBF, + }, + .obj = { + .action = TC_ACT_PIPE, + .burst = 0, + .rate = { + .cell_log = RTAB_CELL_LOGARITHM, + .linklayer = TC_LINKLAYER_ETHERNET, + .cell_align = -1, + .rate = rateInBytesPerSec, + }, + }, + }, + .rtab = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1.opt.rtab), + .nla_type = TCA_POLICE_RATE, + }, + .u32 = {}, + }, + .notexceedact = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act1.opt.notexceedact), + .nla_type = TCA_POLICE_RESULT, + }, + .s32 = TC_ACT_UNSPEC, + }, + }, + }, + .act2 = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2), + .nla_type = 2, // action priority + }, + .kind = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2.kind), + .nla_type = TCA_ACT_KIND, + }, + .str = "bpf", + }, + .opt = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2.opt), + .nla_type = TCA_ACT_OPTIONS | NLA_F_NESTED, + }, + .fd = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2.opt.fd), + .nla_type = TCA_ACT_BPF_FD, + }, + .u32 = 0, // set during build() + }, + .name = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2.opt.name), + .nla_type = TCA_ACT_BPF_NAME, + }, + .str = "placeholder", + }, + .parms = { + .attr = { + .nla_len = sizeof(mRequest.opt.acts.act2.opt.parms), + .nla_type = TCA_ACT_BPF_PARMS, + }, + .obj = { + // default action to be executed when bpf prog + // returns TC_ACT_UNSPEC. + .action = TC_ACT_SHOT, + }, + }, + }, + }, + }, + }, + } { + // constructor body + } + // clang-format on + + ~IngressPoliceFilterBuilder() { + // TODO: use unique_fd + if (mBpfFd != -1) { + close(mBpfFd); + } + } + + constexpr unsigned getRequestSize() const { return sizeof(Request); } + +private: + unsigned calculateXmitTime(unsigned size) { + const uint32_t rate = mRequest.opt.acts.act1.opt.police.obj.rate.rate; + return (static_cast<double>(size) / static_cast<double>(rate)) * + TIME_UNITS_PER_SEC * kTickInUsec; + } + + void initBurstRate() { + mRequest.opt.acts.act1.opt.police.obj.burst = + calculateXmitTime(mBurstInBytes); + } + + // Calculates a table with 256 transmission times for different packet sizes + // (all the way up to MTU). RTAB_CELL_LOGARITHM is used as a scaling factor. + // In this case, MTU size is always 2048, so RTAB_CELL_LOGARITHM is always + // 3. Therefore, this function generates the transmission times for packets + // of size 1..256 x 2^3. + void initRateTable() { + for (unsigned i = 0; i < RTAB_SIZE; ++i) { + unsigned adjustedSize = (i + 1) << RTAB_CELL_LOGARITHM; + mRequest.opt.acts.act1.opt.rtab.u32[i] = calculateXmitTime(adjustedSize); + } + } + + int initBpfFd() { + mBpfFd = bpf::retrieveProgram(mBpfProgPath); + if (mBpfFd == -1) { + int error = errno; + ALOGE("retrieveProgram failed: %d", error); + return -error; + } + + mRequest.opt.acts.act2.opt.fd.u32 = static_cast<uint32_t>(mBpfFd); + snprintf(mRequest.opt.acts.act2.opt.name.str, + sizeof(mRequest.opt.acts.act2.opt.name.str), "%s:[*fsobj]", + basename(mBpfProgPath)); + + return 0; + } + +public: + int build() { + if (kTickInUsec == 0.0) { + return -EINVAL; + } + + initBurstRate(); + initRateTable(); + return initBpfFd(); + } + + const Request *getRequest() const { + // Make sure to call build() before calling this function. Otherwise, the + // request will be invalid. + return &mRequest; + } +}; const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; @@ -67,7 +373,7 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); if (fd == -1) { int error = errno; - logError("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d", + ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d", error); return -error; } @@ -76,7 +382,7 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { static constexpr int on = 1; if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) { int error = errno; - logError("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error); + ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, 1): %d", error); return -error; } @@ -84,7 +390,7 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { if (bind(fd, (const struct sockaddr *)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { int error = errno; - logError("bind(fd, {AF_NETLINK, 0, 0}: %d)", error); + ALOGE("bind(fd, {AF_NETLINK, 0, 0}: %d)", error); return -error; } @@ -92,7 +398,7 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { if (connect(fd, (const struct sockaddr *)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { int error = errno; - logError("connect(fd, {AF_NETLINK, 0, 0}): %d", error); + ALOGE("connect(fd, {AF_NETLINK, 0, 0}): %d", error); return -error; } @@ -100,12 +406,12 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { if (rv == -1) { int error = errno; - logError("send(fd, req, len, 0) failed: %d", error); + ALOGE("send(fd, req, len, 0) failed: %d", error); return -error; } if (rv != len) { - logError("send(fd, req, len = %d, 0) returned invalid message size %d", len, + ALOGE("send(fd, req, len = %d, 0) returned invalid message size %d", len, rv); return -EMSGSIZE; } @@ -120,29 +426,29 @@ int sendAndProcessNetlinkResponse(const void *req, int len) { if (rv == -1) { int error = errno; - logError("recv() failed: %d", error); + ALOGE("recv() failed: %d", error); return -error; } if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { - logError("recv() returned short packet: %d", rv); + ALOGE("recv() returned short packet: %d", rv); return -EBADMSG; } if (resp.h.nlmsg_len != (unsigned)rv) { - logError("recv() returned invalid header length: %d != %d", + ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv); return -EBADMSG; } if (resp.h.nlmsg_type != NLMSG_ERROR) { - logError("recv() did not return NLMSG_ERROR message: %d", + ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); return -ENOMSG; } if (resp.e.error) { - logError("NLMSG_ERROR message return error: %d", resp.e.error); + ALOGE("NLMSG_ERROR message return error: %d", resp.e.error); } return resp.e.error; // returns 0 on success } @@ -168,49 +474,14 @@ int hardwareAddressType(const char *interface) { return ifr.ifr_hwaddr.sa_family; } -// ----------------------------------------------------------------------------- -// 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)) - -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); -} - -bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) { - return kernelVersion() >= KVER(major, minor, sub); -} -// ----------------------------------------------------------------------------- - } // namespace int isEthernet(const char *iface, bool &isEthernet) { int rv = hardwareAddressType(iface); if (rv < 0) { - logError("Get hardware address type of interface %s failed: %s", iface, + ALOGE("Get hardware address type of interface %s failed: %s", iface, strerror(-rv)); - return -rv; + return rv; } // Backwards compatibility with pre-GKI kernels that use various custom @@ -242,18 +513,66 @@ int isEthernet(const char *iface, bool &isEthernet) { isEthernet = false; return 0; default: - logError("Unknown hardware address type %d on interface %s", rv, iface); - return -ENOENT; + ALOGE("Unknown hardware address type %d on interface %s", rv, iface); + return -EAFNOSUPPORT; } } +// 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 1 protocol ipv6/ip bpf object-pinned // /sys/fs/bpf/... direct-action int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto, const char *bpfProgPath) { const int bpfFd = bpf::retrieveProgram(bpfProgPath); if (bpfFd == -1) { - logError("retrieveProgram failed: %d", errno); + ALOGE("retrieveProgram failed: %d", errno); return -errno; } auto scopeGuard = base::make_scope_guard([bpfFd] { close(bpfFd); }); @@ -355,6 +674,37 @@ int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto, return error; } +// tc filter add dev .. ingress prio .. protocol .. matchall \ +// action police rate .. burst .. conform-exceed pipe/continue \ +// action bpf object-pinned .. \ +// drop +// +// TODO: tc-police does not do ECN marking, so in the future, we should consider +// adding a second tc-police filter at a lower priority that rate limits traffic +// at something like 0.8 times the global rate limit and ecn marks exceeding +// packets inside a bpf program (but does not drop them). +int tcAddIngressPoliceFilter(int ifIndex, uint16_t prio, uint16_t proto, + unsigned rateInBytesPerSec, + const char *bpfProgPath) { + // TODO: this value needs to be validated. + // TCP IW10 (initial congestion window) means servers will send 10 mtus worth + // of data on initial connect. + // If nic is LRO capable it could aggregate up to 64KiB, so again probably a + // bad idea to set burst below that, because ingress packets could get + // aggregated to 64KiB at the nic. + // I don't know, but I wonder whether we shouldn't just do 128KiB and not do + // any math. + static constexpr unsigned BURST_SIZE_IN_BYTES = 128 * 1024; // 128KiB + IngressPoliceFilterBuilder filter(ifIndex, prio, proto, rateInBytesPerSec, + BURST_SIZE_IN_BYTES, bpfProgPath); + const int error = filter.build(); + if (error) { + return error; + } + return sendAndProcessNetlinkResponse(filter.getRequest(), + filter.getRequestSize()); +} + // tc filter del dev .. in/egress prio .. protocol .. int tcDeleteFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) { const struct { diff --git a/common/native/tcutils/tests/tcutils_test.cpp b/common/native/tcutils/tests/tcutils_test.cpp new file mode 100644 index 00000000..32736d6b --- /dev/null +++ b/common/native/tcutils/tests/tcutils_test.cpp @@ -0,0 +1,157 @@ +/* + * 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. + * + * TcUtilsTest.cpp - unit tests for TcUtils.cpp + */ + +#include <gtest/gtest.h> + +#include "kernelversion.h" +#include <tcutils/tcutils.h> + +#include <BpfSyscallWrappers.h> +#include <errno.h> +#include <linux/if_ether.h> + +namespace android { + +TEST(LibTcUtilsTest, IsEthernetOfNonExistingIf) { + bool result = false; + int error = isEthernet("not_existing_if", result); + ASSERT_FALSE(result); + ASSERT_EQ(-ENODEV, error); +} + +TEST(LibTcUtilsTest, IsEthernetOfLoopback) { + bool result = false; + int error = isEthernet("lo", result); + ASSERT_FALSE(result); + ASSERT_EQ(-EAFNOSUPPORT, error); +} + +// If wireless 'wlan0' interface exists it should be Ethernet. +// See also HardwareAddressTypeOfWireless. +TEST(LibTcUtilsTest, IsEthernetOfWireless) { + bool result = false; + int error = isEthernet("wlan0", result); + if (!result && error == -ENODEV) + return; + + ASSERT_EQ(0, error); + ASSERT_TRUE(result); +} + +// If cellular 'rmnet_data0' interface exists it should +// *probably* not be Ethernet and instead be RawIp. +// See also HardwareAddressTypeOfCellular. +TEST(LibTcUtilsTest, IsEthernetOfCellular) { + bool result = false; + int error = isEthernet("rmnet_data0", result); + if (!result && error == -ENODEV) + return; + + ASSERT_EQ(0, error); + ASSERT_FALSE(result); +} + +// See Linux kernel source in include/net/flow.h +static constexpr int LOOPBACK_IFINDEX = 1; + +TEST(LibTcUtilsTest, 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, tcAddQdiscClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcReplaceQdiscClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX)); + EXPECT_EQ(-EINVAL, tcDeleteQdiscClsact(LOOPBACK_IFINDEX)); +} + +TEST(LibTcUtilsTest, AddAndDeleteBpfFilter) { + // TODO: this should use bpf_shared.h rather than hardcoding the path + static constexpr char bpfProgPath[] = + "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_downstream6_ether"; + const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL; + + // static test values + static constexpr bool ingress = true; + static constexpr uint16_t prio = 17; + static constexpr uint16_t proto = ETH_P_ALL; + + // try to delete missing filter from missing qdisc + EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto)); + // try to attach bpf filter to missing qdisc + EXPECT_EQ(-EINVAL, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto, + bpfProgPath)); + // add the clsact qdisc + EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX)); + // try to delete missing filter when there is a qdisc attached + EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto)); + // add and delete a bpf filter + EXPECT_EQ( + 0, tcAddBpfFilter(LOOPBACK_IFINDEX, ingress, prio, proto, bpfProgPath)); + EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto)); + // try to remove the same filter a second time + EXPECT_EQ(-errNOENT, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto)); + // remove the clsact qdisc + EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX)); + // once again, try to delete missing filter from missing qdisc + EXPECT_EQ(-EINVAL, tcDeleteFilter(LOOPBACK_IFINDEX, ingress, prio, proto)); +} + +TEST(LibTcUtilsTest, AddAndDeleteIngressPoliceFilter) { + // TODO: this should use bpf_shared.h rather than hardcoding the path + static constexpr char bpfProgPath[] = + "/sys/fs/bpf/prog_netd_schedact_ingress_account"; + int fd = bpf::retrieveProgram(bpfProgPath); + if (fd == -1) { + // ingress policing is not supported. + return; + } + close(fd); + + const int errNOENT = isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL; + + // static test values + static constexpr unsigned rateInBytesPerSec = + 1024 * 1024; // 8mbit/s => 1mbyte/s => 1024*1024 bytes/s. + static constexpr uint16_t prio = 17; + static constexpr uint16_t proto = ETH_P_ALL; + + // try to delete missing filter from missing qdisc + EXPECT_EQ(-EINVAL, + tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto)); + // try to attach bpf filter to missing qdisc + EXPECT_EQ(-EINVAL, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto, + rateInBytesPerSec, bpfProgPath)); + // add the clsact qdisc + EXPECT_EQ(0, tcAddQdiscClsact(LOOPBACK_IFINDEX)); + // try to delete missing filter when there is a qdisc attached + EXPECT_EQ(-errNOENT, + tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto)); + // add and delete a bpf filter + EXPECT_EQ(0, tcAddIngressPoliceFilter(LOOPBACK_IFINDEX, prio, proto, + rateInBytesPerSec, bpfProgPath)); + EXPECT_EQ(0, tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto)); + // try to remove the same filter a second time + EXPECT_EQ(-errNOENT, + tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto)); + // remove the clsact qdisc + EXPECT_EQ(0, tcDeleteQdiscClsact(LOOPBACK_IFINDEX)); + // once again, try to delete missing filter from missing qdisc + EXPECT_EQ(-EINVAL, + tcDeleteFilter(LOOPBACK_IFINDEX, true /*ingress*/, prio, proto)); +} + +} // namespace android diff --git a/common/netd/Android.bp b/common/netd/Android.bp index 6a12d64e..e249e19d 100644 --- a/common/netd/Android.bp +++ b/common/netd/Android.bp @@ -48,6 +48,7 @@ cc_library_static { ], apex_available: [ "com.android.resolv", + "com.android.tethering", ], min_sdk_version: "29", } @@ -97,6 +98,7 @@ aidl_interface { ndk: { apex_available: [ "//apex_available:platform", + "com.android.tethering", ], // This is necessary for the DnsResovler tests to run in Android Q. // Soong would recognize this value and produce the Q compatible aidl library. diff --git a/common/netd/libnetdutils/Android.bp b/common/netd/libnetdutils/Android.bp index 732e37d9..08d54121 100644 --- a/common/netd/libnetdutils/Android.bp +++ b/common/netd/libnetdutils/Android.bp @@ -11,6 +11,7 @@ cc_library { "Log.cpp", "Netfilter.cpp", "Netlink.cpp", + "NetlinkListener.cpp", "Slice.cpp", "Socket.cpp", "SocketOption.cpp", diff --git a/common/netd/libnetdutils/NetlinkListener.cpp b/common/netd/libnetdutils/NetlinkListener.cpp new file mode 100644 index 00000000..decaa9c8 --- /dev/null +++ b/common/netd/libnetdutils/NetlinkListener.cpp @@ -0,0 +1,152 @@ +/* + * 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. + */ + +#define LOG_TAG "NetlinkListener" + +#include <sstream> +#include <vector> + +#include <linux/netfilter/nfnetlink.h> + +#include <log/log.h> +#include <netdutils/Misc.h> +#include <netdutils/NetlinkListener.h> +#include <netdutils/Syscalls.h> + +namespace android { +namespace netdutils { + +using netdutils::Fd; +using netdutils::Slice; +using netdutils::Status; +using netdutils::UniqueFd; +using netdutils::findWithDefault; +using netdutils::forEachNetlinkMessage; +using netdutils::makeSlice; +using netdutils::sSyscalls; +using netdutils::status::ok; +using netdutils::statusFromErrno; + +namespace { + +constexpr int kNetlinkMsgErrorType = (NFNL_SUBSYS_NONE << 8) | NLMSG_ERROR; + +constexpr sockaddr_nl kKernelAddr = { + .nl_family = AF_NETLINK, .nl_pad = 0, .nl_pid = 0, .nl_groups = 0, +}; + +const NetlinkListener::DispatchFn kDefaultDispatchFn = [](const nlmsghdr& nlmsg, const Slice) { + std::stringstream ss; + ss << nlmsg; + ALOGE("unhandled netlink message: %s", ss.str().c_str()); +}; + +} // namespace + +NetlinkListener::NetlinkListener(UniqueFd event, UniqueFd sock, const std::string& name) + : mEvent(std::move(event)), mSock(std::move(sock)), mThreadName(name) { + const auto rxErrorHandler = [](const nlmsghdr& nlmsg, const Slice msg) { + std::stringstream ss; + ss << nlmsg << " " << msg << " " << netdutils::toHex(msg, 32); + ALOGE("unhandled netlink message: %s", ss.str().c_str()); + }; + expectOk(NetlinkListener::subscribe(kNetlinkMsgErrorType, rxErrorHandler)); + + mErrorHandler = [& name = mThreadName](const int fd, const int err) { + ALOGE("Error on NetlinkListener(%s) fd=%d: %s", name.c_str(), fd, strerror(err)); + }; + + // Start the thread + mWorker = std::thread([this]() { run().ignoreError(); }); +} + +NetlinkListener::~NetlinkListener() { + const auto& sys = sSyscalls.get(); + const uint64_t data = 1; + // eventfd should never enter an error state unexpectedly + expectOk(sys.write(mEvent, makeSlice(data)).status()); + mWorker.join(); +} + +Status NetlinkListener::send(const Slice msg) { + const auto& sys = sSyscalls.get(); + ASSIGN_OR_RETURN(auto sent, sys.sendto(mSock, msg, 0, kKernelAddr)); + if (sent != msg.size()) { + return statusFromErrno(EMSGSIZE, "unexpect message size"); + } + return ok; +} + +Status NetlinkListener::subscribe(uint16_t type, const DispatchFn& fn) { + std::lock_guard guard(mMutex); + mDispatchMap[type] = fn; + return ok; +} + +Status NetlinkListener::unsubscribe(uint16_t type) { + std::lock_guard guard(mMutex); + mDispatchMap.erase(type); + return ok; +} + +void NetlinkListener::registerSkErrorHandler(const SkErrorHandler& handler) { + mErrorHandler = handler; +} + +Status NetlinkListener::run() { + std::vector<char> rxbuf(4096); + + const auto rxHandler = [this](const nlmsghdr& nlmsg, const Slice& buf) { + std::lock_guard guard(mMutex); + const auto& fn = findWithDefault(mDispatchMap, nlmsg.nlmsg_type, kDefaultDispatchFn); + fn(nlmsg, buf); + }; + + if (mThreadName.length() > 0) { + int ret = pthread_setname_np(pthread_self(), mThreadName.c_str()); + if (ret) { + ALOGE("thread name set failed, name: %s, ret: %s", mThreadName.c_str(), strerror(ret)); + } + } + const auto& sys = sSyscalls.get(); + const std::array<Fd, 2> fds{{{mEvent}, {mSock}}}; + const int events = POLLIN; + const double timeout = 3600; + while (true) { + ASSIGN_OR_RETURN(auto revents, sys.ppoll(fds, events, timeout)); + // After mEvent becomes readable, we should stop servicing mSock and return + if (revents[0] & POLLIN) { + break; + } + if (revents[1] & (POLLIN|POLLERR)) { + auto rx = sys.recvfrom(mSock, makeSlice(rxbuf), 0); + int err = rx.status().code(); + if (err) { + // Ignore errors. The only error we expect to see here is ENOBUFS, and there's + // nothing we can do about that. The recvfrom above will already have cleared the + // error indication and ensured we won't get EPOLLERR again. + // TODO: Consider using NETLINK_NO_ENOBUFS. + mErrorHandler(((Fd) mSock).get(), err); + continue; + } + forEachNetlinkMessage(rx.value(), rxHandler); + } + } + return ok; +} + +} // namespace netdutils +} // namespace android diff --git a/common/netd/libnetdutils/include/netdutils/NetlinkListener.h b/common/netd/libnetdutils/include/netdutils/NetlinkListener.h new file mode 100644 index 00000000..97f7bb23 --- /dev/null +++ b/common/netd/libnetdutils/include/netdutils/NetlinkListener.h @@ -0,0 +1,105 @@ +/* + * 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. + */ + +#ifndef NETLINK_LISTENER_H +#define NETLINK_LISTENER_H + +#include <functional> +#include <map> +#include <mutex> +#include <thread> + +#include <android-base/thread_annotations.h> +#include <netdutils/Netlink.h> +#include <netdutils/Slice.h> +#include <netdutils/Status.h> +#include <netdutils/UniqueFd.h> + +namespace android { +namespace netdutils { + +class NetlinkListenerInterface { + public: + using DispatchFn = std::function<void(const nlmsghdr& nlmsg, const netdutils::Slice msg)>; + + using SkErrorHandler = std::function<void(const int fd, const int err)>; + + virtual ~NetlinkListenerInterface() = default; + + // Send message to the kernel using the underlying netlink socket + virtual netdutils::Status send(const netdutils::Slice msg) = 0; + + // Deliver future messages with nlmsghdr.nlmsg_type == type to fn. + // + // Threadsafe. + // All dispatch functions invoked on a single service thread. + // subscribe() and join() must not be called from the stack of fn(). + virtual netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) = 0; + + // Halt delivery of future messages with nlmsghdr.nlmsg_type == type. + // Threadsafe. + virtual netdutils::Status unsubscribe(uint16_t type) = 0; + + virtual void registerSkErrorHandler(const SkErrorHandler& handler) = 0; +}; + +// NetlinkListener manages a netlink socket and associated blocking +// service thread. +// +// This class is written in a generic way to allow multiple different +// netlink subsystems to share this common infrastructure. If multiple +// subsystems share the same message delivery requirements (drops ok, +// no drops) they may share a single listener by calling subscribe() +// with multiple types. +// +// This class is suitable for moderate performance message +// processing. In particular it avoids extra copies of received +// message data and allows client code to control which message +// attributes are processed. +// +// Note that NetlinkListener is capable of processing multiple batched +// netlink messages in a single system call. This is useful to +// netfilter extensions that allow batching of events like NFLOG. +class NetlinkListener : public NetlinkListenerInterface { + public: + NetlinkListener(netdutils::UniqueFd event, netdutils::UniqueFd sock, const std::string& name); + + ~NetlinkListener() override; + + netdutils::Status send(const netdutils::Slice msg) override; + + netdutils::Status subscribe(uint16_t type, const DispatchFn& fn) override EXCLUDES(mMutex); + + netdutils::Status unsubscribe(uint16_t type) override EXCLUDES(mMutex); + + void registerSkErrorHandler(const SkErrorHandler& handler) override; + + private: + netdutils::Status run(); + + const netdutils::UniqueFd mEvent; + const netdutils::UniqueFd mSock; + const std::string mThreadName; + std::mutex mMutex; + std::map<uint16_t, DispatchFn> mDispatchMap GUARDED_BY(mMutex); + std::thread mWorker; + SkErrorHandler mErrorHandler; +}; + +} // namespace netdutils +} // namespace android + +#endif /* NETLINK_LISTENER_H */ diff --git a/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt index ff839fe8..2785ea90 100644 --- a/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt +++ b/common/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt @@ -16,12 +16,16 @@ package com.android.net.module.util +import android.net.NetworkStats +import android.text.TextUtils import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock @RunWith(AndroidJUnit4::class) @SmallTest @@ -71,4 +75,68 @@ class NetworkStatsUtilsTest { assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11)) assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11)) } + + @Test + fun testBucketToEntry() { + val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL, + android.app.usage.NetworkStats.Bucket.TAG_NONE, + android.app.usage.NetworkStats.Bucket.STATE_DEFAULT, + android.app.usage.NetworkStats.Bucket.METERED_YES, + android.app.usage.NetworkStats.Bucket.ROAMING_NO, + android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12) + val entry = NetworkStatsUtils.fromBucket(bucket) + val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL, + NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES, + NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12, + 0 /* operations */) + + // TODO: Use assertEquals once all downstreams accept null iface in + // NetworkStats.Entry#equals. + assertEntryEquals(expectedEntry, entry) + } + + private fun makeMockBucket( + uid: Int, + tag: Int, + state: Int, + metered: Int, + roaming: Int, + defaultNetwork: Int, + rxBytes: Long, + rxPackets: Long, + txBytes: Long, + txPackets: Long + ): android.app.usage.NetworkStats.Bucket { + val ret: android.app.usage.NetworkStats.Bucket = + mock(android.app.usage.NetworkStats.Bucket::class.java) + doReturn(uid).`when`(ret).getUid() + doReturn(tag).`when`(ret).getTag() + doReturn(state).`when`(ret).getState() + doReturn(metered).`when`(ret).getMetered() + doReturn(roaming).`when`(ret).getRoaming() + doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus() + doReturn(rxBytes).`when`(ret).getRxBytes() + doReturn(rxPackets).`when`(ret).getRxPackets() + doReturn(txBytes).`when`(ret).getTxBytes() + doReturn(txPackets).`when`(ret).getTxPackets() + return ret + } + + /** + * Assert that the two {@link NetworkStats.Entry} are equals. + */ + private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) { + TextUtils.equals(left.iface, right.iface) + assertEquals(left.uid, right.uid) + assertEquals(left.set, right.set) + assertEquals(left.tag, right.tag) + assertEquals(left.metered, right.metered) + assertEquals(left.roaming, right.roaming) + assertEquals(left.defaultNetwork, right.defaultNetwork) + assertEquals(left.rxBytes, right.rxBytes) + assertEquals(left.rxPackets, right.rxPackets) + assertEquals(left.txBytes, right.txBytes) + assertEquals(left.txPackets, right.txPackets) + assertEquals(left.operations, right.operations) + } }
\ No newline at end of file diff --git a/common/testutils/Android.bp b/common/testutils/Android.bp index 1be64c1e..1a1328f5 100644 --- a/common/testutils/Android.bp +++ b/common/testutils/Android.bp @@ -28,9 +28,11 @@ java_library { ], libs: [ "androidx.annotation_annotation", + "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap. ], static_libs: [ "androidx.test.ext.junit", + "compatibility-device-util-axt", "kotlin-reflect", "libnanohttpd", "net-tests-utils-host-device-common", @@ -79,6 +81,6 @@ java_test_host { "host/**/*.kt", ], libs: ["tradefed"], - test_suites: ["device-tests", "general-tests", "cts", "mts"], + test_suites: ["device-tests", "general-tests", "cts", "mts-networking"], data: [":ConnectivityChecker"], } diff --git a/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt index 201bf2d2..8b58e717 100644 --- a/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt +++ b/common/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt @@ -18,11 +18,15 @@ package com.android.testutils import android.os.Build import com.android.modules.utils.build.SdkLevel +import kotlin.test.fail import org.junit.Assume.assumeTrue import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement +// TODO: Remove it when Build.VERSION_CODES.SC_V2 is available +const val SC_V2 = 32 + /** * Returns true if the development SDK version of the device is in the provided range. * @@ -40,8 +44,10 @@ private fun isDevSdkAfter(minExclusive: Int): Boolean { // For recent SDKs that still have development builds used for testing, use SdkLevel utilities // instead of SDK_INT. return when (minExclusive) { - // TODO: use Build.VERSION_CODES.S when it is not CURRENT_DEVELOPMENT - 31 -> SdkLevel.isAtLeastT() + // TODO: Use Build.VERSION_CODES.SC_V2 when it is available + SC_V2 -> SdkLevel.isAtLeastT() + // TODO: To use SdkLevel.isAtLeastSv2 when available + Build.VERSION_CODES.S -> fail("Do you expect to ignore the test until T? Use SC_V2 instead") Build.VERSION_CODES.R -> SdkLevel.isAtLeastS() // Development builds of SDK versions <= R are not used anymore else -> Build.VERSION.SDK_INT > minExclusive @@ -50,8 +56,11 @@ private fun isDevSdkAfter(minExclusive: Int): Boolean { private fun isDevSdkUpTo(maxInclusive: Int): Boolean { return when (maxInclusive) { - // TODO: use Build.VERSION_CODES.S when it is not CURRENT_DEVELOPMENT - 31 -> !SdkLevel.isAtLeastT() + // TODO: Use Build.VERSION_CODES.SC_V2 when it is available + SC_V2 -> !SdkLevel.isAtLeastT() + // TODO: To use SdkLevel.isAtLeastSv2 when available + Build.VERSION_CODES.S -> + fail("Do you expect to ignore the test before T? Use SC_V2 instead") Build.VERSION_CODES.R -> !SdkLevel.isAtLeastS() // Development builds of SDK versions <= R are not used anymore else -> Build.VERSION.SDK_INT <= maxInclusive diff --git a/common/testutils/devicetests/com/android/testutils/DumpTestUtils.java b/common/testutils/devicetests/com/android/testutils/DumpTestUtils.java new file mode 100644 index 00000000..f2ad1e27 --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/DumpTestUtils.java @@ -0,0 +1,129 @@ +/* + * 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.testutils; + +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.system.ErrnoException; +import android.system.Os; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utilities for testing output of service dumps. + */ +public class DumpTestUtils { + + private static String dumpService(String serviceName, boolean adoptPermission, String... args) + throws RemoteException, InterruptedException, ErrnoException { + final IBinder ib = ServiceManager.getService(serviceName); + FileDescriptor[] pipe = Os.pipe(); + + // Start a thread to read the dump output, or dump might block if it fills the pipe. + final CountDownLatch latch = new CountDownLatch(1); + AtomicReference<String> output = new AtomicReference<>(); + // Used to send exceptions back to the main thread to ensure that the test fails cleanly. + AtomicReference<Exception> exception = new AtomicReference<>(); + new Thread(() -> { + try { + output.set(Streams.readFully( + new InputStreamReader(new FileInputStream(pipe[0]), + StandardCharsets.UTF_8))); + latch.countDown(); + } catch (Exception e) { + exception.set(e); + latch.countDown(); + } + }).start(); + + final int timeoutMs = 5_000; + final String what = "service '" + serviceName + "' with args: " + Arrays.toString(args); + try { + if (adoptPermission) { + runWithShellPermissionIdentity(() -> ib.dump(pipe[1], args), + android.Manifest.permission.DUMP); + } else { + ib.dump(pipe[1], args); + } + IoUtils.closeQuietly(pipe[1]); + assertTrue("Dump of " + what + " timed out after " + timeoutMs + "ms", + latch.await(timeoutMs, TimeUnit.MILLISECONDS)); + } finally { + // Closing the fds will terminate the thread if it's blocked on read. + IoUtils.closeQuietly(pipe[0]); + if (pipe[1].valid()) IoUtils.closeQuietly(pipe[1]); + } + if (exception.get() != null) { + fail("Exception dumping " + what + ": " + exception.get()); + } + return output.get(); + } + + /** + * Dumps the specified service and returns a string. Sends a dump IPC to the given service + * with the specified args and a pipe, then reads from the pipe in a separate thread. + * The current process must already have the DUMP permission. + * + * @param serviceName the service to dump. + * @param args the arguments to pass to the dump function. + * @return The dump text. + * @throws RemoteException dumping the service failed. + * @throws InterruptedException the dump timed out. + * @throws ErrnoException opening or closing the pipe for the dump failed. + */ + public static String dumpService(String serviceName, String... args) + throws RemoteException, InterruptedException, ErrnoException { + return dumpService(serviceName, false, args); + } + + /** + * Dumps the specified service and returns a string. Sends a dump IPC to the given service + * with the specified args and a pipe, then reads from the pipe in a separate thread. + * Adopts the {@code DUMP} permission via {@code adoptShellPermissionIdentity} and then releases + * it. This method should not be used if the caller already has the shell permission identity. + * TODO: when Q and R are no longer supported, use + * {@link android.app.UiAutomation#getAdoptedShellPermissions} to automatically acquire the + * shell permission if the caller does not already have it. + * + * @param serviceName the service to dump. + * @param args the arguments to pass to the dump function. + * @return The dump text. + * @throws RemoteException dumping the service failed. + * @throws InterruptedException the dump timed out. + * @throws ErrnoException opening or closing the pipe for the dump failed. + */ + public static String dumpServiceWithShellPermission(String serviceName, String... args) + throws RemoteException, InterruptedException, ErrnoException { + return dumpService(serviceName, true, args); + } +} diff --git a/common/testutils/devicetests/com/android/testutils/TestBpfMap.java b/common/testutils/devicetests/com/android/testutils/TestBpfMap.java new file mode 100644 index 00000000..5614a99a --- /dev/null +++ b/common/testutils/devicetests/com/android/testutils/TestBpfMap.java @@ -0,0 +1,136 @@ +/* + * 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.testutils; + +import android.system.ErrnoException; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.BpfMap; +import com.android.net.module.util.Struct; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * + * Fake BPF map class for tests that have no no privilege to access real BPF maps. All member + * functions which eventually call JNI to access the real native BPF map are overridden. + * + * Inherits from BpfMap instead of implementing IBpfMap so that any class using a BpfMap can use + * this class in its tests. + * + * @param <K> the key type + * @param <V> the value type + */ +public class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> { + private final HashMap<K, V> mMap = new HashMap<K, V>(); + + public 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 void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException { + if (!mMap.containsKey(key)) throw new NoSuchElementException(); + mMap.put(key, value); + } + + @Override + public boolean insertOrReplaceEntry(K key, V value) throws ErrnoException { + // Returns true if inserted, false if replaced. + boolean ret = !mMap.containsKey(key); + mMap.put(key, value); + return ret; + } + + @Override + public boolean deleteEntry(Struct key) throws ErrnoException { + return mMap.remove(key) != null; + } + + @Override + public boolean isEmpty() throws ErrnoException { + return mMap.isEmpty(); + } + + @Override + public K getNextKey(@NonNull K key) { + // Expensive, but since this is only for tests... + Iterator<K> it = mMap.keySet().iterator(); + while (it.hasNext()) { + if (Objects.equals(it.next(), key)) { + return it.hasNext() ? it.next() : null; + } + } + return null; + } + + @Override + public K getFirstKey() { + for (K key : mMap.keySet()) { + return key; + } + return null; + } + + @Override + public boolean containsKey(@NonNull K key) throws ErrnoException { + return mMap.containsKey(key); + } + + @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(); + } +} diff --git a/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt index 40fb7733..8dc1bc45 100644 --- a/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt +++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt @@ -30,6 +30,7 @@ import com.android.net.module.util.ArrayTrackRecord import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested +import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted @@ -89,6 +90,7 @@ public open class TestableNetworkAgent( data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry() object OnNetworkCreated : CallbackEntry() object OnNetworkDestroyed : CallbackEntry() + data class OnDscpPolicyStatusUpdated(val policyId: Int, val status: Int) : CallbackEntry() data class OnRegisterQosCallback( val callbackId: Int, val filter: QosFilter @@ -162,6 +164,10 @@ public open class TestableNetworkAgent( history.add(OnNetworkDestroyed) } + override fun onDscpPolicyStatusUpdated(policyId: Int, status: Int) { + history.add(OnDscpPolicyStatusUpdated(policyId, status)) + } + // Expects the initial validation event that always occurs immediately after registering // a NetworkAgent whose network does not require validation (which test networks do // not, since they lack the INTERNET capability). It always contains the default argument @@ -197,4 +203,4 @@ public open class TestableNetworkAgent( "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms") assertNull(history.peek()) } -}
\ No newline at end of file +} |