diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp index dd04d6c8a9..586923c78f 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp @@ -69,9 +69,10 @@ apex { ], 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 6718402652..1fe0e9acc0 100644 --- a/bpf_progs/Android.bp +++ b/bpf_progs/Android.bp @@ -47,6 +47,7 @@ cc_library_headers { "//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", @@ -57,6 +58,16 @@ cc_library_headers { // // 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"], diff --git a/bpf_progs/block.c b/bpf_progs/block.c new file mode 100644 index 0000000000..ddd9a1cbed --- /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 +#include +#include +#include + +#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 fa86f3965a..e4efa9261b 100644 --- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java +++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java @@ -21,6 +21,7 @@ import android.util.Log; 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 @@ import com.android.server.nearby.NearbyService; */ 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 @@ public final class ConnectivityServiceInitializer extends SystemService { mEthernetServiceImpl = createEthernetService(context); mConnectivity = new ConnectivityService(context); mIpSecService = createIpSecService(context); + mConnectivityNative = createConnectivityNativeService(context); mNsdService = createNsdService(context); mNearbyService = createNearbyService(context); } @@ -65,6 +68,12 @@ public final class ConnectivityServiceInitializer extends SystemService { 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 @@ public final class ConnectivityServiceInitializer extends SystemService { 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 0e6fe92b3b..25b970a4d5 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -19,6 +19,54 @@ package { 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 @@ cc_library_shared { ], static_libs: [ "libnet_utils_device_common_bpfjni", + "libnet_utils_device_common_bpfutils", ], shared_libs: [ "liblog", @@ -109,6 +158,7 @@ java_library { 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 0000000000..4625b4b942 --- /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 0000000000..b3985a470f --- /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 -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 0000000000..b3985a470f --- /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 -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 0000000000..31e24b4004 --- /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 2f09e55864..d91eb0305c 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 @@ namespace android { 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 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { 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 0000000000..cde6ea75e2 --- /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 portMap = new ArrayList(); + 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 0000000000..cd438f66c2 --- /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 0000000000..8b089ab581 --- /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 +#include // FIRST_APPLICATION_UID +#include +#include +#include +#include + +#include + +using aidl::android::net::connectivity::aidl::IConnectivityNative; + +class ConnectivityNativeBinderTest : public ::testing::Test { + public: + std::vector 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 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 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 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 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 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); +}