From 7052688dde014de45315c368287e07049b32cfc4 Mon Sep 17 00:00:00 2001 From: markchien Date: Thu, 12 Nov 2020 00:17:15 +0800 Subject: [PATCH] Provide a easy way to access bpf maps from java A wrapper for bpf map opening, reading/writing, and iteration. Bug: 173167302 Test: atest BpfMapTest Change-Id: I792b41978b322c9e4969cd7b6c35d6978ab86bc4 --- Tethering/Android.bp | 6 +- Tethering/bpf_progs/offload.c | 4 + Tethering/jarjar-rules.txt | 5 +- .../jni/android_net_util_TetheringUtils.cpp | 17 - ..._android_networkstack_tethering_BpfMap.cpp | 176 +++++++++ Tethering/jni/onload.cpp | 42 +++ Tethering/proguard.flags | 8 + .../networkstack/tethering/BpfMap.java | 222 +++++++++++ .../tethering/TetherIngressKey.java | 72 ++++ .../tethering/TetherIngressValue.java | 80 ++++ .../networkstack/tethering/BpfMapTest.java | 356 ++++++++++++++++++ 11 files changed, 969 insertions(+), 19 deletions(-) create mode 100644 Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp create mode 100644 Tethering/jni/onload.cpp create mode 100644 Tethering/src/com/android/networkstack/tethering/BpfMap.java create mode 100644 Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java create mode 100644 Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java create mode 100644 Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java diff --git a/Tethering/Android.bp b/Tethering/Android.bp index d8557adc61..0c9801c4fe 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -60,8 +60,12 @@ cc_library { "com.android.tethering", ], min_sdk_version: "30", + include_dirs: [ + // TODO: use the libbpf_android_headers instead of just including the header files. + "system/bpf/libbpf_android/include/", + ], srcs: [ - "jni/android_net_util_TetheringUtils.cpp", + "jni/*.cpp", ], shared_libs: [ "liblog", diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c index cc5af3127b..d8dc60dc1a 100644 --- a/Tethering/bpf_progs/offload.c +++ b/Tethering/bpf_progs/offload.c @@ -34,6 +34,10 @@ DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, uint32_t, TetherStatsValue, 16, AID_N // (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif]) DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, uint32_t, uint64_t, 16, AID_NETWORK_STACK) +// Used only by TetheringPrivilegedTests, not by production code. +DEFINE_BPF_MAP_GRW(tether_ingress_map_TEST, HASH, TetherIngressKey, TetherIngressValue, 16, + AID_NETWORK_STACK) + static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethernet) { int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0; void* data = (void*)(long)skb->data; diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt index 591861f5b8..d1ad569e0d 100644 --- a/Tethering/jarjar-rules.txt +++ b/Tethering/jarjar-rules.txt @@ -8,4 +8,7 @@ rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1 # Classes from net-utils-framework-common -rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1 \ No newline at end of file +rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1 + +# Classes from net-utils-device-common +rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1 diff --git a/Tethering/jni/android_net_util_TetheringUtils.cpp b/Tethering/jni/android_net_util_TetheringUtils.cpp index 7bfb6dab4a..27c84cf280 100644 --- a/Tethering/jni/android_net_util_TetheringUtils.cpp +++ b/Tethering/jni/android_net_util_TetheringUtils.cpp @@ -28,9 +28,6 @@ #include #include -#define LOG_TAG "TetheringUtils" -#include - namespace android { static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt); @@ -184,18 +181,4 @@ int register_android_net_util_TetheringUtils(JNIEnv* env) { gMethods, NELEM(gMethods)); } -extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { - JNIEnv *env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ERROR: GetEnv failed"); - return JNI_ERR; - } - - if (register_android_net_util_TetheringUtils(env) < 0) { - return JNI_ERR; - } - - return JNI_VERSION_1_6; -} - }; // namespace android diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp new file mode 100644 index 0000000000..64f7dcfa3c --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2020 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 +#include +#include +#include +#include + +#include "nativehelper/scoped_primitive_array.h" +#include "nativehelper/scoped_utf_chars.h" + +#define BPF_FD_JUST_USE_INT +#include "bpf/BpfUtils.h" + +namespace android { + +static jclass sErrnoExceptionClass; +static jmethodID sErrnoExceptionCtor2; +static jmethodID sErrnoExceptionCtor3; + +static void throwErrnoException(JNIEnv* env, const char* functionName, int error) { + if (sErrnoExceptionClass == nullptr || sErrnoExceptionClass == nullptr) return; + + jthrowable cause = nullptr; + if (env->ExceptionCheck()) { + cause = env->ExceptionOccurred(); + env->ExceptionClear(); + } + + ScopedLocalRef msg(env, env->NewStringUTF(functionName)); + + // Not really much we can do here if msg is null, let's try to stumble on... + if (msg.get() == nullptr) env->ExceptionClear(); + + jobject errnoException; + if (cause != nullptr) { + errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor3, msg.get(), + error, cause); + } else { + errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor2, msg.get(), + error); + } + env->Throw(static_cast(errnoException)); +} + +static jint com_android_networkstack_tethering_BpfMap_closeMap(JNIEnv *env, jobject clazz, + jint fd) { + int ret = close(fd); + + if (ret) throwErrnoException(env, "closeMap", errno); + + return ret; +} + +static jint com_android_networkstack_tethering_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz, + jstring path, jint mode) { + ScopedUtfChars pathname(env, path); + + jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast(mode)); + + return fd; +} + +static void com_android_networkstack_tethering_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray value, jint flags) { + ScopedByteArrayRO keyRO(env, key); + ScopedByteArrayRO valueRO(env, value); + + int ret = bpf::writeToMapEntry(static_cast(fd), keyRO.get(), valueRO.get(), + static_cast(flags)); + + if (ret) throwErrnoException(env, "writeToMapEntry", errno); +} + +static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) { + if (ret == 0) return true; + + if (err != ENOENT) throwErrnoException(env, functionName, err); + return false; +} + +static jboolean com_android_networkstack_tethering_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key) { + ScopedByteArrayRO keyRO(env, key); + + // On success, zero is returned. If the element is not found, -1 is returned and errno is set + // to ENOENT. + int ret = bpf::deleteMapEntry(static_cast(fd), keyRO.get()); + + return throwIfNotEnoent(env, "deleteMapEntry", ret, errno); +} + +static jboolean com_android_networkstack_tethering_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray nextKey) { + // If key is found, the operation returns zero and sets the next key pointer to the key of the + // next element. If key is not found, the operation returns zero and sets the next key pointer + // to the key of the first element. If key is the last element, -1 is returned and errno is + // set to ENOENT. Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL. + ScopedByteArrayRW nextKeyRW(env, nextKey); + int ret; + if (key == nullptr) { + // Called by getFirstKey. Find the first key in the map. + ret = bpf::getNextMapKey(static_cast(fd), nullptr, nextKeyRW.get()); + } else { + ScopedByteArrayRO keyRO(env, key); + ret = bpf::getNextMapKey(static_cast(fd), keyRO.get(), nextKeyRW.get()); + } + + return throwIfNotEnoent(env, "getNextMapKey", ret, errno); +} + +static jboolean com_android_networkstack_tethering_BpfMap_findMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray value) { + ScopedByteArrayRO keyRO(env, key); + ScopedByteArrayRW valueRW(env, value); + + // If an element is found, the operation returns zero and stores the element's value into + // "value". If no element is found, the operation returns -1 and sets errno to ENOENT. + int ret = bpf::findMapEntry(static_cast(fd), keyRO.get(), valueRW.get()); + + return throwIfNotEnoent(env, "findMapEntry", ret, errno); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "closeMap", "(I)I", + (void*) com_android_networkstack_tethering_BpfMap_closeMap }, + { "bpfFdGet", "(Ljava/lang/String;I)I", + (void*) com_android_networkstack_tethering_BpfMap_bpfFdGet }, + { "writeToMapEntry", "(I[B[BI)V", + (void*) com_android_networkstack_tethering_BpfMap_writeToMapEntry }, + { "deleteMapEntry", "(I[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_deleteMapEntry }, + { "getNextMapKey", "(I[B[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_getNextMapKey }, + { "findMapEntry", "(I[B[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_findMapEntry }, + +}; + +int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env) { + sErrnoExceptionClass = static_cast(env->NewGlobalRef( + env->FindClass("android/system/ErrnoException"))); + if (sErrnoExceptionClass == nullptr) return JNI_ERR; + + sErrnoExceptionCtor2 = env->GetMethodID(sErrnoExceptionClass, "", + "(Ljava/lang/String;I)V"); + if (sErrnoExceptionCtor2 == nullptr) return JNI_ERR; + + sErrnoExceptionCtor3 = env->GetMethodID(sErrnoExceptionClass, "", + "(Ljava/lang/String;ILjava/lang/Throwable;)V"); + if (sErrnoExceptionCtor3 == nullptr) return JNI_ERR; + + return jniRegisterNativeMethods(env, + "com/android/networkstack/tethering/BpfMap", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp new file mode 100644 index 0000000000..3766de9076 --- /dev/null +++ b/Tethering/jni/onload.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 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 +#include "jni.h" + +#define LOG_TAG "TetheringJni" +#include + +namespace android { + +int register_android_net_util_TetheringUtils(JNIEnv* env); +int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env); + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (register_android_net_util_TetheringUtils(env) < 0) return JNI_ERR; + + if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} + +}; // namespace android diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags index 86b903353c..9ab56c2b61 100644 --- a/Tethering/proguard.flags +++ b/Tethering/proguard.flags @@ -4,6 +4,14 @@ static final int EVENT_*; } +-keep class com.android.networkstack.tethering.BpfMap { + native ; +} + +-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct { + public (...); +} + -keepclassmembers class android.net.ip.IpServer { static final int CMD_*; } diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java new file mode 100644 index 0000000000..9505709655 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2020 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.networkstack.tethering; + +import static android.system.OsConstants.EEXIST; +import static android.system.OsConstants.ENOENT; + +import android.system.ErrnoException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.net.module.util.Struct; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries. + * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by + * passing syscalls with map file descriptor. + * + * @param the key of the map. + * @param the value of the map. + */ +public class BpfMap implements AutoCloseable { + // Following definitions from kernel include/uapi/linux/bpf.h + public static final int BPF_F_RDWR = 0; + public static final int BPF_F_RDONLY = 1 << 3; + public static final int BPF_F_WRONLY = 1 << 4; + + public static final int BPF_MAP_TYPE_HASH = 1; + + private static final int BPF_F_NO_PREALLOC = 1; + + private static final int BPF_ANY = 0; + private static final int BPF_NOEXIST = 1; + private static final int BPF_EXIST = 2; + + private final int mMapFd; + private final Class mKeyClass; + private final Class mValueClass; + private final int mKeySize; + private final int mValueSize; + + /** + * Create a BpfMap map wrapper with "path" of filesystem. + * + * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY. + * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved. + * @throws NullPointerException if {@code path} is null. + */ + public BpfMap(@NonNull final String path, final int flag, final Class key, + final Class value) throws ErrnoException, NullPointerException { + mMapFd = bpfFdGet(path, flag); + + mKeyClass = key; + mValueClass = value; + mKeySize = Struct.getSize(key); + mValueSize = Struct.getSize(value); + } + + /** + * Update an existing or create a new key -> value entry in an eBbpf map. + */ + public void updateEntry(K key, V value) throws ErrnoException { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY); + } + + /** + * If the key does not exist in the map, insert key -> value entry into eBpf map. + * Otherwise IllegalStateException will be thrown. + */ + public void insertEntry(K key, V value) + throws ErrnoException, IllegalStateException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST); + } catch (ErrnoException e) { + if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists"); + + throw e; + } + } + + /** + * If the key already exists in the map, replace its value. Otherwise NoSuchElementException + * will be thrown. + */ + public void replaceEntry(K key, V value) + throws ErrnoException, NoSuchElementException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST); + } catch (ErrnoException e) { + if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found"); + + throw e; + } + } + + /** Remove existing key from eBpf map. Return false if map was not modified. */ + public boolean deleteEntry(K key) throws ErrnoException { + return deleteMapEntry(mMapFd, key.writeToBytes()); + } + + private K getNextKeyInternal(@Nullable K key) throws ErrnoException { + final byte[] rawKey = getNextRawKey( + key == null ? null : key.writeToBytes()); + if (rawKey == null) return null; + + final ByteBuffer buffer = ByteBuffer.wrap(rawKey); + buffer.order(ByteOrder.nativeOrder()); + return Struct.parse(mKeyClass, buffer); + } + + /** + * Get the next key of the passed-in key. If the passed-in key is not found, return the first + * key. If the passed-in key is the last one, return null. + * + * TODO: consider allowing null passed-in key. + */ + public K getNextKey(@Nullable K key) throws ErrnoException { + Objects.requireNonNull(key); + return getNextKeyInternal(key); + } + + private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException { + byte[] nextKey = new byte[mKeySize]; + if (getNextMapKey(mMapFd, key, nextKey)) return nextKey; + + return null; + } + + /** Get the first key of eBpf map. */ + public K getFirstKey() throws ErrnoException { + return getNextKeyInternal(null); + } + + /** Check whether a key exists in the map. */ + public boolean containsKey(@NonNull K key) throws ErrnoException { + Objects.requireNonNull(key); + + final byte[] rawValue = getRawValue(key.writeToBytes()); + return rawValue != null; + } + + /** Retrieve a value from the map. Return null if there is no such key. */ + public V getValue(@NonNull K key) throws ErrnoException { + Objects.requireNonNull(key); + final byte[] rawValue = getRawValue(key.writeToBytes()); + + if (rawValue == null) return null; + + final ByteBuffer buffer = ByteBuffer.wrap(rawValue); + buffer.order(ByteOrder.nativeOrder()); + return Struct.parse(mValueClass, buffer); + } + + private byte[] getRawValue(final byte[] key) throws ErrnoException { + byte[] value = new byte[mValueSize]; + if (findMapEntry(mMapFd, key, value)) return value; + + return null; + } + + /** + * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer. + * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any + * other structural modifications to the map, such as adding entries or deleting other entries. + * Otherwise, iteration will result in undefined behaviour. + */ + public void forEach(BiConsumer action) throws ErrnoException { + @Nullable + K nextKey = getFirstKey(); + + while (nextKey != null) { + @NonNull + final K curKey = nextKey; + @NonNull + final V value = getValue(curKey); + + nextKey = getNextKey(curKey); + action.accept(curKey, value); + } + } + + @Override + public void close() throws Exception { + closeMap(mMapFd); + } + + private static native int closeMap(int fd) throws ErrnoException; + + private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException; + + private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags) + throws ErrnoException; + + private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException; + + // If key is found, the operation returns true and the nextKey would reference to the next + // element. If key is not found, the operation returns true and the nextKey would reference to + // the first element. If key is the last element, false is returned. + private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException; + + private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException; +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java b/Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java new file mode 100644 index 0000000000..78683c5e1c --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 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.networkstack.tethering; + +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.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; + +/** The key of BpfMap which is used for bpf offload. */ +public class TetherIngressKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long iif; // The input interface index. + + @Field(order = 1, type = Type.ByteArray, arraysize = 16) + public final byte[] neigh6; // The destination IPv6 address. + + public TetherIngressKey(final long iif, final byte[] neigh6) { + try { + final Inet6Address unused = (Inet6Address) InetAddress.getByAddress(neigh6); + } catch (ClassCastException | UnknownHostException e) { + throw new IllegalArgumentException("Invalid IPv6 address: " + + Arrays.toString(neigh6)); + } + this.iif = iif; + this.neigh6 = neigh6; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherIngressKey)) return false; + + final TetherIngressKey that = (TetherIngressKey) obj; + + return iif == that.iif && Arrays.equals(neigh6, that.neigh6); + } + + @Override + public int hashCode() { + return Long.hashCode(iif) ^ Arrays.hashCode(neigh6); + } + + @Override + public String toString() { + try { + return String.format("iif: %d, neigh: %s", iif, Inet6Address.getByAddress(neigh6)); + } catch (UnknownHostException e) { + // Should not happen because construtor already verify neigh6. + throw new IllegalStateException("Invalid TetherIngressKey"); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java b/Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java new file mode 100644 index 0000000000..e2116fc2d5 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +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.util.Objects; + +/** The value of BpfMap which is used for bpf offload. */ +public class TetherIngressValue extends Struct { + @Field(order = 0, type = Type.U32) + public final long oif; // The output interface index. + + // The ethhdr struct which is defined in uapi/linux/if_ether.h + @Field(order = 1, type = Type.EUI48) + public final MacAddress ethDstMac; // The destination mac address. + @Field(order = 2, type = Type.EUI48) + public final MacAddress ethSrcMac; // The source mac address. + @Field(order = 3, type = Type.UBE16) + public final int ethProto; // Packet type ID field. + + @Field(order = 4, type = Type.U16) + public final int pmtu; // The maximum L3 output path/route mtu. + + public TetherIngressValue(final long oif, @NonNull final MacAddress ethDstMac, + @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu) { + Objects.requireNonNull(ethSrcMac); + Objects.requireNonNull(ethDstMac); + + this.oif = oif; + this.ethDstMac = ethDstMac; + this.ethSrcMac = ethSrcMac; + this.ethProto = ethProto; + this.pmtu = pmtu; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherIngressValue)) return false; + + final TetherIngressValue that = (TetherIngressValue) obj; + + return oif == that.oif && ethDstMac.equals(that.ethDstMac) + && ethSrcMac.equals(that.ethSrcMac) && ethProto == that.ethProto + && pmtu == that.pmtu; + } + + @Override + public int hashCode() { + return Objects.hash(oif, ethDstMac, ethSrcMac, ethProto, pmtu); + } + + @Override + public String toString() { + return String.format("oif: %d, dstMac: %s, srcMac: %s, proto: %d, pmtu: %d", oif, + ethDstMac, ethSrcMac, ethProto, pmtu); + } +} diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java new file mode 100644 index 0000000000..77c0961ba8 --- /dev/null +++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2020 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.networkstack.tethering; + +import static android.system.OsConstants.ETH_P_IPV6; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.MacAddress; +import android.os.Build; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.NoSuchElementException; + + +@RunWith(AndroidJUnit4.class) +@IgnoreUpTo(Build.VERSION_CODES.R) +public final class BpfMapTest { + // Sync from packages/modules/Connectivity/Tethering/bpf_progs/offload.c. + private static final int TEST_MAP_SIZE = 16; + private static final String TETHER_INGRESS_FS_PATH = + "/sys/fs/bpf/map_offload_tether_ingress_map_TEST"; + + private ArrayMap mTestData; + + @BeforeClass + public static void setupOnce() { + System.loadLibrary("tetherutilsjni"); + } + + @Before + public void setUp() throws Exception { + // TODO: Simply the test map creation and deletion. + // - Make the map a class member (mTestMap) + // - Open the test map RW in setUp + // - Close the test map in tearDown. + cleanTestMap(); + + mTestData = new ArrayMap<>(); + mTestData.put(createTetherIngressKey(101, "2001:db8::1"), + createTetherIngressValue(11, "00:00:00:00:00:0a", "11:11:11:00:00:0b", ETH_P_IPV6, + 1280)); + mTestData.put(createTetherIngressKey(102, "2001:db8::2"), + createTetherIngressValue(22, "00:00:00:00:00:0c", "22:22:22:00:00:0d", ETH_P_IPV6, + 1400)); + mTestData.put(createTetherIngressKey(103, "2001:db8::3"), + createTetherIngressValue(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f", ETH_P_IPV6, + 1500)); + } + + @After + public void tearDown() throws Exception { + cleanTestMap(); + } + + private BpfMap getTestMap() throws Exception { + return new BpfMap<>( + TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDWR, + TetherIngressKey.class, TetherIngressValue.class); + } + + private void cleanTestMap() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + bpfMap.forEach((key, value) -> { + try { + assertTrue(bpfMap.deleteEntry(key)); + } catch (ErrnoException ignored) { } + }); + assertNull(bpfMap.getFirstKey()); + } + } + + private TetherIngressKey createTetherIngressKey(long iif, String address) throws Exception { + final InetAddress ipv6Address = InetAddress.getByName(address); + + return new TetherIngressKey(iif, ipv6Address.getAddress()); + } + + private TetherIngressValue createTetherIngressValue(long oif, String src, String dst, int proto, + int pmtu) throws Exception { + final MacAddress srcMac = MacAddress.fromString(src); + final MacAddress dstMac = MacAddress.fromString(dst); + + return new TetherIngressValue(oif, dstMac, srcMac, proto, pmtu); + } + + @Test + public void testGetFd() throws Exception { + try (BpfMap readOnlyMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDONLY, + TetherIngressKey.class, TetherIngressValue.class)) { + assertNotNull(readOnlyMap); + try { + readOnlyMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + fail("Writing RO map should throw ErrnoException"); + } catch (ErrnoException expected) { + assertEquals(OsConstants.EPERM, expected.errno); + } + } + try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_WRONLY, + TetherIngressKey.class, TetherIngressValue.class)) { + assertNotNull(writeOnlyMap); + try { + writeOnlyMap.getFirstKey(); + fail("Reading WO map should throw ErrnoException"); + } catch (ErrnoException expected) { + assertEquals(OsConstants.EPERM, expected.errno); + } + } + try (BpfMap readWriteMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDWR, + TetherIngressKey.class, TetherIngressValue.class)) { + assertNotNull(readWriteMap); + } + } + + @Test + public void testGetFirstKey() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + // getFirstKey on an empty map returns null. + assertFalse(bpfMap.containsKey(mTestData.keyAt(0))); + assertNull(bpfMap.getFirstKey()); + assertNull(bpfMap.getValue(mTestData.keyAt(0))); + + // getFirstKey on a non-empty map returns the first key. + bpfMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + assertEquals(mTestData.keyAt(0), bpfMap.getFirstKey()); + } + } + + @Test + public void testGetNextKey() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + // [1] If the passed-in key is not found on empty map, return null. + final TetherIngressKey nonexistentKey = createTetherIngressKey(1234, "2001:db8::10"); + assertNull(bpfMap.getNextKey(nonexistentKey)); + + // [2] If the passed-in key is null on empty map, throw NullPointerException. + try { + bpfMap.getNextKey(null); + fail("Getting next key with null key should throw NullPointerException"); + } catch (NullPointerException expected) { } + + // The BPF map has one entry now. + final ArrayMap resultMap = new ArrayMap<>(); + bpfMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + resultMap.put(mTestData.keyAt(0), mTestData.valueAt(0)); + + // [3] If the passed-in key is the last key, return null. + // Because there is only one entry in the map, the first key equals the last key. + final TetherIngressKey lastKey = bpfMap.getFirstKey(); + assertNull(bpfMap.getNextKey(lastKey)); + + // The BPF map has two entries now. + bpfMap.insertEntry(mTestData.keyAt(1), mTestData.valueAt(1)); + resultMap.put(mTestData.keyAt(1), mTestData.valueAt(1)); + + // [4] If the passed-in key is found, return the next key. + TetherIngressKey nextKey = bpfMap.getFirstKey(); + while (nextKey != null) { + if (resultMap.remove(nextKey).equals(nextKey)) { + fail("Unexpected result: " + nextKey); + } + nextKey = bpfMap.getNextKey(nextKey); + } + assertTrue(resultMap.isEmpty()); + + // [5] If the passed-in key is not found on non-empty map, return the first key. + assertEquals(bpfMap.getFirstKey(), bpfMap.getNextKey(nonexistentKey)); + + // [6] If the passed-in key is null on non-empty map, throw NullPointerException. + try { + bpfMap.getNextKey(null); + fail("Getting next key with null key should throw NullPointerException"); + } catch (NullPointerException expected) { } + } + } + + @Test + public void testUpdateBpfMap() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + + final TetherIngressKey key = mTestData.keyAt(0); + final TetherIngressValue value = mTestData.valueAt(0); + final TetherIngressValue value2 = mTestData.valueAt(1); + assertFalse(bpfMap.deleteEntry(key)); + + // updateEntry will create an entry if it does not exist already. + bpfMap.updateEntry(key, value); + assertTrue(bpfMap.containsKey(key)); + final TetherIngressValue result = bpfMap.getValue(key); + assertEquals(value, result); + + // updateEntry will update an entry that already exists. + bpfMap.updateEntry(key, value2); + assertTrue(bpfMap.containsKey(key)); + final TetherIngressValue result2 = bpfMap.getValue(key); + assertEquals(value2, result2); + + assertTrue(bpfMap.deleteEntry(key)); + assertFalse(bpfMap.containsKey(key)); + } + } + + @Test + public void testInsertReplaceEntry() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + + final TetherIngressKey key = mTestData.keyAt(0); + final TetherIngressValue value = mTestData.valueAt(0); + final TetherIngressValue value2 = mTestData.valueAt(1); + + try { + bpfMap.replaceEntry(key, value); + fail("Replacing non-existent key " + key + " should throw NoSuchElementException"); + } catch (NoSuchElementException expected) { } + assertFalse(bpfMap.containsKey(key)); + + bpfMap.insertEntry(key, value); + assertTrue(bpfMap.containsKey(key)); + final TetherIngressValue result = bpfMap.getValue(key); + assertEquals(value, result); + try { + bpfMap.insertEntry(key, value); + fail("Inserting existing key " + key + " should throw IllegalStateException"); + } catch (IllegalStateException expected) { } + + bpfMap.replaceEntry(key, value2); + assertTrue(bpfMap.containsKey(key)); + final TetherIngressValue result2 = bpfMap.getValue(key); + assertEquals(value2, result2); + } + } + + @Test + public void testIterateBpfMap() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + final ArrayMap resultMap = + new ArrayMap<>(mTestData); + + for (int i = 0; i < resultMap.size(); i++) { + bpfMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i)); + } + + bpfMap.forEach((key, value) -> { + if (!value.equals(resultMap.remove(key))) { + fail("Unexpected result: " + key + ", value: " + value); + } + }); + assertTrue(resultMap.isEmpty()); + } + } + + @Test + public void testIterateEmptyMap() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + // Use an one element array to be a trick for a counter because local variables + // referenced from a lambda expression must be final. + final int[] count = {0}; + bpfMap.forEach((key, value) -> { + count[0]++; + }); + + // Expect that consumer has never be called. + assertEquals(0, count[0]); + } + } + + @Test + public void testIterateDeletion() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + final ArrayMap resultMap = + new ArrayMap<>(mTestData); + + for (int i = 0; i < resultMap.size(); i++) { + bpfMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i)); + } + + // Use an one element array to be a trick for a counter because local variables + // referenced from a lambda expression must be final. + final int[] count = {0}; + bpfMap.forEach((key, value) -> { + try { + assertTrue(bpfMap.deleteEntry(key)); + } catch (ErrnoException e) { + fail("Fail to delete key " + key + ": " + e); + } + if (!value.equals(resultMap.remove(key))) { + fail("Unexpected result: " + key + ", value: " + value); + } + count[0]++; + }); + assertEquals(3, count[0]); + assertTrue(resultMap.isEmpty()); + assertNull(bpfMap.getFirstKey()); + } + } + + @Test + public void testInsertOverflow() throws Exception { + try (BpfMap bpfMap = getTestMap()) { + final ArrayMap testData = new ArrayMap<>(); + + // Build test data for TEST_MAP_SIZE + 1 entries. + for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) { + testData.put(createTetherIngressKey(i, "2001:db8::1"), createTetherIngressValue( + 100, "de:ad:be:ef:00:01", "de:ad:be:ef:00:02", ETH_P_IPV6, 1500)); + } + + // Insert #TEST_MAP_SIZE test entries to the map. The map has reached the limit. + for (int i = 0; i < TEST_MAP_SIZE; i++) { + bpfMap.insertEntry(testData.keyAt(i), testData.valueAt(i)); + } + + // The map won't allow inserting any more entries. + try { + bpfMap.insertEntry(testData.keyAt(TEST_MAP_SIZE), testData.valueAt(TEST_MAP_SIZE)); + fail("Writing too many entries should throw ErrnoException"); + } catch (ErrnoException expected) { + // Expect that can't insert the entry anymore because the number of elements in the + // map reached the limit. See man-pages/bpf. + assertEquals(OsConstants.E2BIG, expected.errno); + } + } + } +}