From 4ab5d9fe561c2117c8e97de5b71595a280dd9ed0 Mon Sep 17 00:00:00 2001 From: Tyler Wear Date: Tue, 19 Oct 2021 11:01:46 -0700 Subject: bpfmap: Move to Common Util Location Multiple packages need access to bpf maps. Moving to common location to allow access from all necessary packages. Test: atest BpfMapTest Bug: 179733303 Change-Id: Idae7b620c15c781b2e7980c3a3157f396cfaf66e --- common/Android.bp | 1 + .../device/com/android/net/module/util/BpfMap.java | 288 +++++++++++++++++++++ common/native/bpf_syscall_wrappers/Android.bp | 1 + common/native/bpfmapjni/Android.bp | 44 ++++ .../com_android_net_module_util_BpfMap.cpp | 136 ++++++++++ 5 files changed, 470 insertions(+) create mode 100644 common/device/com/android/net/module/util/BpfMap.java create mode 100644 common/native/bpfmapjni/Android.bp create mode 100644 common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp diff --git a/common/Android.bp b/common/Android.bp index 6a56889c..f1e7c85e 100644 --- a/common/Android.bp +++ b/common/Android.bp @@ -100,6 +100,7 @@ java_library { // an individual Struct library, and remove the net-utils-framework-common lib dependency. // But there is no need doing this at the moment. srcs: [ + "device/com/android/net/module/util/BpfMap.java", "device/com/android/net/module/util/HexDump.java", "device/com/android/net/module/util/Ipv6Utils.java", "device/com/android/net/module/util/Struct.java", diff --git a/common/device/com/android/net/module/util/BpfMap.java b/common/device/com/android/net/module/util/BpfMap.java new file mode 100644 index 00000000..aa741528 --- /dev/null +++ b/common/device/com/android/net/module/util/BpfMap.java @@ -0,0 +1,288 @@ +/* + * 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.net.module.util; + +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 androidx.annotation.VisibleForTesting; + +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 { + static { + System.loadLibrary("tetherutilsjni"); + } + + // 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); + } + + /** + * Constructor for testing only. + * The derived class implements an internal mocked map. It need to implement all functions + * which are related with the native BPF map because the BPF map handler is not initialized. + * See BpfCoordinatorTest#TestBpfMap. + */ + @VisibleForTesting + protected BpfMap(final Class key, final Class value) { + mMapFd = -1; + 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. + * (use insertOrReplaceEntry() if you need to know whether insert or replace happened) + */ + 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; + } + } + + /** + * 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) + * Note: see inline comment below if running concurrently with delete operations. + */ + public boolean insertOrReplaceEntry(K key, V value) + throws ErrnoException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST); + return true; /* insert succeeded */ + } catch (ErrnoException e) { + if (e.errno != EEXIST) throw e; + } + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST); + return false; /* replace succeeded */ + } catch (ErrnoException e) { + if (e.errno != ENOENT) throw e; + } + /* If we reach here somebody deleted after our insert attempt and before our replace: + * this implies a race happened. The kernel bpf delete interface only takes a key, + * and not the value, so we can safely pretend the replace actually succeeded and + * was immediately followed by the other thread's delete, since the delete cannot + * observe the potential change to the value. + */ + return false; /* pretend replace succeeded */ + } + + /** 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()); + } + + /** Returns {@code true} if this map contains no elements. */ + public boolean isEmpty() throws ErrnoException { + return getFirstKey() == null; + } + + 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(@NonNull 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 ErrnoException { + closeMap(mMapFd); + } + + /** + * Clears the map. The map may already be empty. + * + * @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. + */ + public void clear() throws ErrnoException { + K key = getFirstKey(); + while (key != null) { + deleteEntry(key); // ignores ENOENT. + key = getFirstKey(); + } + } + + 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/common/native/bpf_syscall_wrappers/Android.bp b/common/native/bpf_syscall_wrappers/Android.bp index 136342c7..1416b6bc 100644 --- a/common/native/bpf_syscall_wrappers/Android.bp +++ b/common/native/bpf_syscall_wrappers/Android.bp @@ -33,6 +33,7 @@ cc_library_headers { "com.android.tethering", ], visibility: [ + "//frameworks/libs/net/common/native/bpfmapjni", "//packages/modules/Connectivity/service", "//packages/modules/Connectivity/Tethering", "//system/bpf/libbpf_android", diff --git a/common/native/bpfmapjni/Android.bp b/common/native/bpfmapjni/Android.bp new file mode 100644 index 00000000..edbae7ce --- /dev/null +++ b/common/native/bpfmapjni/Android.bp @@ -0,0 +1,44 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_static { + name: "libbpfmapjni", + srcs: ["com_android_net_module_util_BpfMap.cpp"], + header_libs: [ + "bpf_syscall_wrappers", + "jni_headers", + ], + shared_libs: [ + "liblog", + "libnativehelper_compat_libc++", + ], + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + ], + sdk_version: "30", + min_sdk_version: "30", + apex_available: [ + "com.android.tethering", + "//apex_available:platform", + ], + visibility: [ + "//packages/modules/Connectivity/Tethering", + ], +} diff --git a/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp new file mode 100644 index 00000000..e25e17de --- /dev/null +++ b/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp @@ -0,0 +1,136 @@ +/* + * 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 "nativehelper/scoped_primitive_array.h" +#include "nativehelper/scoped_utf_chars.h" + +#define BPF_FD_JUST_USE_INT +#include "BpfSyscallWrappers.h" + +namespace android { + +static jint com_android_net_module_util_BpfMap_closeMap(JNIEnv *env, jobject clazz, + jint fd) { + int ret = close(fd); + + if (ret) jniThrowErrnoException(env, "closeMap", errno); + + return ret; +} + +static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz, + jstring path, jint mode) { + ScopedUtfChars pathname(env, path); + + jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast(mode)); + + if (fd < 0) jniThrowErrnoException(env, "bpfFdGet", errno); + + return fd; +} + +static void com_android_net_module_util_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) jniThrowErrnoException(env, "writeToMapEntry", errno); +} + +static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) { + if (ret == 0) return true; + + if (err != ENOENT) jniThrowErrnoException(env, functionName, err); + return false; +} + +static jboolean com_android_net_module_util_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_net_module_util_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_net_module_util_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_net_module_util_BpfMap_closeMap }, + { "bpfFdGet", "(Ljava/lang/String;I)I", + (void*) com_android_net_module_util_BpfMap_bpfFdGet }, + { "writeToMapEntry", "(I[B[BI)V", + (void*) com_android_net_module_util_BpfMap_writeToMapEntry }, + { "deleteMapEntry", "(I[B)Z", + (void*) com_android_net_module_util_BpfMap_deleteMapEntry }, + { "getNextMapKey", "(I[B[B)Z", + (void*) com_android_net_module_util_BpfMap_getNextMapKey }, + { "findMapEntry", "(I[B[B)Z", + (void*) com_android_net_module_util_BpfMap_findMapEntry }, + +}; + +int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name) { + return jniRegisterNativeMethods(env, + class_name, + gMethods, NELEM(gMethods)); +} + +}; // namespace android -- cgit v1.2.3