Merge "Provide a easy way to access bpf maps from java" am: 12067258b2

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/1498277

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: Id80e11ce843b4b2ef8578ee6b72368450288afaa
This commit is contained in:
Nucca Chen
2021-01-12 08:22:36 +00:00
committed by Automerger Merge Worker
11 changed files with 969 additions and 19 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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
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

View File

@@ -28,9 +28,6 @@
#include <sys/socket.h>
#include <stdio.h>
#define LOG_TAG "TetheringUtils"
#include <android/log.h>
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<void**>(&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

View File

@@ -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 <linux/bpf.h>
#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 "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<jstring> 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<jthrowable>(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<unsigned>(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<int>(fd), keyRO.get(), valueRO.get(),
static_cast<int>(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<int>(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<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_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<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_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<jclass>(env->NewGlobalRef(
env->FindClass("android/system/ErrnoException")));
if (sErrnoExceptionClass == nullptr) return JNI_ERR;
sErrnoExceptionCtor2 = env->GetMethodID(sErrnoExceptionClass, "<init>",
"(Ljava/lang/String;I)V");
if (sErrnoExceptionCtor2 == nullptr) return JNI_ERR;
sErrnoExceptionCtor3 = env->GetMethodID(sErrnoExceptionClass, "<init>",
"(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

42
Tethering/jni/onload.cpp Normal file
View File

@@ -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 <nativehelper/JNIHelp.h>
#include "jni.h"
#define LOG_TAG "TetheringJni"
#include <android/log.h>
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<void**>(&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

View File

@@ -4,6 +4,14 @@
static final int EVENT_*;
}
-keep class com.android.networkstack.tethering.BpfMap {
native <methods>;
}
-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
public <init>(...);
}
-keepclassmembers class android.net.ip.IpServer {
static final int CMD_*;
}

View File

@@ -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 <K> the key of the map.
* @param <V> the value of the map.
*/
public class BpfMap<K extends Struct, V extends Struct> 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<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);
}
/**
* 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<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 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;
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
final ArrayMap<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
final ArrayMap<TetherIngressKey, TetherIngressValue> 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<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
final ArrayMap<TetherIngressKey, TetherIngressValue> 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);
}
}
}
}