Vendor AIDL interface for port blocking via eBPF
New Connectivity Service exposed to vendor for
restricting certain ports for use only in vendor.
Bug: 179733303
Change-Id: Iad9aff6924498ede5a08cfa5482082f094c0a90b
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index dd04d6c..586923c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -69,9 +69,10 @@
],
canned_fs_config: "canned_fs_config",
bpfs: [
+ "block.o",
"clatd.o_mainline",
- "netd.o_mainline",
"dscp_policy.o",
+ "netd.o_mainline",
"offload.o",
"test.o",
],
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 6718402..1fe0e9a 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -47,6 +47,7 @@
"//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
"//packages/modules/Connectivity/service/native",
+ "//packages/modules/Connectivity/tests/native",
"//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
"//packages/modules/Connectivity/tests/unit/jni",
"//system/netd/server",
@@ -58,6 +59,16 @@
// bpf kernel programs
//
bpf {
+ name: "block.o",
+ srcs: ["block.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ sub_dir: "net_shared",
+}
+
+bpf {
name: "dscp_policy.o",
srcs: ["dscp_policy.c"],
cflags: [
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
new file mode 100644
index 0000000..ddd9a1c
--- /dev/null
+++ b/bpf_progs/block.c
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <linux/types.h>
+#include <linux/bpf.h>
+#include <netinet/in.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+#define ALLOW 1
+#define DISALLOW 0
+
+DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
+ 1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
+
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+ if (!ctx->user_port) return ALLOW;
+
+ switch (ctx->protocol) {
+ case IPPROTO_TCP:
+ case IPPROTO_MPTCP:
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE:
+ case IPPROTO_DCCP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return ALLOW; // unknown protocols are allowed
+ }
+
+ int key = ctx->user_port >> 6;
+ int shift = ctx->user_port & 63;
+
+ uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+ // Lookup should never fail in reality, but if it does return here to keep the
+ // BPF verifier happy.
+ if (!val) return ALLOW;
+
+ if ((*val >> shift) & 1) return DISALLOW;
+ return ALLOW;
+}
+
+DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
+ bind4_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
+ bind6_block_port, KVER(5, 4, 0))
+(struct bpf_sock_addr *ctx) {
+ return block_port(ctx);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("ConnectivityNative");
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index fa86f39..e4efa926 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -21,6 +21,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.networkstack.apishim.ConstantsShim;
+import com.android.server.connectivity.ConnectivityNativeService;
import com.android.server.ethernet.EthernetService;
import com.android.server.ethernet.EthernetServiceImpl;
import com.android.server.nearby.NearbyService;
@@ -31,6 +32,7 @@
*/
public final class ConnectivityServiceInitializer extends SystemService {
private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+ private final ConnectivityNativeService mConnectivityNative;
private final ConnectivityService mConnectivity;
private final IpSecService mIpSecService;
private final NsdService mNsdService;
@@ -44,6 +46,7 @@
mEthernetServiceImpl = createEthernetService(context);
mConnectivity = new ConnectivityService(context);
mIpSecService = createIpSecService(context);
+ mConnectivityNative = createConnectivityNativeService(context);
mNsdService = createNsdService(context);
mNearbyService = createNearbyService(context);
}
@@ -65,6 +68,12 @@
publishBinderService(Context.IPSEC_SERVICE, mIpSecService, /* allowIsolated= */ false);
}
+ if (mConnectivityNative != null) {
+ Log.i(TAG, "Registering " + ConnectivityNativeService.SERVICE_NAME);
+ publishBinderService(ConnectivityNativeService.SERVICE_NAME, mConnectivityNative,
+ /* allowIsolated= */ false);
+ }
+
if (mNsdService != null) {
Log.i(TAG, "Registering " + Context.NSD_SERVICE);
publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false);
@@ -98,6 +107,19 @@
return new IpSecService(context);
}
+ /**
+ * Return ConnectivityNativeService instance, or null if current SDK is lower than T.
+ */
+ private ConnectivityNativeService createConnectivityNativeService(final Context context) {
+ if (!SdkLevel.isAtLeastT()) return null;
+ try {
+ return new ConnectivityNativeService(context);
+ } catch (UnsupportedOperationException e) {
+ Log.d(TAG, "Unable to get ConnectivityNative service", e);
+ return null;
+ }
+ }
+
/** Return NsdService instance or null if current SDK is lower than T */
private NsdService createNsdService(final Context context) {
if (!SdkLevel.isAtLeastT()) return null;
diff --git a/service/Android.bp b/service/Android.bp
index 0e6fe92..25b970a 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -19,6 +19,54 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+aidl_interface {
+ name: "connectivity_native_aidl_interface",
+ local_include_dir: "binder",
+ vendor_available: true,
+ srcs: [
+ "binder/android/net/connectivity/aidl/*.aidl",
+ ],
+ backend: {
+ java: {
+ apex_available: [
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+ },
+ ndk: {
+ apex_available: [
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+ },
+ },
+ versions: ["1"],
+
+}
+
+cc_library_static {
+ name: "connectivity_native_aidl_interface-lateststable-ndk",
+ min_sdk_version: "30",
+ whole_static_libs: [
+ "connectivity_native_aidl_interface-V1-ndk",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
+java_library {
+ name: "connectivity_native_aidl_interface-lateststable-java",
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ static_libs: [
+ "connectivity_native_aidl_interface-V1-java",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
+
// The library name match the service-connectivity jarjar rules that put the JNI utils in the
// android.net.connectivity.com.android.net.module.util package.
cc_library_shared {
@@ -35,6 +83,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
+ "libnet_utils_device_common_bpfutils",
],
shared_libs: [
"liblog",
@@ -109,6 +158,7 @@
static_libs: [
// Do not add libs here if they are already included
// in framework-connectivity
+ "connectivity_native_aidl_interface-lateststable-java",
"dnsresolver_aidl_interface-V9-java",
"modules-utils-shell-command-handler",
"net-utils-device-common",
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/.hash b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
new file mode 100644
index 0000000..4625b4b
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/.hash
@@ -0,0 +1 @@
+037b467eb02b172a3161e11bbc3dd691aebb5fce
diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+ void blockPortForBind(in int port);
+ void unblockPortForBind(in int port);
+ void unblockAllPortsForBind();
+ int[] getPortsBlockedForBind();
+}
diff --git a/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..b3985a4
--- /dev/null
+++ b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.connectivity.aidl;
+interface ConnectivityNative {
+ void blockPortForBind(in int port);
+ void unblockPortForBind(in int port);
+ void unblockAllPortsForBind();
+ int[] getPortsBlockedForBind();
+}
diff --git a/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
new file mode 100644
index 0000000..31e24b4
--- /dev/null
+++ b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.connectivity.aidl;
+
+interface ConnectivityNative {
+ /**
+ * Blocks a port from being assigned during bind(). The caller is responsible for updating
+ * /proc/sys/net/ipv4/ip_local_port_range with the port being blocked so that calls to connect()
+ * will not automatically assign one of the blocked ports.
+ * Will return success even if port was already blocked.
+ *
+ * @param port Int corresponding to port number.
+ *
+ * @throws IllegalArgumentException if the port is invalid.
+ * @throws SecurityException if the UID of the client doesn't have network stack permission.
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void blockPortForBind(in int port);
+
+ /**
+ * Unblocks a port that has previously been blocked.
+ * Will return success even if port was already unblocked.
+ *
+ * @param port Int corresponding to port number.
+ *
+ * @throws IllegalArgumentException if the port is invalid.
+ * @throws SecurityException if the UID of the client doesn't have network stack permission.
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void unblockPortForBind(in int port);
+
+ /**
+ * Unblocks all ports that have previously been blocked.
+ */
+ void unblockAllPortsForBind();
+
+ /**
+ * Gets the list of ports that have been blocked.
+ *
+ * @return List of blocked ports.
+ */
+ int[] getPortsBlockedForBind();
+}
\ No newline at end of file
diff --git a/service/jni/com_android_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp
index 2f09e55..d91eb03 100644
--- a/service/jni/com_android_net_module_util/onload.cpp
+++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -21,6 +21,7 @@
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -35,6 +36,9 @@
if (register_com_android_net_module_util_TcUtils(env,
"android/net/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR;
+ if (register_com_android_net_module_util_BpfUtils(env,
+ "android/net/connectivity/com/android/net/module/util/BpfUtils") < 0) return JNI_ERR;
+
return JNI_VERSION_1_6;
}
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
new file mode 100644
index 0000000..cde6ea7
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -0,0 +1,176 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.connectivity.aidl.ConnectivityNative;
+import android.os.Binder;
+import android.os.Process;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BpfBitmap;
+import com.android.net.module.util.BpfUtils;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class ConnectivityNativeService extends ConnectivityNative.Stub {
+ public static final String SERVICE_NAME = "connectivity_native";
+
+ private static final String TAG = ConnectivityNativeService.class.getSimpleName();
+ private static final String CGROUP_PATH = "/sys/fs/cgroup";
+ private static final String V4_PROG_PATH =
+ "/sys/fs/bpf/prog_block_bind4_block_port";
+ private static final String V6_PROG_PATH =
+ "/sys/fs/bpf/prog_block_bind6_block_port";
+ private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map";
+
+ private final Context mContext;
+
+ // BPF map for port blocking. Exactly 65536 entries long, with one entry per port number
+ @Nullable
+ private final BpfBitmap mBpfBlockedPortsMap;
+
+ /**
+ * Dependencies of ConnectivityNativeService, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get BPF maps. */
+ @Nullable public BpfBitmap getBlockPortsMap() {
+ try {
+ return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);
+ } catch (ErrnoException e) {
+ throw new UnsupportedOperationException("Failed to create blocked ports map: "
+ + e);
+ }
+ }
+ }
+
+ private void enforceBlockPortPermission() {
+ final int uid = Binder.getCallingUid();
+ if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ }
+
+ private void ensureValidPortNumber(int port) {
+ if (port < 0 || port > 65535) {
+ throw new IllegalArgumentException("Invalid port number " + port);
+ }
+ }
+
+ public ConnectivityNativeService(final Context context) {
+ this(context, new Dependencies());
+ }
+
+ @VisibleForTesting
+ protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
+ mContext = context;
+ mBpfBlockedPortsMap = deps.getBlockPortsMap();
+ attachProgram();
+ }
+
+ @Override
+ public void blockPortForBind(int port) {
+ enforceBlockPortPermission();
+ ensureValidPortNumber(port);
+ try {
+ mBpfBlockedPortsMap.set(port);
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, e.getMessage());
+ }
+ }
+
+ @Override
+ public void unblockPortForBind(int port) {
+ enforceBlockPortPermission();
+ ensureValidPortNumber(port);
+ try {
+ mBpfBlockedPortsMap.unset(port);
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Could not unset bitmap value for (port: " + port + "): " + e);
+ }
+ }
+
+ @Override
+ public void unblockAllPortsForBind() {
+ enforceBlockPortPermission();
+ try {
+ mBpfBlockedPortsMap.clear();
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Could not clear map: " + e);
+ }
+ }
+
+ @Override
+ public int[] getPortsBlockedForBind() {
+ enforceBlockPortPermission();
+
+ ArrayList<Integer> portMap = new ArrayList<Integer>();
+ for (int i = 0; i <= 65535; i++) {
+ try {
+ if (mBpfBlockedPortsMap.get(i)) portMap.add(i);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to get index " + i, e);
+ }
+ }
+ return CollectionUtils.toIntArray(portMap);
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return this.HASH;
+ }
+
+ /**
+ * Attach BPF program
+ */
+ private void attachProgram() {
+ try {
+ BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
+ } catch (IOException e) {
+ throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
+ + e);
+ }
+ try {
+ BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
+ } catch (IOException e) {
+ throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
+ + e);
+ }
+ Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
+ }
+}
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
new file mode 100644
index 0000000..cd438f6
--- /dev/null
+++ b/tests/native/Android.bp
@@ -0,0 +1,30 @@
+cc_test {
+ name: "connectivity_native_test",
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ "vts",
+ ],
+ min_sdk_version: "31",
+ require_root: true,
+ tidy: false,
+ srcs: [
+ "connectivity_native_test.cpp",
+ ],
+ header_libs: ["bpf_connectivity_headers"],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ "liblog",
+ "libnetutils",
+ "libprocessgroup",
+ ],
+ static_libs: [
+ "connectivity_native_aidl_interface-lateststable-ndk",
+ "libcutils",
+ "libmodules-utils-build",
+ "libutils",
+ ],
+ compile_multilib: "first",
+ defaults: ["connectivity-mainline-presubmit-cc-defaults"],
+}
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
new file mode 100644
index 0000000..8b089ab
--- /dev/null
+++ b/tests/native/connectivity_native_test.cpp
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android-modules-utils/sdk_level.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
+#include <gtest/gtest.h>
+#include <netinet/in.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
+
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+
+using aidl::android::net::connectivity::aidl::IConnectivityNative;
+
+class ConnectivityNativeBinderTest : public ::testing::Test {
+ public:
+ std::vector<int32_t> mActualBlockedPorts;
+
+ ConnectivityNativeBinderTest() {
+ AIBinder* binder = AServiceManager_getService("connectivity_native");
+ ndk::SpAIBinder sBinder = ndk::SpAIBinder(binder);
+ mService = aidl::android::net::connectivity::aidl::IConnectivityNative::fromBinder(sBinder);
+ }
+
+ void SetUp() override {
+ // Skip test case if not on T.
+ if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() <<
+ "Should be at least T device.";
+
+ ASSERT_NE(nullptr, mService.get());
+
+ // If there are already ports being blocked on device unblockAllPortsForBind() store
+ // the currently blocked ports and add them back at the end of the test. Do this for
+ // every test case so additional test cases do not forget to add ports back.
+ ndk::ScopedAStatus status = mService->getPortsBlockedForBind(&mActualBlockedPorts);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+
+ }
+
+ void TearDown() override {
+ ndk::ScopedAStatus status;
+ if (mActualBlockedPorts.size() > 0) {
+ for (int i : mActualBlockedPorts) {
+ mService->blockPortForBind(i);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ }
+ }
+ }
+
+ protected:
+ std::shared_ptr<IConnectivityNative> mService;
+
+ void runSocketTest (sa_family_t family, const int type, bool blockPort) {
+ ndk::ScopedAStatus status;
+ in_port_t port = 0;
+ int sock, sock2;
+ // Open two sockets with SO_REUSEADDR and expect they can both bind to port.
+ sock = openSocket(&port, family, type, false /* expectBindFail */);
+ sock2 = openSocket(&port, family, type, false /* expectBindFail */);
+
+ int blockedPort = 0;
+ if (blockPort) {
+ blockedPort = ntohs(port);
+ status = mService->blockPortForBind(blockedPort);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ }
+
+ int sock3 = openSocket(&port, family, type, blockPort /* expectBindFail */);
+
+ if (blockPort) {
+ EXPECT_EQ(-1, sock3);
+ status = mService->unblockPortForBind(blockedPort);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ } else {
+ EXPECT_NE(-1, sock3);
+ }
+
+ close(sock);
+ close(sock2);
+ close(sock3);
+ }
+
+ /*
+ * Open the socket and update the port.
+ */
+ int openSocket(in_port_t* port, sa_family_t family, const int type, bool expectBindFail) {
+ int ret = 0;
+ int enable = 1;
+ const int sock = socket(family, type, 0);
+ ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
+ EXPECT_EQ(0, ret);
+
+ if (family == AF_INET) {
+ struct sockaddr_in addr4 = { .sin_family = family, .sin_port = htons(*port) };
+ ret = bind(sock, (struct sockaddr*) &addr4, sizeof(addr4));
+ } else {
+ struct sockaddr_in6 addr6 = { .sin6_family = family, .sin6_port = htons(*port) };
+ ret = bind(sock, (struct sockaddr*) &addr6, sizeof(addr6));
+ }
+
+ if (expectBindFail) {
+ EXPECT_NE(0, ret);
+ // If port is blocked, return here since the port is not needed
+ // for subsequent sockets.
+ close(sock);
+ return -1;
+ }
+ EXPECT_EQ(0, ret) << "bind unexpectedly failed, errno: " << errno;
+
+ if (family == AF_INET) {
+ struct sockaddr_in sin;
+ socklen_t len = sizeof(sin);
+ EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+ EXPECT_NE(0, ntohs(sin.sin_port));
+ if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin_port));
+ *port = ntohs(sin.sin_port);
+ } else {
+ struct sockaddr_in6 sin;
+ socklen_t len = sizeof(sin);
+ EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len));
+ EXPECT_NE(0, ntohs(sin.sin6_port));
+ if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin6_port));
+ *port = ntohs(sin.sin6_port);
+ }
+ return sock;
+ }
+};
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Udp) {
+ runSocketTest(AF_INET, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Tcp) {
+ runSocketTest(AF_INET, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Udp) {
+ runSocketTest(AF_INET6, SOCK_DGRAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Tcp) {
+ runSocketTest(AF_INET6, SOCK_STREAM, false);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Udp) {
+ runSocketTest(AF_INET, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort4Tcp) {
+ runSocketTest(AF_INET, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Udp) {
+ runSocketTest(AF_INET6, SOCK_DGRAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPort6Tcp) {
+ runSocketTest(AF_INET6, SOCK_STREAM, true);
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
+ ndk::ScopedAStatus status = mService->blockPortForBind(5555);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ status = mService->blockPortForBind(5555);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ status = mService->unblockPortForBind(5555);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+}
+
+TEST_F(ConnectivityNativeBinderTest, GetBlockedPorts) {
+ ndk::ScopedAStatus status;
+ std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+ for (int i : blockedPorts) {
+ status = mService->blockPortForBind(i);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ }
+ std::vector<int32_t> actualBlockedPorts;
+ status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ EXPECT_FALSE(actualBlockedPorts.empty());
+ EXPECT_EQ(blockedPorts, actualBlockedPorts);
+
+ // Remove the ports we added.
+ status = mService->unblockAllPortsForBind();
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ EXPECT_TRUE(actualBlockedPorts.empty());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockAllPorts) {
+ ndk::ScopedAStatus status;
+ std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
+
+ if (mActualBlockedPorts.size() > 0) {
+ status = mService->unblockAllPortsForBind();
+ }
+
+ for (int i : blockedPorts) {
+ status = mService->blockPortForBind(i);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ }
+
+ std::vector<int32_t> actualBlockedPorts;
+ status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ EXPECT_FALSE(actualBlockedPorts.empty());
+
+ status = mService->unblockAllPortsForBind();
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ status = mService->getPortsBlockedForBind(&actualBlockedPorts);
+ EXPECT_TRUE(status.isOk()) << status.getDescription ();
+ EXPECT_TRUE(actualBlockedPorts.empty());
+ // If mActualBlockedPorts is not empty, ports will be added back in teardown.
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockNegativePort) {
+ int retry = 0;
+ ndk::ScopedAStatus status;
+ do {
+ status = mService->blockPortForBind(-1);
+ // TODO: find out why transaction failed is being thrown on the first attempt.
+ } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+ EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockNegativePort) {
+ int retry = 0;
+ ndk::ScopedAStatus status;
+ do {
+ status = mService->unblockPortForBind(-1);
+ // TODO: find out why transaction failed is being thrown on the first attempt.
+ } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+ EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, BlockMaxPort) {
+ int retry = 0;
+ ndk::ScopedAStatus status;
+ do {
+ status = mService->blockPortForBind(65536);
+ // TODO: find out why transaction failed is being thrown on the first attempt.
+ } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+ EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, UnblockMaxPort) {
+ int retry = 0;
+ ndk::ScopedAStatus status;
+ do {
+ status = mService->unblockPortForBind(65536);
+ // TODO: find out why transaction failed is being thrown on the first attempt.
+ } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+ EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
+}
+
+TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
+ int retry = 0;
+ int curUid = getuid();
+ EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
+ ndk::ScopedAStatus status;
+ do {
+ status = mService->blockPortForBind(5555);
+ // TODO: find out why transaction failed is being thrown on the first attempt.
+ } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+ EXPECT_EQ(EX_SECURITY, status.getExceptionCode());
+ EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
+}