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
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 6a56889..f1e7c85 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -100,6 +100,7 @@
     // 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/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
new file mode 100644
index 0000000..aa74152
--- /dev/null
+++ b/staticlibs/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 <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public class BpfMap<K extends Struct, V extends Struct> 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<K> mKeyClass;
+    private final Class<V> 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<K> key,
+            final Class<V> 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<K> key, final Class<V> 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<K, V> 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/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index 136342c..1416b6b 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -33,6 +33,7 @@
         "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/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
new file mode 100644
index 0000000..edbae7c
--- /dev/null
+++ b/staticlibs/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/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
new file mode 100644
index 0000000..e25e17d
--- /dev/null
+++ b/staticlibs/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 <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#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<unsigned>(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<int>(fd), keyRO.get(), valueRO.get(),
+            static_cast<int>(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<int>(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<int>(fd), nullptr, nextKeyRW.get());
+    } else {
+        ScopedByteArrayRO keyRO(env, key);
+        ret = bpf::getNextMapKey(static_cast<int>(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<int>(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